<?xml version="1.0" encoding="UTF-8"?><rss version="2.0"
	xmlns:content="http://purl.org/rss/1.0/modules/content/"
	xmlns:wfw="http://wellformedweb.org/CommentAPI/"
	xmlns:dc="http://purl.org/dc/elements/1.1/"
	xmlns:atom="http://www.w3.org/2005/Atom"
	xmlns:sy="http://purl.org/rss/1.0/modules/syndication/"
	xmlns:slash="http://purl.org/rss/1.0/modules/slash/"
	>

<channel>
	<title>润物无声 &#187; Training</title>
	<atom:link href="http://blog.zhourunsheng.com/tag/training/feed/" rel="self" type="application/rss+xml" />
	<link>http://blog.zhourunsheng.com</link>
	<description>天空一朵雨做的云</description>
	<lastBuildDate>Sat, 08 May 2021 05:17:21 +0000</lastBuildDate>
	<language>zh-CN</language>
	<sy:updatePeriod>hourly</sy:updatePeriod>
	<sy:updateFrequency>1</sy:updateFrequency>
	<generator>https://wordpress.org/?v=4.1.41</generator>
	<item>
		<title>Android 多APK设计专题之为不同的API Level创建不同的应用APK</title>
		<link>http://blog.zhourunsheng.com/2012/02/android-%e5%a4%9aapk%e8%ae%be%e8%ae%a1%e4%b8%93%e9%a2%98%e4%b9%8b%e4%b8%ba%e4%b8%8d%e5%90%8c%e7%9a%84api-level%e5%88%9b%e5%bb%ba%e4%b8%8d%e5%90%8c%e7%9a%84%e5%ba%94%e7%94%a8apk/</link>
		<comments>http://blog.zhourunsheng.com/2012/02/android-%e5%a4%9aapk%e8%ae%be%e8%ae%a1%e4%b8%93%e9%a2%98%e4%b9%8b%e4%b8%ba%e4%b8%8d%e5%90%8c%e7%9a%84api-level%e5%88%9b%e5%bb%ba%e4%b8%8d%e5%90%8c%e7%9a%84%e5%ba%94%e7%94%a8apk/#comments</comments>
		<pubDate>Tue, 07 Feb 2012 14:00:32 +0000</pubDate>
		<dc:creator><![CDATA[润物无声]]></dc:creator>
				<category><![CDATA[移动开发]]></category>
		<category><![CDATA[Android]]></category>
		<category><![CDATA[Training]]></category>

		<guid isPermaLink="false">http://blog.zhourunsheng.com/?p=1316</guid>
		<description><![CDATA[<p>多APK支持是 Android Market 的一个新特性，它允许您为同一款应用对应不同的设备API Leve [&#8230;]</p>
<p><a rel="nofollow" href="http://blog.zhourunsheng.com/2012/02/android-%e5%a4%9aapk%e8%ae%be%e8%ae%a1%e4%b8%93%e9%a2%98%e4%b9%8b%e4%b8%ba%e4%b8%8d%e5%90%8c%e7%9a%84api-level%e5%88%9b%e5%bb%ba%e4%b8%8d%e5%90%8c%e7%9a%84%e5%ba%94%e7%94%a8apk/">Android 多APK设计专题之为不同的API Level创建不同的应用APK</a>，首发于<a rel="nofollow" href="http://blog.zhourunsheng.com">润物无声</a>。</p>
]]></description>
				<content:encoded><![CDATA[<p><img class="alignleft" title="training-prof" alt="training-prof" src="http://blog.zhourunsheng.com/wp-content/uploads/2011/12/training-prof-150x150.png" width="150" height="150"/></p>
<p>多APK支持是 Android Market 的一个新特性，它允许您为同一款应用对应不同的设备API Level 创建不同的APK，Android Market 会自动进行管理，通过识别用户设备的系统等级，比如2.2，2.3，4.0等等，然后会选择适合该设备的APK来让用户安装。</p>
<p>为不同的API Level设计的APK，可以进行特别的优化设置，比如GL纹理设置， API级别，屏幕尺寸，或其中的多个组合。</p>
<p>本节课就带领大家熟悉一下多APK的设计思路，您可以从中找到必要的开发工具和开发维护多个版本APK的方法。</p>
<p><span id="more-1316"></span><br />
<h3>确认您需要多个APKs</h3>
<p>当您设计一款应用程序，为了满足多版本多设备之间的需求，在不牺牲向后兼容性的情况下，又可以利用最新版本的新特性，似乎多APK支持是个不错的解决方案。但事实并非如此，您可以通过阅读资料《<a href="http://developer.android.com/guide/market/publishing/multiple-apks.html">Using Single APK Instead</a> 》和《<a href="http://android-developers.blogspot.com/2010/07/how-to-have-your-cupcake-and-eat-it-too.html">this article</a>》来学习怎样在单一APK中实现上面提到的功能需求，以及怎样利用我们的支持库，同时也可以了解一下如何来编码实现。</p>
<p>使用单一APK有如下优势：</p>
<ul>
<li>发布和测试更容易</li>
<li>只需要维护一个代码库</li>
<li>应用程序能够自适应设备配置的变化</li>
<li>应用恢复功能跨设备通用</li>
<li>应用升级容易</li>
</ul>
<p>本文的余下章节，假定您已经认为您确实需要多APKs的解决方案，而不使用单一 APK 的方法。</p>
<h3>图示化需求</h3>
<p>可以通过创建一个简单的图表来确定需要几个APK来支持，以及每个APK与之对应的API Level范围。您可以在Android 平台版本的信息页面（<a href="http://developer.android.com/resources/dashboard/platform-versions.html">Platform Versions</a> ）中找到有用的参考资料，通过这种方法可以快速确定需要的APK个数和每个APK对应的API Level范围，尤其在出现API Level范围有重叠情况的时候更加有效，同时也方便了日后参考。</p>
<p>创建一个水平的列表来表示出当前已经发布的Android 版本号，在列表的最后一个位置放置一个+号来代表未来发布的版本。</p>
<table style="PADDING-BOTTOM: 0px; BORDER-RIGHT-WIDTH: 0px; MARGIN: 0px 0px 1em 1em; PADDING-LEFT: 0px; PADDING-RIGHT: 0px; BORDER-COLLAPSE: collapse; EMPTY-CELLS: show; BORDER-TOP-WIDTH: 0px; BORDER-BOTTOM-WIDTH: 0px; FONT-SIZE: 1em; BORDER-LEFT-WIDTH: 0px; PADDING-TOP: 0px" border="1" cellspacing="0" cellpadding="10">
<tbody style="PADDING-BOTTOM: 0px; BORDER-RIGHT-WIDTH: 0px; MARGIN: 0px; PADDING-LEFT: 0px; PADDING-RIGHT: 0px; BORDER-TOP-WIDTH: 0px; BORDER-BOTTOM-WIDTH: 0px; BORDER-LEFT-WIDTH: 0px; PADDING-TOP: 0px">
<tr style="PADDING-BOTTOM: 0px; BORDER-RIGHT-WIDTH: 0px; MARGIN: 0px; PADDING-LEFT: 0px; PADDING-RIGHT: 0px; BORDER-TOP-WIDTH: 0px; BORDER-BOTTOM-WIDTH: 0px; BORDER-LEFT-WIDTH: 0px; PADDING-TOP: 0px">
<td style="BORDER-BOTTOM: rgb(204,204,204) 1px solid; TEXT-ALIGN: left; BORDER-LEFT: rgb(204,204,204) 1px solid; PADDING-BOTTOM: 6px; MARGIN: 0px; PADDING-LEFT: 12px; PADDING-RIGHT: 12px; VERTICAL-ALIGN: top; BORDER-TOP: rgb(204,204,204) 1px solid; BORDER-RIGHT: rgb(204,204,204) 1px solid; PADDING-TOP: 6px">3</td>
<td style="BORDER-BOTTOM: rgb(204,204,204) 1px solid; TEXT-ALIGN: left; BORDER-LEFT: rgb(204,204,204) 1px solid; PADDING-BOTTOM: 6px; MARGIN: 0px; PADDING-LEFT: 12px; PADDING-RIGHT: 12px; VERTICAL-ALIGN: top; BORDER-TOP: rgb(204,204,204) 1px solid; BORDER-RIGHT: rgb(204,204,204) 1px solid; PADDING-TOP: 6px">4</td>
<td style="BORDER-BOTTOM: rgb(204,204,204) 1px solid; TEXT-ALIGN: left; BORDER-LEFT: rgb(204,204,204) 1px solid; PADDING-BOTTOM: 6px; MARGIN: 0px; PADDING-LEFT: 12px; PADDING-RIGHT: 12px; VERTICAL-ALIGN: top; BORDER-TOP: rgb(204,204,204) 1px solid; BORDER-RIGHT: rgb(204,204,204) 1px solid; PADDING-TOP: 6px">5</td>
<td style="BORDER-BOTTOM: rgb(204,204,204) 1px solid; TEXT-ALIGN: left; BORDER-LEFT: rgb(204,204,204) 1px solid; PADDING-BOTTOM: 6px; MARGIN: 0px; PADDING-LEFT: 12px; PADDING-RIGHT: 12px; VERTICAL-ALIGN: top; BORDER-TOP: rgb(204,204,204) 1px solid; BORDER-RIGHT: rgb(204,204,204) 1px solid; PADDING-TOP: 6px">6</td>
<td style="BORDER-BOTTOM: rgb(204,204,204) 1px solid; TEXT-ALIGN: left; BORDER-LEFT: rgb(204,204,204) 1px solid; PADDING-BOTTOM: 6px; MARGIN: 0px; PADDING-LEFT: 12px; PADDING-RIGHT: 12px; VERTICAL-ALIGN: top; BORDER-TOP: rgb(204,204,204) 1px solid; BORDER-RIGHT: rgb(204,204,204) 1px solid; PADDING-TOP: 6px">7</td>
<td style="BORDER-BOTTOM: rgb(204,204,204) 1px solid; TEXT-ALIGN: left; BORDER-LEFT: rgb(204,204,204) 1px solid; PADDING-BOTTOM: 6px; MARGIN: 0px; PADDING-LEFT: 12px; PADDING-RIGHT: 12px; VERTICAL-ALIGN: top; BORDER-TOP: rgb(204,204,204) 1px solid; BORDER-RIGHT: rgb(204,204,204) 1px solid; PADDING-TOP: 6px">8</td>
<td style="BORDER-BOTTOM: rgb(204,204,204) 1px solid; TEXT-ALIGN: left; BORDER-LEFT: rgb(204,204,204) 1px solid; PADDING-BOTTOM: 6px; MARGIN: 0px; PADDING-LEFT: 12px; PADDING-RIGHT: 12px; VERTICAL-ALIGN: top; BORDER-TOP: rgb(204,204,204) 1px solid; BORDER-RIGHT: rgb(204,204,204) 1px solid; PADDING-TOP: 6px">9</td>
<td style="BORDER-BOTTOM: rgb(204,204,204) 1px solid; TEXT-ALIGN: left; BORDER-LEFT: rgb(204,204,204) 1px solid; PADDING-BOTTOM: 6px; MARGIN: 0px; PADDING-LEFT: 12px; PADDING-RIGHT: 12px; VERTICAL-ALIGN: top; BORDER-TOP: rgb(204,204,204) 1px solid; BORDER-RIGHT: rgb(204,204,204) 1px solid; PADDING-TOP: 6px">10</td>
<td style="BORDER-BOTTOM: rgb(204,204,204) 1px solid; TEXT-ALIGN: left; BORDER-LEFT: rgb(204,204,204) 1px solid; PADDING-BOTTOM: 6px; MARGIN: 0px; PADDING-LEFT: 12px; PADDING-RIGHT: 12px; VERTICAL-ALIGN: top; BORDER-TOP: rgb(204,204,204) 1px solid; BORDER-RIGHT: rgb(204,204,204) 1px solid; PADDING-TOP: 6px">11</td>
<td style="BORDER-BOTTOM: rgb(204,204,204) 1px solid; TEXT-ALIGN: left; BORDER-LEFT: rgb(204,204,204) 1px solid; PADDING-BOTTOM: 6px; MARGIN: 0px; PADDING-LEFT: 12px; PADDING-RIGHT: 12px; VERTICAL-ALIGN: top; BORDER-TOP: rgb(204,204,204) 1px solid; BORDER-RIGHT: rgb(204,204,204) 1px solid; PADDING-TOP: 6px">12</td>
<td style="BORDER-BOTTOM: rgb(204,204,204) 1px solid; TEXT-ALIGN: left; BORDER-LEFT: rgb(204,204,204) 1px solid; PADDING-BOTTOM: 6px; MARGIN: 0px; PADDING-LEFT: 12px; PADDING-RIGHT: 12px; VERTICAL-ALIGN: top; BORDER-TOP: rgb(204,204,204) 1px solid; BORDER-RIGHT: rgb(204,204,204) 1px solid; PADDING-TOP: 6px">13</td>
<td style="BORDER-BOTTOM: rgb(204,204,204) 1px solid; TEXT-ALIGN: left; BORDER-LEFT: rgb(204,204,204) 1px solid; PADDING-BOTTOM: 6px; MARGIN: 0px; PADDING-LEFT: 12px; PADDING-RIGHT: 12px; VERTICAL-ALIGN: top; BORDER-TOP: rgb(204,204,204) 1px solid; BORDER-RIGHT: rgb(204,204,204) 1px solid; PADDING-TOP: 6px">+</td>
</tr>
</tbody>
</table>
<p>对上面的列表进行着色，每一种颜色对应一个APK，之后您的团队就可以以此来进行讨论还开发。</p>
<table style="PADDING-BOTTOM: 0px; BORDER-RIGHT-WIDTH: 0px; MARGIN: 0px 0px 1em 1em; PADDING-LEFT: 0px; PADDING-RIGHT: 0px; BORDER-COLLAPSE: collapse; EMPTY-CELLS: show; BORDER-TOP-WIDTH: 0px; BORDER-BOTTOM-WIDTH: 0px; FONT-SIZE: 1em; BORDER-LEFT-WIDTH: 0px; PADDING-TOP: 0px" border="1" cellspacing="0" cellpadding="10">
<tbody style="PADDING-BOTTOM: 0px; BORDER-RIGHT-WIDTH: 0px; MARGIN: 0px; PADDING-LEFT: 0px; PADDING-RIGHT: 0px; BORDER-TOP-WIDTH: 0px; BORDER-BOTTOM-WIDTH: 0px; BORDER-LEFT-WIDTH: 0px; PADDING-TOP: 0px">
<tr style="PADDING-BOTTOM: 0px; BORDER-RIGHT-WIDTH: 0px; MARGIN: 0px; PADDING-LEFT: 0px; PADDING-RIGHT: 0px; BORDER-TOP-WIDTH: 0px; BORDER-BOTTOM-WIDTH: 0px; BORDER-LEFT-WIDTH: 0px; PADDING-TOP: 0px">
<td style="BORDER-BOTTOM: rgb(204,204,204) 1px solid; TEXT-ALIGN: left; BORDER-LEFT: rgb(204,204,204) 1px solid; PADDING-BOTTOM: 6px; BACKGROUND-COLOR: rgb(159,197,232); MARGIN: 0px; PADDING-LEFT: 12px; PADDING-RIGHT: 12px; VERTICAL-ALIGN: top; BORDER-TOP: rgb(204,204,204) 1px solid; BORDER-RIGHT: rgb(204,204,204) 1px solid; PADDING-TOP: 6px" class="blueCell">3</td>
<td style="BORDER-BOTTOM: rgb(204,204,204) 1px solid; TEXT-ALIGN: left; BORDER-LEFT: rgb(204,204,204) 1px solid; PADDING-BOTTOM: 6px; BACKGROUND-COLOR: rgb(159,197,232); MARGIN: 0px; PADDING-LEFT: 12px; PADDING-RIGHT: 12px; VERTICAL-ALIGN: top; BORDER-TOP: rgb(204,204,204) 1px solid; BORDER-RIGHT: rgb(204,204,204) 1px solid; PADDING-TOP: 6px" class="blueCell">4</td>
<td style="BORDER-BOTTOM: rgb(204,204,204) 1px solid; TEXT-ALIGN: left; BORDER-LEFT: rgb(204,204,204) 1px solid; PADDING-BOTTOM: 6px; BACKGROUND-COLOR: rgb(159,197,232); MARGIN: 0px; PADDING-LEFT: 12px; PADDING-RIGHT: 12px; VERTICAL-ALIGN: top; BORDER-TOP: rgb(204,204,204) 1px solid; BORDER-RIGHT: rgb(204,204,204) 1px solid; PADDING-TOP: 6px" class="blueCell">5</td>
<td style="BORDER-BOTTOM: rgb(204,204,204) 1px solid; TEXT-ALIGN: left; BORDER-LEFT: rgb(204,204,204) 1px solid; PADDING-BOTTOM: 6px; BACKGROUND-COLOR: rgb(159,197,232); MARGIN: 0px; PADDING-LEFT: 12px; PADDING-RIGHT: 12px; VERTICAL-ALIGN: top; BORDER-TOP: rgb(204,204,204) 1px solid; BORDER-RIGHT: rgb(204,204,204) 1px solid; PADDING-TOP: 6px" class="blueCell">6</td>
<td style="BORDER-BOTTOM: rgb(204,204,204) 1px solid; TEXT-ALIGN: left; BORDER-LEFT: rgb(204,204,204) 1px solid; PADDING-BOTTOM: 6px; BACKGROUND-COLOR: rgb(182,215,168); MARGIN: 0px; PADDING-LEFT: 12px; PADDING-RIGHT: 12px; VERTICAL-ALIGN: top; BORDER-TOP: rgb(204,204,204) 1px solid; BORDER-RIGHT: rgb(204,204,204) 1px solid; PADDING-TOP: 6px" class="greenCell">7</td>
<td style="BORDER-BOTTOM: rgb(204,204,204) 1px solid; TEXT-ALIGN: left; BORDER-LEFT: rgb(204,204,204) 1px solid; PADDING-BOTTOM: 6px; BACKGROUND-COLOR: rgb(182,215,168); MARGIN: 0px; PADDING-LEFT: 12px; PADDING-RIGHT: 12px; VERTICAL-ALIGN: top; BORDER-TOP: rgb(204,204,204) 1px solid; BORDER-RIGHT: rgb(204,204,204) 1px solid; PADDING-TOP: 6px" class="greenCell">8</td>
<td style="BORDER-BOTTOM: rgb(204,204,204) 1px solid; TEXT-ALIGN: left; BORDER-LEFT: rgb(204,204,204) 1px solid; PADDING-BOTTOM: 6px; BACKGROUND-COLOR: rgb(182,215,168); MARGIN: 0px; PADDING-LEFT: 12px; PADDING-RIGHT: 12px; VERTICAL-ALIGN: top; BORDER-TOP: rgb(204,204,204) 1px solid; BORDER-RIGHT: rgb(204,204,204) 1px solid; PADDING-TOP: 6px" class="greenCell">9</td>
<td style="BORDER-BOTTOM: rgb(204,204,204) 1px solid; TEXT-ALIGN: left; BORDER-LEFT: rgb(204,204,204) 1px solid; PADDING-BOTTOM: 6px; BACKGROUND-COLOR: rgb(182,215,168); MARGIN: 0px; PADDING-LEFT: 12px; PADDING-RIGHT: 12px; VERTICAL-ALIGN: top; BORDER-TOP: rgb(204,204,204) 1px solid; BORDER-RIGHT: rgb(204,204,204) 1px solid; PADDING-TOP: 6px" class="greenCell">10</td>
<td style="BORDER-BOTTOM: rgb(204,204,204) 1px solid; TEXT-ALIGN: left; BORDER-LEFT: rgb(204,204,204) 1px solid; PADDING-BOTTOM: 6px; BACKGROUND-COLOR: rgb(234,153,153); MARGIN: 0px; PADDING-LEFT: 12px; PADDING-RIGHT: 12px; VERTICAL-ALIGN: top; BORDER-TOP: rgb(204,204,204) 1px solid; BORDER-RIGHT: rgb(204,204,204) 1px solid; PADDING-TOP: 6px" class="redCell">11</td>
<td style="BORDER-BOTTOM: rgb(204,204,204) 1px solid; TEXT-ALIGN: left; BORDER-LEFT: rgb(204,204,204) 1px solid; PADDING-BOTTOM: 6px; BACKGROUND-COLOR: rgb(234,153,153); MARGIN: 0px; PADDING-LEFT: 12px; PADDING-RIGHT: 12px; VERTICAL-ALIGN: top; BORDER-TOP: rgb(204,204,204) 1px solid; BORDER-RIGHT: rgb(204,204,204) 1px solid; PADDING-TOP: 6px" class="redCell">12</td>
<td style="BORDER-BOTTOM: rgb(204,204,204) 1px solid; TEXT-ALIGN: left; BORDER-LEFT: rgb(204,204,204) 1px solid; PADDING-BOTTOM: 6px; BACKGROUND-COLOR: rgb(234,153,153); MARGIN: 0px; PADDING-LEFT: 12px; PADDING-RIGHT: 12px; VERTICAL-ALIGN: top; BORDER-TOP: rgb(204,204,204) 1px solid; BORDER-RIGHT: rgb(204,204,204) 1px solid; PADDING-TOP: 6px" class="redCell">13</td>
<td style="BORDER-BOTTOM: rgb(204,204,204) 1px solid; TEXT-ALIGN: left; BORDER-LEFT: rgb(204,204,204) 1px solid; PADDING-BOTTOM: 6px; BACKGROUND-COLOR: rgb(234,153,153); MARGIN: 0px; PADDING-LEFT: 12px; PADDING-RIGHT: 12px; VERTICAL-ALIGN: top; BORDER-TOP: rgb(204,204,204) 1px solid; BORDER-RIGHT: rgb(204,204,204) 1px solid; PADDING-TOP: 6px" class="redCell">+</td>
</tr>
</tbody>
</table>
<h3>设计公共库</h3>
<p>无论是修改已有的代码做二次开发，还是从头开发一个新的应用，第一件事情就是提取公共的代码库，这些代码可以被不同的应用使用，并且只需要在一处编写和维护，比如多语言支持（language-localized strings），颜色主题（color themes），bug修复代码等，提供公共代码可以大大提高开发效率和节省开发时间，以及降低更新代码时引入的错误。</p>
<p>注：如何提取和创建公共的代码库，可参照下面的文章</p>
<ol>
<li><a href="http://developer.android.com/guide/developing/projects/projects-eclipse.html#SettingUpLibraryProject">Setting up a library project (Eclipse)</a></li>
<li><a href="http://developer.android.com/guide/developing/projects/projects-cmdline.html#SettingUpLibraryProject">Setting up a library project (Command line)</a></li>
</ol>
<p>如果您打算将已开发的单一APK转化为多APK支持，那么以下资源通常可以放置到公共代码库中，比如字符串资源文件（localized string file），常量值（list of values），颜色定义（theme colors），菜单图标（menu icons ）和可共用的布局文件（layout ）。或者，您打算从头开发一个应用，这时候也应该先开发公共的代码库，然后将各个APK不同的特定代码部分移动到具体的APK中，便于代码的管理和以后的扩展，当开发过一段时间之后，您可以逆向从各个不同的APK中再提取公共的部分来更新公共代码库。</p>
<h3>建立工程项目</h3>
<p>每一个独立的APK对应于一个独立的工程文件夹，为了方便代码的组织和管理，所有APK项目的工程文件夹和公共项目文件夹应该放在同一个根目录下面，并且所有的APK项目应该是相同的包名（package name）。示例代码展示了建立上面列举的3个独立APK的项目文件夹，组织结构如下：</p>
<pre>   alexlucas:~/code/multi-apks-root$ ls
   foo-blue
   foo-green
   foo-lib
   foo-red</pre>
<p>工程目录建立好以后，每个独立的APK项目都可以引用公共项目库，一般将启动Activity（starting Activity ）放在公共项目库中，然后独立的APK工程中可以继承和扩展该Activity，这样便于将初始化代码集中在一个地方，例如 initializing Analytics, running licensing checks, and any other initialization procedures。</p>
<h3>配置 Manifests 文件</h3>
<p>当用户通过Android Market下载支持多APK的应用时，Market过滤器会依据如下的规则来选择合适的APK版本：</p>
<ul>
<li>manifest 中声明的版本号必须匹配</li>
<li>manifest 中声明匹配的所有版本号中最大者优先</li>
</ul>
<p>用我们前面讨论过的例子来进行说明，假如 manifest 中未设置支持的最大API等级（max API level） 选项，则每一个APK对应的API Level 如下：</p>
<table style="PADDING-BOTTOM: 0px; BORDER-RIGHT-WIDTH: 0px; MARGIN: 0px 0px 1em 1em; PADDING-LEFT: 0px; PADDING-RIGHT: 0px; BORDER-COLLAPSE: collapse; EMPTY-CELLS: show; BORDER-TOP-WIDTH: 0px; BORDER-BOTTOM-WIDTH: 0px; FONT-SIZE: 1em; BORDER-LEFT-WIDTH: 0px; PADDING-TOP: 0px" border="1" cellspacing="0" cellpadding="10">
<tbody style="PADDING-BOTTOM: 0px; BORDER-RIGHT-WIDTH: 0px; MARGIN: 0px; PADDING-LEFT: 0px; PADDING-RIGHT: 0px; BORDER-TOP-WIDTH: 0px; BORDER-BOTTOM-WIDTH: 0px; BORDER-LEFT-WIDTH: 0px; PADDING-TOP: 0px">
<tr style="PADDING-BOTTOM: 0px; BORDER-RIGHT-WIDTH: 0px; MARGIN: 0px; PADDING-LEFT: 0px; PADDING-RIGHT: 0px; BORDER-TOP-WIDTH: 0px; BORDER-BOTTOM-WIDTH: 0px; BORDER-LEFT-WIDTH: 0px; PADDING-TOP: 0px">
<td style="BORDER-BOTTOM: rgb(204,204,204) 1px solid; TEXT-ALIGN: left; BORDER-LEFT: rgb(204,204,204) 1px solid; PADDING-BOTTOM: 6px; BACKGROUND-COLOR: rgb(159,197,232); MARGIN: 0px; PADDING-LEFT: 12px; PADDING-RIGHT: 12px; VERTICAL-ALIGN: top; BORDER-TOP: rgb(204,204,204) 1px solid; BORDER-RIGHT: rgb(204,204,204) 1px solid; PADDING-TOP: 6px" class="blueCell">3</td>
<td style="BORDER-BOTTOM: rgb(204,204,204) 1px solid; TEXT-ALIGN: left; BORDER-LEFT: rgb(204,204,204) 1px solid; PADDING-BOTTOM: 6px; BACKGROUND-COLOR: rgb(159,197,232); MARGIN: 0px; PADDING-LEFT: 12px; PADDING-RIGHT: 12px; VERTICAL-ALIGN: top; BORDER-TOP: rgb(204,204,204) 1px solid; BORDER-RIGHT: rgb(204,204,204) 1px solid; PADDING-TOP: 6px" class="blueCell">4</td>
<td style="BORDER-BOTTOM: rgb(204,204,204) 1px solid; TEXT-ALIGN: left; BORDER-LEFT: rgb(204,204,204) 1px solid; PADDING-BOTTOM: 6px; BACKGROUND-COLOR: rgb(159,197,232); MARGIN: 0px; PADDING-LEFT: 12px; PADDING-RIGHT: 12px; VERTICAL-ALIGN: top; BORDER-TOP: rgb(204,204,204) 1px solid; BORDER-RIGHT: rgb(204,204,204) 1px solid; PADDING-TOP: 6px" class="blueCell">5</td>
<td style="BORDER-BOTTOM: rgb(204,204,204) 1px solid; TEXT-ALIGN: left; BORDER-LEFT: rgb(204,204,204) 1px solid; PADDING-BOTTOM: 6px; BACKGROUND-COLOR: rgb(159,197,232); MARGIN: 0px; PADDING-LEFT: 12px; PADDING-RIGHT: 12px; VERTICAL-ALIGN: top; BORDER-TOP: rgb(204,204,204) 1px solid; BORDER-RIGHT: rgb(204,204,204) 1px solid; PADDING-TOP: 6px" class="blueCell">6</td>
<td style="BORDER-BOTTOM: rgb(204,204,204) 1px solid; TEXT-ALIGN: left; BORDER-LEFT: rgb(204,204,204) 1px solid; PADDING-BOTTOM: 6px; BACKGROUND-COLOR: rgb(159,197,232); MARGIN: 0px; PADDING-LEFT: 12px; PADDING-RIGHT: 12px; VERTICAL-ALIGN: top; BORDER-TOP: rgb(204,204,204) 1px solid; BORDER-RIGHT: rgb(204,204,204) 1px solid; PADDING-TOP: 6px" class="blueCell">7</td>
<td style="BORDER-BOTTOM: rgb(204,204,204) 1px solid; TEXT-ALIGN: left; BORDER-LEFT: rgb(204,204,204) 1px solid; PADDING-BOTTOM: 6px; BACKGROUND-COLOR: rgb(159,197,232); MARGIN: 0px; PADDING-LEFT: 12px; PADDING-RIGHT: 12px; VERTICAL-ALIGN: top; BORDER-TOP: rgb(204,204,204) 1px solid; BORDER-RIGHT: rgb(204,204,204) 1px solid; PADDING-TOP: 6px" class="blueCell">8</td>
<td style="BORDER-BOTTOM: rgb(204,204,204) 1px solid; TEXT-ALIGN: left; BORDER-LEFT: rgb(204,204,204) 1px solid; PADDING-BOTTOM: 6px; BACKGROUND-COLOR: rgb(159,197,232); MARGIN: 0px; PADDING-LEFT: 12px; PADDING-RIGHT: 12px; VERTICAL-ALIGN: top; BORDER-TOP: rgb(204,204,204) 1px solid; BORDER-RIGHT: rgb(204,204,204) 1px solid; PADDING-TOP: 6px" class="blueCell">9</td>
<td style="BORDER-BOTTOM: rgb(204,204,204) 1px solid; TEXT-ALIGN: left; BORDER-LEFT: rgb(204,204,204) 1px solid; PADDING-BOTTOM: 6px; BACKGROUND-COLOR: rgb(159,197,232); MARGIN: 0px; PADDING-LEFT: 12px; PADDING-RIGHT: 12px; VERTICAL-ALIGN: top; BORDER-TOP: rgb(204,204,204) 1px solid; BORDER-RIGHT: rgb(204,204,204) 1px solid; PADDING-TOP: 6px" class="blueCell">10</td>
<td style="BORDER-BOTTOM: rgb(204,204,204) 1px solid; TEXT-ALIGN: left; BORDER-LEFT: rgb(204,204,204) 1px solid; PADDING-BOTTOM: 6px; BACKGROUND-COLOR: rgb(159,197,232); MARGIN: 0px; PADDING-LEFT: 12px; PADDING-RIGHT: 12px; VERTICAL-ALIGN: top; BORDER-TOP: rgb(204,204,204) 1px solid; BORDER-RIGHT: rgb(204,204,204) 1px solid; PADDING-TOP: 6px" class="blueCell">11</td>
<td style="BORDER-BOTTOM: rgb(204,204,204) 1px solid; TEXT-ALIGN: left; BORDER-LEFT: rgb(204,204,204) 1px solid; PADDING-BOTTOM: 6px; BACKGROUND-COLOR: rgb(159,197,232); MARGIN: 0px; PADDING-LEFT: 12px; PADDING-RIGHT: 12px; VERTICAL-ALIGN: top; BORDER-TOP: rgb(204,204,204) 1px solid; BORDER-RIGHT: rgb(204,204,204) 1px solid; PADDING-TOP: 6px" class="blueCell">12</td>
<td style="BORDER-BOTTOM: rgb(204,204,204) 1px solid; TEXT-ALIGN: left; BORDER-LEFT: rgb(204,204,204) 1px solid; PADDING-BOTTOM: 6px; BACKGROUND-COLOR: rgb(159,197,232); MARGIN: 0px; PADDING-LEFT: 12px; PADDING-RIGHT: 12px; VERTICAL-ALIGN: top; BORDER-TOP: rgb(204,204,204) 1px solid; BORDER-RIGHT: rgb(204,204,204) 1px solid; PADDING-TOP: 6px" class="blueCell">13</td>
<td style="BORDER-BOTTOM: rgb(204,204,204) 1px solid; TEXT-ALIGN: left; BORDER-LEFT: rgb(204,204,204) 1px solid; PADDING-BOTTOM: 6px; BACKGROUND-COLOR: rgb(159,197,232); MARGIN: 0px; PADDING-LEFT: 12px; PADDING-RIGHT: 12px; VERTICAL-ALIGN: top; BORDER-TOP: rgb(204,204,204) 1px solid; BORDER-RIGHT: rgb(204,204,204) 1px solid; PADDING-TOP: 6px" class="blueCell">+</td>
</tr>
<tr style="PADDING-BOTTOM: 0px; BORDER-RIGHT-WIDTH: 0px; MARGIN: 0px; PADDING-LEFT: 0px; PADDING-RIGHT: 0px; BORDER-TOP-WIDTH: 0px; BORDER-BOTTOM-WIDTH: 0px; BORDER-LEFT-WIDTH: 0px; PADDING-TOP: 0px">
<td style="BORDER-BOTTOM: rgb(204,204,204) 1px solid; TEXT-ALIGN: left; BORDER-LEFT: rgb(204,204,204) 1px solid; PADDING-BOTTOM: 6px; BACKGROUND-COLOR: rgb(0,0,0); MARGIN: 0px; PADDING-LEFT: 12px; PADDING-RIGHT: 12px; VERTICAL-ALIGN: top; BORDER-TOP: rgb(204,204,204) 1px solid; BORDER-RIGHT: rgb(204,204,204) 1px solid; PADDING-TOP: 6px" class="blackCell">3</td>
<td style="BORDER-BOTTOM: rgb(204,204,204) 1px solid; TEXT-ALIGN: left; BORDER-LEFT: rgb(204,204,204) 1px solid; PADDING-BOTTOM: 6px; BACKGROUND-COLOR: rgb(0,0,0); MARGIN: 0px; PADDING-LEFT: 12px; PADDING-RIGHT: 12px; VERTICAL-ALIGN: top; BORDER-TOP: rgb(204,204,204) 1px solid; BORDER-RIGHT: rgb(204,204,204) 1px solid; PADDING-TOP: 6px" class="blackCell">4</td>
<td style="BORDER-BOTTOM: rgb(204,204,204) 1px solid; TEXT-ALIGN: left; BORDER-LEFT: rgb(204,204,204) 1px solid; PADDING-BOTTOM: 6px; BACKGROUND-COLOR: rgb(0,0,0); MARGIN: 0px; PADDING-LEFT: 12px; PADDING-RIGHT: 12px; VERTICAL-ALIGN: top; BORDER-TOP: rgb(204,204,204) 1px solid; BORDER-RIGHT: rgb(204,204,204) 1px solid; PADDING-TOP: 6px" class="blackCell">5</td>
<td style="BORDER-BOTTOM: rgb(204,204,204) 1px solid; TEXT-ALIGN: left; BORDER-LEFT: rgb(204,204,204) 1px solid; PADDING-BOTTOM: 6px; BACKGROUND-COLOR: rgb(0,0,0); MARGIN: 0px; PADDING-LEFT: 12px; PADDING-RIGHT: 12px; VERTICAL-ALIGN: top; BORDER-TOP: rgb(204,204,204) 1px solid; BORDER-RIGHT: rgb(204,204,204) 1px solid; PADDING-TOP: 6px" class="blackCell">6</td>
<td style="BORDER-BOTTOM: rgb(204,204,204) 1px solid; TEXT-ALIGN: left; BORDER-LEFT: rgb(204,204,204) 1px solid; PADDING-BOTTOM: 6px; BACKGROUND-COLOR: rgb(182,215,168); MARGIN: 0px; PADDING-LEFT: 12px; PADDING-RIGHT: 12px; VERTICAL-ALIGN: top; BORDER-TOP: rgb(204,204,204) 1px solid; BORDER-RIGHT: rgb(204,204,204) 1px solid; PADDING-TOP: 6px" class="greenCell">7</td>
<td style="BORDER-BOTTOM: rgb(204,204,204) 1px solid; TEXT-ALIGN: left; BORDER-LEFT: rgb(204,204,204) 1px solid; PADDING-BOTTOM: 6px; BACKGROUND-COLOR: rgb(182,215,168); MARGIN: 0px; PADDING-LEFT: 12px; PADDING-RIGHT: 12px; VERTICAL-ALIGN: top; BORDER-TOP: rgb(204,204,204) 1px solid; BORDER-RIGHT: rgb(204,204,204) 1px solid; PADDING-TOP: 6px" class="greenCell">8</td>
<td style="BORDER-BOTTOM: rgb(204,204,204) 1px solid; TEXT-ALIGN: left; BORDER-LEFT: rgb(204,204,204) 1px solid; PADDING-BOTTOM: 6px; BACKGROUND-COLOR: rgb(182,215,168); MARGIN: 0px; PADDING-LEFT: 12px; PADDING-RIGHT: 12px; VERTICAL-ALIGN: top; BORDER-TOP: rgb(204,204,204) 1px solid; BORDER-RIGHT: rgb(204,204,204) 1px solid; PADDING-TOP: 6px" class="greenCell">9</td>
<td style="BORDER-BOTTOM: rgb(204,204,204) 1px solid; TEXT-ALIGN: left; BORDER-LEFT: rgb(204,204,204) 1px solid; PADDING-BOTTOM: 6px; BACKGROUND-COLOR: rgb(182,215,168); MARGIN: 0px; PADDING-LEFT: 12px; PADDING-RIGHT: 12px; VERTICAL-ALIGN: top; BORDER-TOP: rgb(204,204,204) 1px solid; BORDER-RIGHT: rgb(204,204,204) 1px solid; PADDING-TOP: 6px" class="greenCell">10</td>
<td style="BORDER-BOTTOM: rgb(204,204,204) 1px solid; TEXT-ALIGN: left; BORDER-LEFT: rgb(204,204,204) 1px solid; PADDING-BOTTOM: 6px; BACKGROUND-COLOR: rgb(182,215,168); MARGIN: 0px; PADDING-LEFT: 12px; PADDING-RIGHT: 12px; VERTICAL-ALIGN: top; BORDER-TOP: rgb(204,204,204) 1px solid; BORDER-RIGHT: rgb(204,204,204) 1px solid; PADDING-TOP: 6px" class="greenCell">11</td>
<td style="BORDER-BOTTOM: rgb(204,204,204) 1px solid; TEXT-ALIGN: left; BORDER-LEFT: rgb(204,204,204) 1px solid; PADDING-BOTTOM: 6px; BACKGROUND-COLOR: rgb(182,215,168); MARGIN: 0px; PADDING-LEFT: 12px; PADDING-RIGHT: 12px; VERTICAL-ALIGN: top; BORDER-TOP: rgb(204,204,204) 1px solid; BORDER-RIGHT: rgb(204,204,204) 1px solid; PADDING-TOP: 6px" class="greenCell">12</td>
<td style="BORDER-BOTTOM: rgb(204,204,204) 1px solid; TEXT-ALIGN: left; BORDER-LEFT: rgb(204,204,204) 1px solid; PADDING-BOTTOM: 6px; BACKGROUND-COLOR: rgb(182,215,168); MARGIN: 0px; PADDING-LEFT: 12px; PADDING-RIGHT: 12px; VERTICAL-ALIGN: top; BORDER-TOP: rgb(204,204,204) 1px solid; BORDER-RIGHT: rgb(204,204,204) 1px solid; PADDING-TOP: 6px" class="greenCell">13</td>
<td style="BORDER-BOTTOM: rgb(204,204,204) 1px solid; TEXT-ALIGN: left; BORDER-LEFT: rgb(204,204,204) 1px solid; PADDING-BOTTOM: 6px; BACKGROUND-COLOR: rgb(182,215,168); MARGIN: 0px; PADDING-LEFT: 12px; PADDING-RIGHT: 12px; VERTICAL-ALIGN: top; BORDER-TOP: rgb(204,204,204) 1px solid; BORDER-RIGHT: rgb(204,204,204) 1px solid; PADDING-TOP: 6px" class="greenCell">+</td>
</tr>
<tr style="PADDING-BOTTOM: 0px; BORDER-RIGHT-WIDTH: 0px; MARGIN: 0px; PADDING-LEFT: 0px; PADDING-RIGHT: 0px; BORDER-TOP-WIDTH: 0px; BORDER-BOTTOM-WIDTH: 0px; BORDER-LEFT-WIDTH: 0px; PADDING-TOP: 0px">
<td style="BORDER-BOTTOM: rgb(204,204,204) 1px solid; TEXT-ALIGN: left; BORDER-LEFT: rgb(204,204,204) 1px solid; PADDING-BOTTOM: 6px; BACKGROUND-COLOR: rgb(0,0,0); MARGIN: 0px; PADDING-LEFT: 12px; PADDING-RIGHT: 12px; VERTICAL-ALIGN: top; BORDER-TOP: rgb(204,204,204) 1px solid; BORDER-RIGHT: rgb(204,204,204) 1px solid; PADDING-TOP: 6px" class="blackCell">3</td>
<td style="BORDER-BOTTOM: rgb(204,204,204) 1px solid; TEXT-ALIGN: left; BORDER-LEFT: rgb(204,204,204) 1px solid; PADDING-BOTTOM: 6px; BACKGROUND-COLOR: rgb(0,0,0); MARGIN: 0px; PADDING-LEFT: 12px; PADDING-RIGHT: 12px; VERTICAL-ALIGN: top; BORDER-TOP: rgb(204,204,204) 1px solid; BORDER-RIGHT: rgb(204,204,204) 1px solid; PADDING-TOP: 6px" class="blackCell">4</td>
<td style="BORDER-BOTTOM: rgb(204,204,204) 1px solid; TEXT-ALIGN: left; BORDER-LEFT: rgb(204,204,204) 1px solid; PADDING-BOTTOM: 6px; BACKGROUND-COLOR: rgb(0,0,0); MARGIN: 0px; PADDING-LEFT: 12px; PADDING-RIGHT: 12px; VERTICAL-ALIGN: top; BORDER-TOP: rgb(204,204,204) 1px solid; BORDER-RIGHT: rgb(204,204,204) 1px solid; PADDING-TOP: 6px" class="blackCell">5</td>
<td style="BORDER-BOTTOM: rgb(204,204,204) 1px solid; TEXT-ALIGN: left; BORDER-LEFT: rgb(204,204,204) 1px solid; PADDING-BOTTOM: 6px; BACKGROUND-COLOR: rgb(0,0,0); MARGIN: 0px; PADDING-LEFT: 12px; PADDING-RIGHT: 12px; VERTICAL-ALIGN: top; BORDER-TOP: rgb(204,204,204) 1px solid; BORDER-RIGHT: rgb(204,204,204) 1px solid; PADDING-TOP: 6px" class="blackCell">6</td>
<td style="BORDER-BOTTOM: rgb(204,204,204) 1px solid; TEXT-ALIGN: left; BORDER-LEFT: rgb(204,204,204) 1px solid; PADDING-BOTTOM: 6px; BACKGROUND-COLOR: rgb(0,0,0); MARGIN: 0px; PADDING-LEFT: 12px; PADDING-RIGHT: 12px; VERTICAL-ALIGN: top; BORDER-TOP: rgb(204,204,204) 1px solid; BORDER-RIGHT: rgb(204,204,204) 1px solid; PADDING-TOP: 6px" class="blackCell">7</td>
<td style="BORDER-BOTTOM: rgb(204,204,204) 1px solid; TEXT-ALIGN: left; BORDER-LEFT: rgb(204,204,204) 1px solid; PADDING-BOTTOM: 6px; BACKGROUND-COLOR: rgb(0,0,0); MARGIN: 0px; PADDING-LEFT: 12px; PADDING-RIGHT: 12px; VERTICAL-ALIGN: top; BORDER-TOP: rgb(204,204,204) 1px solid; BORDER-RIGHT: rgb(204,204,204) 1px solid; PADDING-TOP: 6px" class="blackCell">8</td>
<td style="BORDER-BOTTOM: rgb(204,204,204) 1px solid; TEXT-ALIGN: left; BORDER-LEFT: rgb(204,204,204) 1px solid; PADDING-BOTTOM: 6px; BACKGROUND-COLOR: rgb(0,0,0); MARGIN: 0px; PADDING-LEFT: 12px; PADDING-RIGHT: 12px; VERTICAL-ALIGN: top; BORDER-TOP: rgb(204,204,204) 1px solid; BORDER-RIGHT: rgb(204,204,204) 1px solid; PADDING-TOP: 6px" class="blackCell">9</td>
<td style="BORDER-BOTTOM: rgb(204,204,204) 1px solid; TEXT-ALIGN: left; BORDER-LEFT: rgb(204,204,204) 1px solid; PADDING-BOTTOM: 6px; BACKGROUND-COLOR: rgb(0,0,0); MARGIN: 0px; PADDING-LEFT: 12px; PADDING-RIGHT: 12px; VERTICAL-ALIGN: top; BORDER-TOP: rgb(204,204,204) 1px solid; BORDER-RIGHT: rgb(204,204,204) 1px solid; PADDING-TOP: 6px" class="blackCell">10</td>
<td style="BORDER-BOTTOM: rgb(204,204,204) 1px solid; TEXT-ALIGN: left; BORDER-LEFT: rgb(204,204,204) 1px solid; PADDING-BOTTOM: 6px; BACKGROUND-COLOR: rgb(234,153,153); MARGIN: 0px; PADDING-LEFT: 12px; PADDING-RIGHT: 12px; VERTICAL-ALIGN: top; BORDER-TOP: rgb(204,204,204) 1px solid; BORDER-RIGHT: rgb(204,204,204) 1px solid; PADDING-TOP: 6px" class="redCell">11</td>
<td style="BORDER-BOTTOM: rgb(204,204,204) 1px solid; TEXT-ALIGN: left; BORDER-LEFT: rgb(204,204,204) 1px solid; PADDING-BOTTOM: 6px; BACKGROUND-COLOR: rgb(234,153,153); MARGIN: 0px; PADDING-LEFT: 12px; PADDING-RIGHT: 12px; VERTICAL-ALIGN: top; BORDER-TOP: rgb(204,204,204) 1px solid; BORDER-RIGHT: rgb(204,204,204) 1px solid; PADDING-TOP: 6px" class="redCell">12</td>
<td style="BORDER-BOTTOM: rgb(204,204,204) 1px solid; TEXT-ALIGN: left; BORDER-LEFT: rgb(204,204,204) 1px solid; PADDING-BOTTOM: 6px; BACKGROUND-COLOR: rgb(234,153,153); MARGIN: 0px; PADDING-LEFT: 12px; PADDING-RIGHT: 12px; VERTICAL-ALIGN: top; BORDER-TOP: rgb(204,204,204) 1px solid; BORDER-RIGHT: rgb(204,204,204) 1px solid; PADDING-TOP: 6px" class="redCell">13</td>
<td style="BORDER-BOTTOM: rgb(204,204,204) 1px solid; TEXT-ALIGN: left; BORDER-LEFT: rgb(204,204,204) 1px solid; PADDING-BOTTOM: 6px; BACKGROUND-COLOR: rgb(234,153,153); MARGIN: 0px; PADDING-LEFT: 12px; PADDING-RIGHT: 12px; VERTICAL-ALIGN: top; BORDER-TOP: rgb(204,204,204) 1px solid; BORDER-RIGHT: rgb(204,204,204) 1px solid; PADDING-TOP: 6px" class="redCell">+</td>
</tr>
</tbody>
</table>
<p>minSdkVersion 越大则代表支持越高版本的代码，那么如上图 red ≥ green ≥ blue，则上图可以合并如下：</p>
<table style="PADDING-BOTTOM: 0px; BORDER-RIGHT-WIDTH: 0px; MARGIN: 0px 0px 1em 1em; PADDING-LEFT: 0px; PADDING-RIGHT: 0px; BORDER-COLLAPSE: collapse; EMPTY-CELLS: show; BORDER-TOP-WIDTH: 0px; BORDER-BOTTOM-WIDTH: 0px; FONT-SIZE: 1em; BORDER-LEFT-WIDTH: 0px; PADDING-TOP: 0px" border="1" cellspacing="0" cellpadding="10">
<tbody style="PADDING-BOTTOM: 0px; BORDER-RIGHT-WIDTH: 0px; MARGIN: 0px; PADDING-LEFT: 0px; PADDING-RIGHT: 0px; BORDER-TOP-WIDTH: 0px; BORDER-BOTTOM-WIDTH: 0px; BORDER-LEFT-WIDTH: 0px; PADDING-TOP: 0px">
<tr style="PADDING-BOTTOM: 0px; BORDER-RIGHT-WIDTH: 0px; MARGIN: 0px; PADDING-LEFT: 0px; PADDING-RIGHT: 0px; BORDER-TOP-WIDTH: 0px; BORDER-BOTTOM-WIDTH: 0px; BORDER-LEFT-WIDTH: 0px; PADDING-TOP: 0px">
<td style="BORDER-BOTTOM: rgb(204,204,204) 1px solid; TEXT-ALIGN: left; BORDER-LEFT: rgb(204,204,204) 1px solid; PADDING-BOTTOM: 6px; BACKGROUND-COLOR: rgb(159,197,232); MARGIN: 0px; PADDING-LEFT: 12px; PADDING-RIGHT: 12px; VERTICAL-ALIGN: top; BORDER-TOP: rgb(204,204,204) 1px solid; BORDER-RIGHT: rgb(204,204,204) 1px solid; PADDING-TOP: 6px" class="blueCell">3</td>
<td style="BORDER-BOTTOM: rgb(204,204,204) 1px solid; TEXT-ALIGN: left; BORDER-LEFT: rgb(204,204,204) 1px solid; PADDING-BOTTOM: 6px; BACKGROUND-COLOR: rgb(159,197,232); MARGIN: 0px; PADDING-LEFT: 12px; PADDING-RIGHT: 12px; VERTICAL-ALIGN: top; BORDER-TOP: rgb(204,204,204) 1px solid; BORDER-RIGHT: rgb(204,204,204) 1px solid; PADDING-TOP: 6px" class="blueCell">4</td>
<td style="BORDER-BOTTOM: rgb(204,204,204) 1px solid; TEXT-ALIGN: left; BORDER-LEFT: rgb(204,204,204) 1px solid; PADDING-BOTTOM: 6px; BACKGROUND-COLOR: rgb(159,197,232); MARGIN: 0px; PADDING-LEFT: 12px; PADDING-RIGHT: 12px; VERTICAL-ALIGN: top; BORDER-TOP: rgb(204,204,204) 1px solid; BORDER-RIGHT: rgb(204,204,204) 1px solid; PADDING-TOP: 6px" class="blueCell">5</td>
<td style="BORDER-BOTTOM: rgb(204,204,204) 1px solid; TEXT-ALIGN: left; BORDER-LEFT: rgb(204,204,204) 1px solid; PADDING-BOTTOM: 6px; BACKGROUND-COLOR: rgb(159,197,232); MARGIN: 0px; PADDING-LEFT: 12px; PADDING-RIGHT: 12px; VERTICAL-ALIGN: top; BORDER-TOP: rgb(204,204,204) 1px solid; BORDER-RIGHT: rgb(204,204,204) 1px solid; PADDING-TOP: 6px" class="blueCell">6</td>
<td style="BORDER-BOTTOM: rgb(204,204,204) 1px solid; TEXT-ALIGN: left; BORDER-LEFT: rgb(204,204,204) 1px solid; PADDING-BOTTOM: 6px; BACKGROUND-COLOR: rgb(182,215,168); MARGIN: 0px; PADDING-LEFT: 12px; PADDING-RIGHT: 12px; VERTICAL-ALIGN: top; BORDER-TOP: rgb(204,204,204) 1px solid; BORDER-RIGHT: rgb(204,204,204) 1px solid; PADDING-TOP: 6px" class="greenCell">7</td>
<td style="BORDER-BOTTOM: rgb(204,204,204) 1px solid; TEXT-ALIGN: left; BORDER-LEFT: rgb(204,204,204) 1px solid; PADDING-BOTTOM: 6px; BACKGROUND-COLOR: rgb(182,215,168); MARGIN: 0px; PADDING-LEFT: 12px; PADDING-RIGHT: 12px; VERTICAL-ALIGN: top; BORDER-TOP: rgb(204,204,204) 1px solid; BORDER-RIGHT: rgb(204,204,204) 1px solid; PADDING-TOP: 6px" class="greenCell">8</td>
<td style="BORDER-BOTTOM: rgb(204,204,204) 1px solid; TEXT-ALIGN: left; BORDER-LEFT: rgb(204,204,204) 1px solid; PADDING-BOTTOM: 6px; BACKGROUND-COLOR: rgb(182,215,168); MARGIN: 0px; PADDING-LEFT: 12px; PADDING-RIGHT: 12px; VERTICAL-ALIGN: top; BORDER-TOP: rgb(204,204,204) 1px solid; BORDER-RIGHT: rgb(204,204,204) 1px solid; PADDING-TOP: 6px" class="greenCell">9</td>
<td style="BORDER-BOTTOM: rgb(204,204,204) 1px solid; TEXT-ALIGN: left; BORDER-LEFT: rgb(204,204,204) 1px solid; PADDING-BOTTOM: 6px; BACKGROUND-COLOR: rgb(182,215,168); MARGIN: 0px; PADDING-LEFT: 12px; PADDING-RIGHT: 12px; VERTICAL-ALIGN: top; BORDER-TOP: rgb(204,204,204) 1px solid; BORDER-RIGHT: rgb(204,204,204) 1px solid; PADDING-TOP: 6px" class="greenCell">10</td>
<td style="BORDER-BOTTOM: rgb(204,204,204) 1px solid; TEXT-ALIGN: left; BORDER-LEFT: rgb(204,204,204) 1px solid; PADDING-BOTTOM: 6px; BACKGROUND-COLOR: rgb(234,153,153); MARGIN: 0px; PADDING-LEFT: 12px; PADDING-RIGHT: 12px; VERTICAL-ALIGN: top; BORDER-TOP: rgb(204,204,204) 1px solid; BORDER-RIGHT: rgb(204,204,204) 1px solid; PADDING-TOP: 6px" class="redCell">11</td>
<td style="BORDER-BOTTOM: rgb(204,204,204) 1px solid; TEXT-ALIGN: left; BORDER-LEFT: rgb(204,204,204) 1px solid; PADDING-BOTTOM: 6px; BACKGROUND-COLOR: rgb(234,153,153); MARGIN: 0px; PADDING-LEFT: 12px; PADDING-RIGHT: 12px; VERTICAL-ALIGN: top; BORDER-TOP: rgb(204,204,204) 1px solid; BORDER-RIGHT: rgb(204,204,204) 1px solid; PADDING-TOP: 6px" class="redCell">12</td>
<td style="BORDER-BOTTOM: rgb(204,204,204) 1px solid; TEXT-ALIGN: left; BORDER-LEFT: rgb(204,204,204) 1px solid; PADDING-BOTTOM: 6px; BACKGROUND-COLOR: rgb(234,153,153); MARGIN: 0px; PADDING-LEFT: 12px; PADDING-RIGHT: 12px; VERTICAL-ALIGN: top; BORDER-TOP: rgb(204,204,204) 1px solid; BORDER-RIGHT: rgb(204,204,204) 1px solid; PADDING-TOP: 6px" class="redCell">13</td>
<td style="BORDER-BOTTOM: rgb(204,204,204) 1px solid; TEXT-ALIGN: left; BORDER-LEFT: rgb(204,204,204) 1px solid; PADDING-BOTTOM: 6px; BACKGROUND-COLOR: rgb(234,153,153); MARGIN: 0px; PADDING-LEFT: 12px; PADDING-RIGHT: 12px; VERTICAL-ALIGN: top; BORDER-TOP: rgb(204,204,204) 1px solid; BORDER-RIGHT: rgb(204,204,204) 1px solid; PADDING-TOP: 6px" class="redCell">+</td>
</tr>
</tbody>
</table>
<p>现在，假如 Red APK 使用了一种其他APK没有的新功能，比如前置相机，API Level 11以后支持。当用户设备通过Android Market进行程序下载的时候，Market Filters就会检测设备的信息，看是否是Level 11以上，是否有前置相机，如果不符合条件的话，Red APK就不会列在下载清单中，用户也就没法下载。事实上，目前仍有很多的设备还没有达到Level 11，也没有配置前置相机，那么如何解决此类问题呢。</p>
<p>幸运的是，Android Market会自动处理这种情况，它发现当前设备与Red APK的manifest 文件声明不符，会简单忽略它，然后继续检测Green APK，因为 Green APK没有设置maxSdkVersion 选项，本身又向前兼容，这样不管设备的版本是不是Level 11以上，也不管设备是否有前置摄像头，Green APK 仍然可用，用户也可以下载。</p>
<p>为了便于管理所有的APKs，应该合理设置版本号（version code scheme）。推荐阅读指南《<a href="http://developer.android.com/guide/market/publishing/multiple-apks.html#VersionCodes">Version Codes</a>》，我们的例子中有3个独立的APK，每一个可以设置1000的版本号范围，前几位的数字（minSdkVersion ）用来标志不同的APK，每次更新程序依次递增。示例如下：</p>
<pre>   Blue: 03001, 03002, 03003, 03004...
   Green: 07001, 07002, 07003, 07004...
   Red:11001, 11002, 11003, 11004...</pre>
<p>综合以上，您的 Android Manifests 文件可能如下：</p>
<p>Blue:</p>
<pre>   &lt;manifest xmlns:android="&lt;a href="http://schemas.android.com/apk/res/android"&gt;http://schemas.android.com/apk/res/android&lt;/a&gt;"    android:versionCode="03001" android:versionName="1.0" package="com.example.foo"&gt;    &lt;uses-sdk android:minSdkVersion="3" /&gt;    ... </pre>
<p>Green:</p>
<pre>   &lt;manifest xmlns:android="&lt;a href="http://schemas.android.com/apk/res/android"&gt;http://schemas.android.com/apk/res/android&lt;/a&gt;"    android:versionCode="07001" android:versionName="1.0" package="com.example.foo"&gt;    &lt;uses-sdk android:minSdkVersion="7" /&gt;    ... </pre>
<p>Red:</p>
<pre>   &lt;manifest xmlns:android="&lt;a href="http://schemas.android.com/apk/res/android"&gt;http://schemas.android.com/apk/res/android&lt;/a&gt;"    android:versionCode="11001" android:versionName="1.0" package="com.example.foo"&gt;    &lt;uses-sdk android:minSdkVersion="11" /&gt;    ... </pre>
<h3>发布前的检测清单</h3>
<p>在将您的应用发布到Android Market之前，请仔细检查下面清单中列举的项目。请记住这些检查只适用于多APKs的程序设计，本文无法列出要发布到Android Market程序的所有检测项目。</p>
<ul>
<li>所有的APKs拥有相同的包名</li>
<li>所有的APKs必须使用相同的签名文件</li>
<li>如果多个APKs的平台兼容版本有重叠，那么拥有较高minSdkVersion版本的APK必须拥有较高的版本号（version code）</li>
<li>检查manifest配置文件中的filter，防止出现矛盾的地方（例如，APK声明为只支持屏幕类型为XLARGE的cupcake版本，那么此APK将不能被任何设备下载）</li>
<li>每一个APK的manifest配置文件必须是唯一的，至少支持一种类型的屏幕，OpenGL纹理，或者Android固件版本</li>
<li>每一个APK都至少在一个平台上做过测试，开发环境中可以建立多个不同类型自定义的模拟器，请尽量做完整的测试</li>
</ul>
<p>利用 Aapt (the Android Asset Packaging Tool) 工具来做最后的打包和检查工作，有效防止意外的小错误导致您的应用无法在Android Market的下载清单中正常显示出来。</p>
<pre>   &gt;aapt dump badging
   package: name='com.example.hello' versionCode='1' versionName='1.0'
   sdkVersion:'11'
   uses-permission:'android.permission.SEND_SMS'
   application-label:'Hello'
   application-icon-120:'res/drawable-ldpi/icon.png'
   application-icon-160:'res/drawable-mdpi/icon.png'
   application-icon-240:'res/drawable-hdpi/icon.png'
   application: label='Hello' icon='res/drawable-mdpi/icon.png'
   launchable-activity: name='com.example.hello.HelloActivity' label='Hello' icon=''
   uses-feature:'android.hardware.telephony'
   uses-feature:'android.hardware.touchscreen'
   main
   supports-screens: 'small' 'normal' 'large' 'xlarge'
   supports-any-density: 'true'
   locales: '--_--'
   densities: '120' '160' '240'</pre>
<p>仔细检查aapt工具的输出，看看有没有设置有矛盾的地方，尤其是关于支持的屏幕类型（supports-screens ）和兼容的屏幕类型（ compatible-screens），没有预期之外的多余的用户功能（uses-feature）权限，在上面的示例中，该APK在很多设备中都不可见。</p>
<p>什么原因导致的呢？因为上面的APK配置文件中加入了发信息（SEND_SMS）权限，android.hardware.telephony 特性就被隐式自动添加了。API 11 是Honeycomb （专门为平板设计的系统），而该设备没有支持电话的硬件（telephony hardware），Android Market就会将此APK过滤掉，导致 Honeycomb 设备没法下载和安装该APK，直到未来的设备APK Level升级，加入了支持电话的硬件，这个APK才能被下载安装。</p>
<p>幸运的是，我们可以只简单的修改一下配置文件即可解决这个问题：</p>
<pre>   &lt;uses-feature android:name="android.hardware.telephony" android:required="false" /&gt;</pre>
<p>完成了以上的检查工作，提交APKs到Android Market。经过审核后，您就可以在Android Market中下载和安装这些应用到您的测试设备中，确保所有的APKs都正确匹配配置文件中声明的设备类型，APK在预期的设备平台上会正常工作，到此为止，恭喜您，多APK设计的工作就完成了。</p>
<p><strong>参考文摘：</strong></p>
<p><a href="http://developer.android.com/training/multiple-apks/api.html">http://developer.android.com/training/multiple-apks/api.html</a></p>
<p><a rel="nofollow" href="http://blog.zhourunsheng.com/2012/02/android-%e5%a4%9aapk%e8%ae%be%e8%ae%a1%e4%b8%93%e9%a2%98%e4%b9%8b%e4%b8%ba%e4%b8%8d%e5%90%8c%e7%9a%84api-level%e5%88%9b%e5%bb%ba%e4%b8%8d%e5%90%8c%e7%9a%84%e5%ba%94%e7%94%a8apk/">Android 多APK设计专题之为不同的API Level创建不同的应用APK</a>，首发于<a rel="nofollow" href="http://blog.zhourunsheng.com">润物无声</a>。</p>
]]></content:encoded>
			<wfw:commentRss>http://blog.zhourunsheng.com/2012/02/android-%e5%a4%9aapk%e8%ae%be%e8%ae%a1%e4%b8%93%e9%a2%98%e4%b9%8b%e4%b8%ba%e4%b8%8d%e5%90%8c%e7%9a%84api-level%e5%88%9b%e5%bb%ba%e4%b8%8d%e5%90%8c%e7%9a%84%e5%ba%94%e7%94%a8apk/feed/</wfw:commentRss>
		<slash:comments>1</slash:comments>
		</item>
		<item>
		<title>Android 相片管理专题之相机操作</title>
		<link>http://blog.zhourunsheng.com/2012/01/android-%e7%9b%b8%e7%89%87%e7%ae%a1%e7%90%86%e4%b8%93%e9%a2%98%e4%b9%8b%e7%9b%b8%e6%9c%ba%e6%93%8d%e4%bd%9c/</link>
		<comments>http://blog.zhourunsheng.com/2012/01/android-%e7%9b%b8%e7%89%87%e7%ae%a1%e7%90%86%e4%b8%93%e9%a2%98%e4%b9%8b%e7%9b%b8%e6%9c%ba%e6%93%8d%e4%bd%9c/#comments</comments>
		<pubDate>Thu, 12 Jan 2012 11:57:14 +0000</pubDate>
		<dc:creator><![CDATA[润物无声]]></dc:creator>
				<category><![CDATA[移动开发]]></category>
		<category><![CDATA[Android]]></category>
		<category><![CDATA[Training]]></category>

		<guid isPermaLink="false">http://blog.zhourunsheng.com/?p=1284</guid>
		<description><![CDATA[<p>本节课主要讲述怎样通过调用系统的framework APIs来控制设备的摄像头操作。 相比调用系统内置的其他相 [&#8230;]</p>
<p><a rel="nofollow" href="http://blog.zhourunsheng.com/2012/01/android-%e7%9b%b8%e7%89%87%e7%ae%a1%e7%90%86%e4%b8%93%e9%a2%98%e4%b9%8b%e7%9b%b8%e6%9c%ba%e6%93%8d%e4%bd%9c/">Android 相片管理专题之相机操作</a>，首发于<a rel="nofollow" href="http://blog.zhourunsheng.com">润物无声</a>。</p>
]]></description>
				<content:encoded><![CDATA[<p><img class="alignleft" title="training-prof" alt src="http://blog.zhourunsheng.com/wp-content/uploads/2011/12/training-prof-150x150.png" width="150" height="150"/></p>
<p>本节课主要讲述怎样通过调用系统的framework APIs来控制设备的摄像头操作。</p>
<p>相比调用系统内置的其他相机应用来拍照和摄像，自己编写代码来直接控制相机操作需要更多的工作，然而，如果您要设计一款专业的拍照应用，或者相机功能与您的UI界面进行深度整合，那么您可以从本节课找到所需的知识。</p>
<p> <span id="more-1284"></span><br />
<h3>开启相机</h3>
<p>自己编写代码控制相机的第一步就是获得一个<a href="http://developer.android.com/reference/android/hardware/Camera.html">Camera</a> 实例对象，与Android系统内置的其他相机应用类似，推荐的方法是在<a href="http://developer.android.com/reference/android/app/Activity.html#onCreate(android.os.Bundle)">onCreate()</a>方法中启动一个新的线程来开启相机，因为这个过程可能比较耗时，新启一个线程来处理可以有效避免UI主线程的停顿，另一种方式是将开启相机的过程推迟到 <a href="http://developer.android.com/reference/android/app/Activity.html#onResume()">onResume()</a> 方法中，这样便于代码的重用和简化流程控制。</p>
<p>如果此时有其他应用正在使用相机，那么当您的程序调用方法<a href="http://developer.android.com/reference/android/hardware/Camera.html#open()">Camera.open()</a> 的时候会出现异常，需要进行捕获（ <span style="COLOR: #ff00ff">try block</span>）。</p>
<pre>   private boolean safeCameraOpen(int id) {
     boolean qOpened = false;
  
     try {
       releaseCameraAndPreview();
       mCamera = Camera.open(id);
       qOpened = (mCamera != null);
     } catch (Exception e) {
       Log.e(getString(R.string.app_name), "failed to open Camera");
       e.printStackTrace();
     }
     return qOpened;
   }

   private void releaseCameraAndPreview() {
     mPreview.setCamera(null);
     if (mCamera != null) {
       mCamera.release();
       mCamera = null;
     }
   }</pre>
<p>从 API level 9 开始，camera framework 支持用户程序控制多个相机，如果您在调用方法<a href="http://developer.android.com/reference/android/hardware/Camera.html#open()">open()</a> 的时候没有传递任何参数，那么系统会默认启动Androd设备的第一个后置摄像头。</p>
<h3>创建相机预览画面</h3>
<p>按下快门键拍照以前，用户希望看到当前相机的预览画面，要实现此功能，那么您可以使用SurfaceView，通过获取实时的相机捕获的数据并展示到界面上。</p>
<p><strong>自定义预览界面类型（Preview Class）</strong></p>
<p>要创建一个预览画面，需要自定义一个画面预览类型（preview class）。您需要实现 <span style="COLOR: #ff00ff">android.view.SurfaceHolder.Callback</span> 接口函数，相机捕获的数据流会通过这个接口传递到负责显示预览画面的程序中。</p>
<pre>   class Preview extends ViewGroup implements SurfaceHolder.Callback {
    SurfaceView mSurfaceView;
    SurfaceHolder mHolder;

    Preview(Context context) {
     super(context);

     mSurfaceView = new SurfaceView(context);
     addView(mSurfaceView);

     // Install a SurfaceHolder.Callback so we get notified when the
     // underlying surface is created and destroyed.
     mHolder = mSurfaceView.getHolder();
     mHolder.addCallback(this);
     mHolder.setType(SurfaceHolder.SURFACE_TYPE_PUSH_BUFFERS);
    }
   ...
   }</pre>
<p>在开启相机的预览操作前，您必须首先将自定义的预览界面实例对象处传递给<a href="http://developer.android.com/reference/android/hardware/Camera.html">Camera</a> 实例对象，稍后会有介绍。</p>
<p><strong>设置和开启预览（Set and Start the Preview）</strong></p>
<p>创建Camera实例和创建与其绑定的预览界面实例必须按照一个特定的顺序，且创建Camera实例必须是第一步。下面的实例代码中，将开启相机的预览操作封装在一个函数中，这样可以保证不论用户什么时候只要调用过 <span style="COLOR: #ff00ff">setCamera()</span>方法更新了相机参数配置，用来开启预览功能的<a href="http://developer.android.com/reference/android/hardware/Camera.html#startPreview()">Camera.startPreview()</a>方法就会被调用，同样在画面大小变更的回调函数 <span style="COLOR: #ff00ff">surfaceChanged()</span> 中，预览也必须得重新启动。</p>
<pre>   public void setCamera(Camera camera) {
   if (mCamera == camera) { return; }

   stopPreviewAndFreeCamera();

   mCamera = camera;

   if (mCamera != null) {
      List&lt;Size&gt; localSizes = mCamera.getParameters().getSupportedPreviewSizes();
      mSupportedPreviewSizes = localSizes;
      requestLayout();

      try {
         mCamera.setPreviewDisplay(mHolder);
      } catch (IOException e) {
         e.printStackTrace();
      }

     /*
      Important: Call startPreview() to start updating the preview surface. Preview must
      be started before you can take a picture.
     */
     mCamera.startPreview();
    }
   }</pre>
<h3>配置相机参数</h3>
<p>在控制相机拍照的过程中可以修改相机的配置参数，比如放大倍率，曝光补偿等等，示例代码展示了修改相机的预览画面大小，如果想了解更多的配置参数请参阅Camera APP的源代码。</p>
<pre>   public void surfaceChanged(SurfaceHolder holder, int format, int w, int h) {
      // Now that the size is known, set up the camera parameters and begin
      // the preview.
      Camera.Parameters parameters = mCamera.getParameters();
      parameters.setPreviewSize(mPreviewSize.width, mPreviewSize.height);
      requestLayout();
      mCamera.setParameters(parameters);

      /*
      Important: Call startPreview() to start updating the preview surface. Preview must be
      started before you can take a picture.
      */
     mCamera.startPreview();
   }</pre>
<h3>设置预览方向</h3>
<p>多数相机应用在启动预览的时候，默认选定在横屏模式，因为这是相机传感器最自然的方向，但是，这些设定并不影响您拍摄竖屏模式的照片，当前设备的方向会记录在照片的EXIF header中。方法<a href="http://developer.android.com/reference/android/hardware/Camera.html#setDisplayOrientation(int)">setCameraDisplayOrientation()</a> 可以用来改变预览画面的方向而不影响最终照片的拍摄方向。需要注意的是在API level 14之前，您需要首先停止预览画面，切换横竖屏模式，再重新启动预览画面。</p>
<h3>拍照</h3>
<p>在预览的过程中，调用方法 <a href="http://developer.android.com/reference/android/hardware/Camera.html#takePicture(android.hardware.Camera.ShutterCallback, android.hardware.Camera.PictureCallback, android.hardware.Camera.PictureCallback)">Camera.takePicture()</a> 来拍摄照片，可以创建<a href="http://developer.android.com/reference/android/hardware/Camera.PictureCallback.html">Camera.PictureCallback</a> 和 <a href="http://developer.android.com/reference/android/hardware/Camera.ShutterCallback.html">Camera.ShutterCallback</a> 实例对象作为参数传递给方法 <a href="http://developer.android.com/reference/android/hardware/Camera.html#takePicture(android.hardware.Camera.ShutterCallback, android.hardware.Camera.PictureCallback, android.hardware.Camera.PictureCallback)">Camera.takePicture()</a> 中。想要实现连续拍照，需要实现接口<a href="http://developer.android.com/reference/android/hardware/Camera.PreviewCallback.html">Camera.PreviewCallback</a> 中的<a href="http://developer.android.com/reference/android/hardware/Camera.PreviewCallback.html#onPreviewFrame(byte[], android.hardware.Camera)">onPreviewFrame()</a>函数，在实现代码中，您可以捕获当前选定的预览帧，或者设定一个延迟时间来调用拍照函数 <a href="http://developer.android.com/reference/android/hardware/Camera.html#takePicture(android.hardware.Camera.ShutterCallback, android.hardware.Camera.PictureCallback, android.hardware.Camera.PictureCallback)">takePicture()</a>。</p>
<h3>重新启动拍照预览</h3>
<p>拍摄了一张照片以后，您必须重新启动画面预览才可以继续拍摄下一张照片，示例代码展示了用户按下快门键拍照后重新启动预览。</p>
<pre>   @Override
   public void onClick(View v) {
     switch(mPreviewState) {
      case K_STATE_FROZEN:
        mCamera.startPreview();
        mPreviewState = K_STATE_PREVIEW;
        break;

      default:
        mCamera.takePicture( null, rawCallback, null);
        mPreviewState = K_STATE_BUSY;
    } // switch
    shutterBtnConfig();
   }</pre>
<h3>关闭预览释放相机资源</h3>
<p>一旦您的应用使用完了相机，那么应该及时释放资源。特别地，需要释放Camera 实例对象，否则可能会破坏其他应用程序的执行，包括您自己应用的重新启动。</p>
<p>那么哪个地方是最合适停止预览与释放Camera资源的呢？预览画面销毁的回调函数中是个不错的选择。需要参照前面实现的自定义预览类型（ Preview class）。</p>
<pre>   public void surfaceDestroyed(SurfaceHolder holder) {
     // Surface will be destroyed when we return, so stop the preview.
     if (mCamera != null) {
        /*
         Call stopPreview() to stop updating the preview surface.
        */
        mCamera.stopPreview();
     }
   }

   /**
   * When this function returns, mCamera will be null.
   */
   private void stopPreviewAndFreeCamera() {

    if (mCamera != null) {
      /*
       Call stopPreview() to stop updating the preview surface.
      */
      mCamera.stopPreview();

      /*
      Important: Call release() to release the camera for use by other applications.
      Applications should release the camera immediately in onPause() (and re-open() it in
      onResume()).
     */
     mCamera.release();
     mCamera = null;
    }
   }</pre>
<p>本节课的开始部分，<span style="COLOR: #ff00ff">setCamera()</span> 方法中，在初始化camera 实例之前总是首先停止预览。</p>
<p><strong>参考文摘：</strong></p>
<p><a href="http://developer.android.com/training/camera/cameradirect.html">http://developer.android.com/training/camera/cameradirect.html</a></p>
<p><a rel="nofollow" href="http://blog.zhourunsheng.com/2012/01/android-%e7%9b%b8%e7%89%87%e7%ae%a1%e7%90%86%e4%b8%93%e9%a2%98%e4%b9%8b%e7%9b%b8%e6%9c%ba%e6%93%8d%e4%bd%9c/">Android 相片管理专题之相机操作</a>，首发于<a rel="nofollow" href="http://blog.zhourunsheng.com">润物无声</a>。</p>
]]></content:encoded>
			<wfw:commentRss>http://blog.zhourunsheng.com/2012/01/android-%e7%9b%b8%e7%89%87%e7%ae%a1%e7%90%86%e4%b8%93%e9%a2%98%e4%b9%8b%e7%9b%b8%e6%9c%ba%e6%93%8d%e4%bd%9c/feed/</wfw:commentRss>
		<slash:comments>4</slash:comments>
		</item>
		<item>
		<title>Android 相片管理专题之视频捕捉</title>
		<link>http://blog.zhourunsheng.com/2012/01/android-%e7%9b%b8%e7%89%87%e7%ae%a1%e7%90%86%e4%b8%93%e9%a2%98%e4%b9%8b%e8%a7%86%e9%a2%91%e6%8d%95%e6%8d%89/</link>
		<comments>http://blog.zhourunsheng.com/2012/01/android-%e7%9b%b8%e7%89%87%e7%ae%a1%e7%90%86%e4%b8%93%e9%a2%98%e4%b9%8b%e8%a7%86%e9%a2%91%e6%8d%95%e6%8d%89/#comments</comments>
		<pubDate>Wed, 11 Jan 2012 15:11:37 +0000</pubDate>
		<dc:creator><![CDATA[润物无声]]></dc:creator>
				<category><![CDATA[移动开发]]></category>
		<category><![CDATA[Android]]></category>
		<category><![CDATA[Training]]></category>

		<guid isPermaLink="false">http://blog.zhourunsheng.com/?p=1281</guid>
		<description><![CDATA[<p>这节课主要讲述怎样利用设备已安装的相机应用程序来进行视频捕捉。 假如您的应用中有一个功能是视频整合，但是您又不 [&#8230;]</p>
<p><a rel="nofollow" href="http://blog.zhourunsheng.com/2012/01/android-%e7%9b%b8%e7%89%87%e7%ae%a1%e7%90%86%e4%b8%93%e9%a2%98%e4%b9%8b%e8%a7%86%e9%a2%91%e6%8d%95%e6%8d%89/">Android 相片管理专题之视频捕捉</a>，首发于<a rel="nofollow" href="http://blog.zhourunsheng.com">润物无声</a>。</p>
]]></description>
				<content:encoded><![CDATA[<p><img class="alignleft" title="training-prof" alt src="http://blog.zhourunsheng.com/wp-content/uploads/2011/12/training-prof-150x150.png" width="150" height="150"/></p>
<p>这节课主要讲述怎样利用设备已安装的相机应用程序来进行视频捕捉。</p>
<p>假如您的应用中有一个功能是视频整合，但是您又不想编写太复杂的代码来自己控制相机的视频捕捉，幸好，大多数Android设备已至少安装有一个相机程序，本课程就教您怎么样利用设备自带的相机应用来拍摄视频。</p>
<p> <span id="more-1281"></span><br />
<h3>申请相机权限</h3>
<p>如果您的应用的基本功能就是拍照或拍摄视频，需要在Android Market 中进行可见性限制，只有拥有摄像头的设备才能浏览和下载您的应用程序，那么您需要在manifest 文件的<a href="http://developer.android.com/guide/topics/manifest/uses-feature-element.html">&lt; uses-feature &gt;</a> tag中加入权限限制。</p>
<pre>   &lt;manifest ... &gt;
     &lt;uses-feature android:name="android.hardware.camera" /&gt;
     ...
   &lt;/manifest ... &gt;</pre>
<p>如果您的应用在设备没有相机的情况下仍能正常工作，那么可以在该tag中加入<span style="COLOR: #00ff00">android:required="false"</span>。设定以后，所有的设备不管有没有摄像头都可以在Android Market 上面浏览和下载您的应用。但是，您需要自己在运行时检测设备是否有摄像头，调用方法<a href="http://developer.android.com/reference/android/content/pm/PackageManager.html#hasSystemFeature(java.lang.String)">hasSystemFeature(PackageManager.FEATURE_CAMERA)</a>即可，如果相机不可用，那么您就需要禁用应用中与拍照或拍摄视频有关的功能模块。</p>
<h3>用相机录制视频</h3>
<p>在Andriod系统中，您的应用要委托其他的应用帮忙完成一些工作，可以使用<a href="http://developer.android.com/reference/android/content/Intent.html">Intent</a>，把期望完成的任务和参数列表设置到<a href="http://developer.android.com/reference/android/content/Intent.html">Intent</a>中，然后通过系统广播出去，最后当其他应用完成工作的时候，您的应用可以收到一个反馈信息，在回调函数中就可以取得任务处理结果。调用相机程序录制视频的过程与上面描述的类似，有三个关注点，定义拍摄视频的 <a href="http://developer.android.com/reference/android/content/Intent.html">Intent</a>，启动其他相机应用<a href="http://developer.android.com/reference/android/app/Activity.html">Activity</a>，处理拍摄后的视频流数据。</p>
<p>示例代码，启动视频录制应用</p>
<pre>   private void dispatchTakeVideoIntent() {
    Intent takeVideoIntent = new Intent(MediaStore.ACTION_VIDEO_CAPTURE);
    startActivityForResult(takeVideoIntent, ACTION_TAKE_VIDEO);
  }</pre>
<p>一个好的习惯是，在调用上面的代码前首先检查设备中是否有内置相机应用程序可以处理您的请求，示例代码如下：</p>
<pre>   public static boolean isIntentAvailable(Context context, String action) {
    final PackageManager packageManager = context.getPackageManager();
    final Intent intent = new Intent(action);
    List&lt;ResolveInfo&gt; list =
        packageManager.queryIntentActivities(intent,
            PackageManager.MATCH_DEFAULT_ONLY);
    return list.size() &gt; 0;
  }</pre>
<h3>查看视频</h3>
<p>捕获的视频流数据会通过回调函数<a href="http://developer.android.com/reference/android/app/Activity.html#onActivityResult(int, int, android.content.Intent)">onactivityresult()</a>返回到您的应用中，您可以从 <a href="http://developer.android.com/reference/android/content/Intent.html">Intent</a>中获得该视频数据的<a href="http://developer.android.com/reference/android/net/Uri.html">Uri</a>地址，示例代码展示了如何将捕获到的视频数据显示在<a href="http://developer.android.com/reference/android/widget/VideoView.html">VideoView</a>界面中。</p>
<pre>   private void handleCameraVideo(Intent intent) {
    mVideoUri = intent.getData();
    mVideoView.setVideoURI(mVideoUri);
  }</pre>
<p><strong>参考文摘：</strong></p>
<p><a href="http://developer.android.com/training/camera/videobasics.html">http://developer.android.com/training/camera/videobasics.html</a></p>
<p><a rel="nofollow" href="http://blog.zhourunsheng.com/2012/01/android-%e7%9b%b8%e7%89%87%e7%ae%a1%e7%90%86%e4%b8%93%e9%a2%98%e4%b9%8b%e8%a7%86%e9%a2%91%e6%8d%95%e6%8d%89/">Android 相片管理专题之视频捕捉</a>，首发于<a rel="nofollow" href="http://blog.zhourunsheng.com">润物无声</a>。</p>
]]></content:encoded>
			<wfw:commentRss>http://blog.zhourunsheng.com/2012/01/android-%e7%9b%b8%e7%89%87%e7%ae%a1%e7%90%86%e4%b8%93%e9%a2%98%e4%b9%8b%e8%a7%86%e9%a2%91%e6%8d%95%e6%8d%89/feed/</wfw:commentRss>
		<slash:comments>0</slash:comments>
		</item>
		<item>
		<title>Android 相片管理专题之拍照功能</title>
		<link>http://blog.zhourunsheng.com/2012/01/android-%e7%9b%b8%e7%89%87%e7%ae%a1%e7%90%86%e4%b8%93%e9%a2%98%e4%b9%8b%e6%8b%8d%e7%85%a7%e5%8a%9f%e8%83%bd/</link>
		<comments>http://blog.zhourunsheng.com/2012/01/android-%e7%9b%b8%e7%89%87%e7%ae%a1%e7%90%86%e4%b8%93%e9%a2%98%e4%b9%8b%e6%8b%8d%e7%85%a7%e5%8a%9f%e8%83%bd/#comments</comments>
		<pubDate>Tue, 10 Jan 2012 15:06:17 +0000</pubDate>
		<dc:creator><![CDATA[润物无声]]></dc:creator>
				<category><![CDATA[移动开发]]></category>
		<category><![CDATA[Android]]></category>
		<category><![CDATA[Training]]></category>

		<guid isPermaLink="false">http://blog.zhourunsheng.com/?p=1278</guid>
		<description><![CDATA[<p>这节课主要讲述怎样利用设备已安装的其他照相应用程序来拍摄照片。 例如您想要实现一个气象服务的APP，用来把全球 [&#8230;]</p>
<p><a rel="nofollow" href="http://blog.zhourunsheng.com/2012/01/android-%e7%9b%b8%e7%89%87%e7%ae%a1%e7%90%86%e4%b8%93%e9%a2%98%e4%b9%8b%e6%8b%8d%e7%85%a7%e5%8a%9f%e8%83%bd/">Android 相片管理专题之拍照功能</a>，首发于<a rel="nofollow" href="http://blog.zhourunsheng.com">润物无声</a>。</p>
]]></description>
				<content:encoded><![CDATA[<p><img class="alignleft" title="training-prof" alt src="http://blog.zhourunsheng.com/wp-content/uploads/2011/12/training-prof-150x150.png" width="150" height="150"/></p>
<p>这节课主要讲述怎样利用设备已安装的其他照相应用程序来拍摄照片。</p>
<p>例如您想要实现一个气象服务的APP，用来把全球的天气图片收集在一起，整合这些图片只是您应用程序的一部分工作，但是您又不想编写太复杂的代码来自己控制相机的拍照，幸好，大多数Android设备已至少安装有一个拍照程序，本课程就教您怎么样利用设备自带的拍照应用来拍摄这些图片。</p>
<p><span id="more-1278"></span><br />
<h3>申请相机权限</h3>
<p>如果您的应用的基本功能就是拍照，需要在Android Market 中进行可见性限制，只有拥有摄像头的设备才能浏览和下载您的应用程序，那么您需要在manifest 文件的<a href="http://developer.android.com/guide/topics/manifest/uses-feature-element.html">&lt; uses-feature &gt;</a> tag中加入权限限制。</p>
<pre>   &lt;manifest ... &gt;
     &lt;uses-feature android:name="android.hardware.camera" /&gt;
     ...
   &lt;/manifest ... &gt;</pre>
<p>如果您的应用在设备没有相机的情况下仍能正常工作，那么可以在该tag中加入<span style="COLOR: #00ff00">android:required="false"</span>。设定以后，所有的设备不管有没有摄像头都可以在Android Market 上面浏览和下载您的应用。但是，您需要自己在运行时检测设备是否有摄像头，调用方法<a href="http://developer.android.com/reference/android/content/pm/PackageManager.html#hasSystemFeature(java.lang.String)">hasSystemFeature(PackageManager.FEATURE_CAMERA)</a>即可，如果相机不可用，那么您就需要禁用应用中与拍照有关的功能模块。</p>
<h3>用相机程序拍照</h3>
<p>在Andriod系统中，如果您的应用要委托其他的应用帮忙完成一些工作，可以使用<a href="http://developer.android.com/reference/android/content/Intent.html">Intent</a>，把期望完成的任务和参数列表设置到<a href="http://developer.android.com/reference/android/content/Intent.html">Intent</a>中，然后通过系统广播出去，最后当其他应用完成工作的时候，您的应用可以收到一个反馈信息，在回调函数中就可以取得任务处理结果。调用相机程序进行拍照，过程与上面描述的类似，有三个关注点，定义拍照的 <a href="http://developer.android.com/reference/android/content/Intent.html">Intent</a>，启动其他相机应用<a href="http://developer.android.com/reference/android/app/Activity.html">Activity</a>，处理拍照后的照片数据。</p>
<p>示例代码，启动拍照应用</p>
<pre>   private void dispatchTakePictureIntent(int actionCode) {
     Intent takePictureIntent = new Intent(MediaStore.ACTION_IMAGE_CAPTURE);
     startActivityForResult(takePictureIntent, actionCode);
   }</pre>
<p>恭喜您，通过如上的代码就可以给您的应用加入使用设备内置应用拍照的功能，当然，如果恰巧设备中没有安装任何的拍照应用，那么您的应用就有可能出错，一个好的解决办法是，在调用上面的代码前首先检查设备中是否有内置拍照应用，示例代码如下：</p>
<pre>   public static boolean isIntentAvailable(Context context, String action) {
     final PackageManager packageManager = context.getPackageManager();
     final Intent intent = new Intent(action);
     List&lt;ResolveInfo&gt; list =
          packageManager.queryIntentActivities(intent, PackageManager.MATCH_DEFAULT_ONLY);
     return list.size() &gt; 0;
   }</pre>
<h3>查看照片</h3>
<p>如果您的应用不仅仅是通过设备内置的拍照应用拍摄照片，还需要对照片进行进一步的变换处理，那么您需要获得原始照片数据，拍照应用会将编码后的相片数据（small <a href="http://developer.android.com/reference/android/graphics/Bitmap.html">Bitmap</a> ）放到<a href="http://developer.android.com/reference/android/content/Intent.html">Intent</a>的<span style="COLOR: #00ff80">"data"</span>键值中，然后通过回调<a href="http://developer.android.com/reference/android/app/Activity.html#onActivityResult(int, int, android.content.Intent)">onactivityresult()</a>返回，如下示例代码将返回后的照片数据显示到 <a href="http://developer.android.com/reference/android/widget/ImageView.html">ImageView</a>中。</p>
<pre>   private void handleSmallCameraPhoto(Intent intent) {
      Bundle extras = intent.getExtras();
      mImageBitmap = (Bitmap) extras.get("data");
      mImageView.setImageBitmap(mImageBitmap);
   }</pre>
<p><font color="#FF00FF">注意</font>：从<a href="http://developer.android.com/reference/android/content/Intent.html">Intent</a>的<span style="COLOR: #00ff80">"data"</span>键值中取得的缩略图数据适合作为图标icon，如果要处理大的图片数据，您还要做更多的工作。</p>
<h3>保存照片</h3>
<p>如果您在指定拍照Intent的时候设置了照片的存储路径，那么拍照应用就会把拍摄的照片以文件的形式保存到该路径中，路径要求是绝对路径（如（存储器名SD卡）/(路径名)/（文件名））。</p>
<p>这里有一个获取照片路径的简单办法，仅限于<span style="COLOR: #ff0080">Android 2.2 (API level 8)</span> 及以上。</p>
<pre>   storageDir = new File(
     Environment.getExternalStoragePublicDirectory(
       Environment.DIRECTORY_PICTURES
     ),
     getAlbumName()
   );</pre>
<p>在早期的Android版本中，您必须手动指定照片的目录</p>
<pre>   storageDir = new File (
     Environment.getExternalStorageDirectory()
      + PICTURES_DIR
      + getAlbumName()
   );</pre>
<p><font color="#FF00FF">注意</font>：<span style="COLOR: #00ff40">PICTURES_DIR</span> 实际上就是 <span style="COLOR: #00ff40">Pictures/</span>，external/shared storage（外部/共享存储设备） 上共享照片的标准目录。</p>
<h4>图片命名</h4>
<p>如上文所述，拍摄的照片会存放在一个标准的图片目录中，您要做的是解决图片的命名冲突，或许您还会设置一个成员变量来保存，以便后续引用该图片，示例如下：</p>
<pre>   private File createImageFile() throws IOException {
     // Create an image file name
     String timeStamp =
        new SimpleDateFormat("yyyyMMdd_HHmmss").format(new Date());
     String imageFileName = JPEG_FILE_PREFIX + timeStamp + "_";
     File image = File.createTempFile(
       imageFileName,
       JPEG_FILE_SUFFIX,
       getAlbumDir()
     );
     mCurrentPhotoPath = image.getAbsolutePath();
     return image;
   }</pre>
<h4>为Intent添加图片存储路径参数</h4>
<p>一旦您选定了图片的存储路径，那么在启动拍照应用的时候就可以通过设置<a href="http://developer.android.com/reference/android/content/Intent.html">Intent</a>来传递给它。</p>
<pre>   File f = createImageFile();
   takePictureIntent.putExtra(MediaStore.EXTRA_OUTPUT, Uri.fromFile(f));</pre>
<h3>添加照片到Gallery中</h3>
<p>通过拍照应用，您可以知道拍摄的照片具体存放在系统的什么位置，因为这是您在Intent中指定的路径，若想让其他的应用也可以知道该照片的存在，那么最好的办法便是利于系统的媒体管理（system's Media Provider）。</p>
<p>示例代码展示了如何调用系统的媒体管理模块（system's media scanner），将您的照片添加到媒体数据库（Media Provider's database）中，这样系统的Gallery APP和其他的APP就能访问该照片了。</p>
<pre>   private void galleryAddPic() {
      Intent mediaScanIntent = new Intent(Intent.ACTION_MEDIA_SCANNER_SCAN_FILE);
      File f = new File(mCurrentPhotoPath);
      Uri contentUri = Uri.fromFile(f);
      mediaScanIntent.setData(contentUri);
      this.sendBroadcast(mediaScanIntent);
   }</pre>
<h3>图片解码</h3>
<p>在有限的内存设备上面处理全尺寸的大图片是一个需要关注的问题。如果您发现只显示了几张图片后就内存溢出了，您可以动态地调整堆容量，把图片缩放到界面需要展示的ImageView大小，然后映射到一个内存的JPEG数组中。 代码示例展示了如何使用这一技术。</p>
<pre>   private void setPic() {
      // Get the dimensions of the View
      int targetW = mImageView.getWidth();
      int targetH = mImageView.getHeight();

     // Get the dimensions of the bitmap
     BitmapFactory.Options bmOptions = new BitmapFactory.Options();
     bmOptions.inJustDecodeBounds = true;
     BitmapFactory.decodeFile(mCurrentPhotoPath, bmOptions);
     int photoW = bmOptions.outWidth;
     int photoH = bmOptions.outHeight;

     // Determine how much to scale down the image
     int scaleFactor = Math.min(photoW/targetW, photoH/targetH);

     // Decode the image file into a Bitmap sized to fill the View
     bmOptions.inJustDecodeBounds = false;
     bmOptions.inSampleSize = scaleFactor;
     bmOptions.inPurgeable = true;

     Bitmap bitmap = BitmapFactory.decodeFile(mCurrentPhotoPath, bmOptions);
     mImageView.setImageBitmap(bitmap);
   }</pre>
<p><strong>参考文摘：</strong></p>
<p><a href="http://developer.android.com/training/camera/photobasics.html">http://developer.android.com/training/camera/photobasics.html</a></p>
<p><a rel="nofollow" href="http://blog.zhourunsheng.com/2012/01/android-%e7%9b%b8%e7%89%87%e7%ae%a1%e7%90%86%e4%b8%93%e9%a2%98%e4%b9%8b%e6%8b%8d%e7%85%a7%e5%8a%9f%e8%83%bd/">Android 相片管理专题之拍照功能</a>，首发于<a rel="nofollow" href="http://blog.zhourunsheng.com">润物无声</a>。</p>
]]></content:encoded>
			<wfw:commentRss>http://blog.zhourunsheng.com/2012/01/android-%e7%9b%b8%e7%89%87%e7%ae%a1%e7%90%86%e4%b8%93%e9%a2%98%e4%b9%8b%e6%8b%8d%e7%85%a7%e5%8a%9f%e8%83%bd/feed/</wfw:commentRss>
		<slash:comments>0</slash:comments>
		</item>
		<item>
		<title>Android 信息共享专题之ActionBar添加分享动作</title>
		<link>http://blog.zhourunsheng.com/2012/01/android-%e4%bf%a1%e6%81%af%e5%85%b1%e4%ba%ab%e4%b8%93%e9%a2%98%e4%b9%8bactionbar%e6%b7%bb%e5%8a%a0%e5%88%86%e4%ba%ab%e5%8a%a8%e4%bd%9c/</link>
		<comments>http://blog.zhourunsheng.com/2012/01/android-%e4%bf%a1%e6%81%af%e5%85%b1%e4%ba%ab%e4%b8%93%e9%a2%98%e4%b9%8bactionbar%e6%b7%bb%e5%8a%a0%e5%88%86%e4%ba%ab%e5%8a%a8%e4%bd%9c/#comments</comments>
		<pubDate>Sun, 08 Jan 2012 08:12:31 +0000</pubDate>
		<dc:creator><![CDATA[润物无声]]></dc:creator>
				<category><![CDATA[移动开发]]></category>
		<category><![CDATA[Android]]></category>
		<category><![CDATA[Training]]></category>

		<guid isPermaLink="false">http://blog.zhourunsheng.com/?p=1264</guid>
		<description><![CDATA[<p>在Android 4.0 (API Level 14)中引入的ActionProvider方便了在Action [&#8230;]</p>
<p><a rel="nofollow" href="http://blog.zhourunsheng.com/2012/01/android-%e4%bf%a1%e6%81%af%e5%85%b1%e4%ba%ab%e4%b8%93%e9%a2%98%e4%b9%8bactionbar%e6%b7%bb%e5%8a%a0%e5%88%86%e4%ba%ab%e5%8a%a8%e4%bd%9c/">Android 信息共享专题之ActionBar添加分享动作</a>，首发于<a rel="nofollow" href="http://blog.zhourunsheng.com">润物无声</a>。</p>
]]></description>
				<content:encoded><![CDATA[<p><img class="alignleft" title="training-prof" src="http://blog.zhourunsheng.com/wp-content/uploads/2011/12/training-prof-150x150.png" alt="" width="150" height="150" /> 在Android 4.0 (API Level 14)中引入的<a href="http://developer.android.com/reference/android/view/ActionProvider.html">ActionProvider</a>方便了在<a href="http://developer.android.com/reference/android/app/ActionBar.html">ActionBar</a>中添加share action，一个<a href="http://developer.android.com/reference/android/view/ActionProvider.html">ActionProvider</a>一旦和<a href="http://developer.android.com/reference/android/app/ActionBar.html">ActionBar</a>中的某个菜单项绑定起来，它就会负责该菜单项的界面绘画更新和事件处理。借助于<a href="http://developer.android.com/reference/android/widget/ShareActionProvider.html">ShareActionProvider</a>，您只需要设置一个share intent就可以了，其余的事情都交给<a href="http://developer.android.com/reference/android/widget/ShareActionProvider.html">ShareActionProvider</a>来处理。 <span style="color: #ff0000;">注意</span>：ShareActionProvider 要求最低 API Level 14 <span id="more-1264"></span></p>
<h3>更新菜单声明</h3>
<p>要想使用<a href="http://developer.android.com/reference/android/widget/ShareActionProvider.html">ShareActionProviders</a>，您需要在菜单资源文件( <a href="http://developer.android.com/guide/topics/resources/menu-resource.html">menu resource</a>)的Share菜单项&lt;item&gt;中声明<span style="color: #ff00ff;">android:actionProviderClass</span>属性，示例如下</p>
<pre>  
&lt;menu xmlns:android="&lt;a href="http://schemas.android.com/apk/res/android"&gt;http://schemas.android.com/apk/res/android&lt;/a&gt;"&gt;       
&lt;item android:id="@+id/menu_item_share"           
android:showAsAction="ifRoom"           
android:title="Share"           
android:actionProviderClass="android.widget.ShareActionProvider" /&gt;       
...   
&lt;/menu&gt;</pre>
<p>通过如上的定义，就把该菜单项的界面显示和消息事件处理委托给<a href="http://developer.android.com/reference/android/widget/ShareActionProvider.html">ShareActionProviders</a>，不过具体要显示的内容还是需要您来指定。</p>
<h3>设置 Share Intent</h3>
<p>为了使 <a href="http://developer.android.com/reference/android/widget/ShareActionProvider.html">ShareActionProvider</a> 可以正常工作，您还必须要设置Share Intent。这个Share Intent和第一节课《<a href="http://blog.zhourunsheng.com/2012/01/android-%e4%bf%a1%e6%81%af%e5%85%b1%e4%ba%ab%e4%b8%93%e9%a2%98%e4%b9%8b%e5%86%85%e5%ae%b9%e5%8f%91%e5%b8%83/">Android 信息共享专题之内容发布</a>》中讲解的Intent一样，需要设置其Action为 <a href="http://developer.android.com/reference/android/content/Intent.html#ACTION_SEND">ACTION_SEND</a>，要发送的内容通过键值 <a href="http://developer.android.com/reference/android/content/Intent.html#EXTRA_TEXT">EXTRA_TEXT</a> 或者<a href="http://developer.android.com/reference/android/content/Intent.html#EXTRA_STREAM">EXTRA_STREAM</a>设置到该Intent中。具体步骤如下，您首先需要在<a href="http://developer.android.com/reference/android/app/Activity.html">Activity</a> 或者 <a href="http://developer.android.com/reference/android/app/Fragment.html">Fragment</a>中inflating（加载）菜单资源文件，找到上文中声明的那个菜单项<a href="http://developer.android.com/reference/android/view/MenuItem.html">MenuItem</a>（如<span style="color: #ff00ff;">menu_item_share</span>），接着呼叫方法<a href="http://developer.android.com/reference/android/view/MenuItem.html#getActionProvider()">MenuItem.getActionProvider()</a>来获得一个<a href="http://developer.android.com/reference/android/widget/ShareActionProvider.html">ShareActionProvider</a>实例，最后调用<a href="http://developer.android.com/reference/android/widget/ShareActionProvider.html#setShareIntent(android.content.Intent)">setShareIntent()</a>把Share Intent和该菜单项关联起来。 示例代码：</p>
<pre>  private ShareActionProvider mShareActionProvider;

  ...
  @Override
  public boolean onCreateOptionsMenu(Menu menu) {
      // Inflate menu resource file.
      getMenuInflater().inflate(R.menu.share_menu, menu);

      // Locate MenuItem with ShareActionProvider
      MenuItem item = menu.findItem(R.id.menu_item_share);

      // Fetch and store ShareActionProvider
      mShareActionProvider = (ShareActionProvider) item.getActionProvider();

      // Return true to display menu
      return true;
  }

  // Call to update the share intent
  private void setShareIntent(Intent shareIntent) {
      if (mShareActionProvider != null) {
          mShareActionProvider.setShareIntent(shareIntent);
      }
  }</pre>
<p>您可能会选择只有在第一次创建菜单的时候设置share intent一次，其余菜单界面刷新的时候都不会再改变intent的设置了，或者也会选择每次菜单刷新的时候就重新设置intent一次，随着UI界面的改变会更新不同的intent。例如，您在Gallery app中全屏幕浏览图片的时候和滚动浏览图片的时候可能会设置不同的sharing intent。 想了解更多的关于<a href="http://developer.android.com/reference/android/widget/ShareActionProvider.html">ShareActionProvider</a> 方面的知识，请参照<a href="http://developer.android.com/guide/topics/ui/actionbar.html#ActionProvider">Action Bar</a>指南。 <strong>参考文摘：</strong> <a href="http://developer.android.com/training/sharing/shareaction.html">http://developer.android.com/training/sharing/shareaction.html</a></p>
<p><a rel="nofollow" href="http://blog.zhourunsheng.com/2012/01/android-%e4%bf%a1%e6%81%af%e5%85%b1%e4%ba%ab%e4%b8%93%e9%a2%98%e4%b9%8bactionbar%e6%b7%bb%e5%8a%a0%e5%88%86%e4%ba%ab%e5%8a%a8%e4%bd%9c/">Android 信息共享专题之ActionBar添加分享动作</a>，首发于<a rel="nofollow" href="http://blog.zhourunsheng.com">润物无声</a>。</p>
]]></content:encoded>
			<wfw:commentRss>http://blog.zhourunsheng.com/2012/01/android-%e4%bf%a1%e6%81%af%e5%85%b1%e4%ba%ab%e4%b8%93%e9%a2%98%e4%b9%8bactionbar%e6%b7%bb%e5%8a%a0%e5%88%86%e4%ba%ab%e5%8a%a8%e4%bd%9c/feed/</wfw:commentRss>
		<slash:comments>0</slash:comments>
		</item>
		<item>
		<title>Android 信息共享专题之内容接收</title>
		<link>http://blog.zhourunsheng.com/2012/01/android-%e4%bf%a1%e6%81%af%e5%85%b1%e4%ba%ab%e4%b8%93%e9%a2%98%e4%b9%8b%e5%86%85%e5%ae%b9%e6%8e%a5%e6%94%b6/</link>
		<comments>http://blog.zhourunsheng.com/2012/01/android-%e4%bf%a1%e6%81%af%e5%85%b1%e4%ba%ab%e4%b8%93%e9%a2%98%e4%b9%8b%e5%86%85%e5%ae%b9%e6%8e%a5%e6%94%b6/#comments</comments>
		<pubDate>Sat, 07 Jan 2012 12:15:54 +0000</pubDate>
		<dc:creator><![CDATA[润物无声]]></dc:creator>
				<category><![CDATA[移动开发]]></category>
		<category><![CDATA[Android]]></category>
		<category><![CDATA[Training]]></category>

		<guid isPermaLink="false">http://blog.zhourunsheng.com/?p=1260</guid>
		<description><![CDATA[<p>如上节课《Android 信息共享之内容发布》所讲，您的应用可以给其他的APP发送内容，同样地，您的应用也可以 [&#8230;]</p>
<p><a rel="nofollow" href="http://blog.zhourunsheng.com/2012/01/android-%e4%bf%a1%e6%81%af%e5%85%b1%e4%ba%ab%e4%b8%93%e9%a2%98%e4%b9%8b%e5%86%85%e5%ae%b9%e6%8e%a5%e6%94%b6/">Android 信息共享专题之内容接收</a>，首发于<a rel="nofollow" href="http://blog.zhourunsheng.com">润物无声</a>。</p>
]]></description>
				<content:encoded><![CDATA[<p><img class="alignleft" title="training-prof" alt src="http://blog.zhourunsheng.com/wp-content/uploads/2011/12/training-prof-150x150.png" width="150" height="150"/></p>
<p>如上节课《<a href="http://blog.zhourunsheng.com/2012/01/android-%E4%BF%A1%E6%81%AF%E5%85%B1%E4%BA%AB%E4%B8%93%E9%A2%98%E4%B9%8B%E5%86%85%E5%AE%B9%E5%8F%91%E5%B8%83/">Android 信息共享之内容发布</a>》所讲，您的应用可以给其他的APP发送内容，同样地，您的应用也可以接收和处理其他APP发送的内容。设计之初，您就应该考虑，如何让您的APP与用户交互，以及您的APP主要处理哪些类型的数据。例如，一个网络社交类的APP可能对纯文本格式的数据感兴趣，像网页的URL地址。 <a href="https://market.android.com/details?id=com.google.android.apps.plus">Google+ Android application</a> 则同时对纯文本格式的数据和单张或多张图片类型的数据感兴趣，因为这样可以方便用户在 Gallery APP 浏览图片的时候发送一条附带图片的 Google+ 信息流（Post）。</p>
<p><span id="more-1260"></span><br />
<h3>更新 Manifest 文件</h3>
<p>Intent filters 主要用来告诉系统您的APP关心什么类型的系统消息。如前面章节《<a href="http://blog.zhourunsheng.com/2012/01/android-%E4%BF%A1%E6%81%AF%E5%85%B1%E4%BA%AB%E4%B8%93%E9%A2%98%E4%B9%8B%E5%86%85%E5%AE%B9%E5%8F%91%E5%B8%83/">Android 信息共享之内容发布</a>》构造发送内容的Intent时需要设置Action为<a href="http://developer.android.com/reference/android/content/Intent.html#ACTION_SEND">ACTION_SEND</a>，那么接收内容同样要告诉系统您的APP对Action为ACTION_SEND的系统消息感兴趣。您需要在 <a href="http://developer.android.com/guide/topics/intents/intents-filters.html#ifs">&lt;intent-filter&gt;</a> tag中定义关注的intent filter，比如，您的APP可以用来处理纯文本格式的数据，通用类型的单张图片，或者混合类型的图片列表，那么您的manifest 可能是这样的：</p>
<pre>   &lt;activity android:name=".ui.MyActivity" &gt;
   &lt;intent-filter&gt;
      &lt;action android:name="android.intent.action.SEND" /&gt;
      &lt;category android:name="android.intent.category.DEFAULT" /&gt;
      &lt;data android:mimeType="image/*" /&gt;
   &lt;/intent-filter&gt;
   &lt;intent-filter&gt;
      &lt;action android:name="android.intent.action.SEND" /&gt;
      &lt;category android:name="android.intent.category.DEFAULT" /&gt;
      &lt;data android:mimeType="text/plain" /&gt;
   &lt;/intent-filter&gt;
   &lt;intent-filter&gt;
     &lt;action android:name="android.intent.action.SEND_MULTIPLE" /&gt;
     &lt;category android:name="android.intent.category.DEFAULT" /&gt;
     &lt;data android:mimeType="image/*" /&gt;
   &lt;/intent-filter&gt;
   &lt;/activity&gt;</pre>
<p><font color="#FF00FF">注</font>：想了解更多的有关Intent和Intent Filter的资讯请参照 《 <a href="http://developer.android.com/guide/topics/intents/intents-filters.html#ifs">Intents and Intent Filters</a>》。</p>
<p>假如其他的APP发送了与上面manifest文件中声明的intent filter匹配的数据（通过声明Intent，设置Action值，填充数据，调用<a href="http://developer.android.com/reference/android/content/Context.html#startActivity(android.content.Intent)">startActivity()</a>方法），那么您的APP就会出现在 intent chooser（选择器）中，当用户选择了您的应用程序，对应的activity （如上文声明的 <span style="COLOR: #ff0080">.ui.MyActivity</span>）就会被启动，具体怎么样处理接收到的数据就取决于您的设计了。</p>
<h3>处理接收到的数据</h3>
<p>要想得到其他APP发送过来的数据，您首先需要调用方法<a href="http://developer.android.com/reference/android/content/Intent.html#getIntent(java.lang.String)">getIntent()</a> 来获得包含数据的<a href="http://developer.android.com/reference/android/content/Intent.html">Intent</a>对象，一旦您得到了该对象，就可以读取里面的值来决定下一步的动作。有一点您需要注意，如果您的Activity的启动入口不止这一处，比如用户可以通过主界面launcher启动，那么您就应该小心应对此类问题，主要区别是两者 <a href="http://developer.android.com/reference/android/content/Intent.html">Intent</a>包含的内容不同。</p>
<p>示例代码</p>
<pre>   void onCreate (Bundle savedInstanceState) {
   ...
   // Get intent, action and MIME type
   Intent intent = getIntent();
   String action = intent.getAction();
   String type = intent.getType();

   if (Intent.ACTION_SEND.equals(action) &amp;&amp; type != null) {
      if ("text/plain".equals(type)) {
        handleSendText(intent); // Handle text being sent
      } else if (type.startsWith("image/")) {
        handleSendImage(intent); // Handle single image being sent
      }
   } else if (Intent.ACTION_SEND_MULTIPLE.equals(action) &amp;&amp; type != null) {
     if (type.startsWith("image/")) {
        handleSendMultipleImages(intent); // Handle multiple images being sent
     }
   } else {
     // Handle other intents, such as being started from the home screen
   }
   ...
   }

   void handleSendText(Intent intent) {
     String sharedText = intent.getStringExtra(Intent.EXTRA_TEXT);
     if (sharedText != null) {
      // Update UI to reflect text being shared
     }
   }

   void handleSendImage(Intent intent) {
     Uri imageUri = (Uri) intent.getParcelableExtra(Intent.EXTRA_STREAM);
     if (imageUri != null) {
       // Update UI to reflect image being shared
     }
   }

   void handleSendMultipleImages(Intent intent) {
      ArrayList&lt;Uri&gt; imageUris = intent.getParcelableArrayListExtra(Intent.EXTRA_STREAM);
      if (imageUris != null) {
        // Update UI to reflect multiple images being shared
      }
   }</pre>
<p><span style="COLOR: #ff0000"><strong>注意</strong></span> ：对于从其他APP发送过来的数据，您需要特别小心地检查和处理，因为您不可能提前预知它们会发送什么样的数据。例如，它们有可能设置了错误的MIME 类型，或者传递一张非常大的图片，还有一点需要注意，处理二进制数据的时候最好新启一个独立的线程来处理，而不要放在主线程（main ("UI") thread）中，防止界面阻塞。</p>
<p>更新UI界面可以很简单，比如将接收到的纯文本格式的数据显示到界面的<a href="http://developer.android.com/reference/android/widget/EditText.html">EditText</a>控件中，也可以很复杂，比如对接收到的图片数据做图像的变换处理，然后再显示。具体进行怎样的处理各不相同，这要看您APP的设计了。<br/></p>
<p><strong>参考文摘：</strong></p>
<p><a href="http://developer.android.com/training/sharing/receive.html">http://developer.android.com/training/sharing/receive.html</a></p>
<p><a rel="nofollow" href="http://blog.zhourunsheng.com/2012/01/android-%e4%bf%a1%e6%81%af%e5%85%b1%e4%ba%ab%e4%b8%93%e9%a2%98%e4%b9%8b%e5%86%85%e5%ae%b9%e6%8e%a5%e6%94%b6/">Android 信息共享专题之内容接收</a>，首发于<a rel="nofollow" href="http://blog.zhourunsheng.com">润物无声</a>。</p>
]]></content:encoded>
			<wfw:commentRss>http://blog.zhourunsheng.com/2012/01/android-%e4%bf%a1%e6%81%af%e5%85%b1%e4%ba%ab%e4%b8%93%e9%a2%98%e4%b9%8b%e5%86%85%e5%ae%b9%e6%8e%a5%e6%94%b6/feed/</wfw:commentRss>
		<slash:comments>0</slash:comments>
		</item>
		<item>
		<title>Android 信息共享专题之内容发布</title>
		<link>http://blog.zhourunsheng.com/2012/01/android-%e4%bf%a1%e6%81%af%e5%85%b1%e4%ba%ab%e4%b8%93%e9%a2%98%e4%b9%8b%e5%86%85%e5%ae%b9%e5%8f%91%e5%b8%83/</link>
		<comments>http://blog.zhourunsheng.com/2012/01/android-%e4%bf%a1%e6%81%af%e5%85%b1%e4%ba%ab%e4%b8%93%e9%a2%98%e4%b9%8b%e5%86%85%e5%ae%b9%e5%8f%91%e5%b8%83/#comments</comments>
		<pubDate>Fri, 06 Jan 2012 12:03:18 +0000</pubDate>
		<dc:creator><![CDATA[润物无声]]></dc:creator>
				<category><![CDATA[移动开发]]></category>
		<category><![CDATA[Android]]></category>
		<category><![CDATA[Training]]></category>

		<guid isPermaLink="false">http://blog.zhourunsheng.com/?p=1257</guid>
		<description><![CDATA[<p>构造和触发一个Intent，需要先要指定其Action。Android系统已经预定义了很多Action，其中的 [&#8230;]</p>
<p><a rel="nofollow" href="http://blog.zhourunsheng.com/2012/01/android-%e4%bf%a1%e6%81%af%e5%85%b1%e4%ba%ab%e4%b8%93%e9%a2%98%e4%b9%8b%e5%86%85%e5%ae%b9%e5%8f%91%e5%b8%83/">Android 信息共享专题之内容发布</a>，首发于<a rel="nofollow" href="http://blog.zhourunsheng.com">润物无声</a>。</p>
]]></description>
				<content:encoded><![CDATA[<p><img class="alignleft" title="training-prof" alt src="http://blog.zhourunsheng.com/wp-content/uploads/2011/12/training-prof-150x150.png" width="150" height="150"/>
<p>构造和触发一个Intent，需要先要指定其Action。Android系统已经预定义了很多Action，其中的<a href="http://developer.android.com/reference/android/content/Intent.html#ACTION_SEND">ACTION_SEND</a>用来做数据分享，可以把数据从一个Activity发送到另一个Activity， 甚至跨越进程边界的限制。要想给其他的Activity发送内容，您需要做的是构造一个Intent，设置Action类别为<a href="http://developer.android.com/reference/android/content/Intent.html#ACTION_SEND">ACTION_SEND</a>，把数据填充到该Intent中，并且设置正确的数据的类型，最后触发该Intent，这样系统就会选择最合适的目标Activity来接收和处理该数据。如果候选的目标Activity多于一个，则系统通常会弹出一个对话框让用户来选择，如果候选的目标Activity只有一个，那么系统就会直接启动该Activity来处理数据。同样地，您自己的应用程序也可以在manifest文件中声明可以处理的数据类型，这样当其他Activity发送此种类型的数据时，系统就会将您的App列入到候选名单中。</p>
<p>数据的共享常应用在社会化分享的APP中，方便用户快速和简单的分享自己的共享信息。</p>
<p><span style="COLOR: #ff0000"><strong>注意</strong></span> ：最简单的在<a href="http://developer.android.com/reference/android/app/ActionBar.html">ActionBar</a>中添加一个"share action item"的方法是使用<a href="http://developer.android.com/reference/android/widget/ShareActionProvider.html">ShareActionProvider</a>，其要求<span style="COLOR: #ff0080">API level 14</span>，将在后面的章节讨论。</p>
<p> <span id="more-1257"></span><br />
<h3>发送文本数据</h3>
<p>数据分享最常用的是Activity之间发送文本内容，例如，系统的内置浏览器可以共享当前正在浏览页面的URL地址，以纯文本的数据格式存储，这非常有助于用户通过Email或者社会化分享APP分享有趣的文章链接和网站链接。</p>
<p>示例代码：</p>
<pre>   Intent sendIntent = new Intent();
   sendIntent.setAction(Intent.ACTION_SEND);
   sendIntent.putExtra(Intent.EXTRA_TEXT, "This is my text to send.");
   sendIntent.setType("text/plain");
   startActivity(sendIntent);</pre>
<p>如果当前设备里安装的所有应用程序中其中有一个APP的intent filter设置了<a href="http://developer.android.com/reference/android/content/Intent.html#ACTION_SEND">ACTION_SEND</a>，并且声明的数据类型匹配纯文本（<span style="COLOR: #ff0080">text/plain</span>）格式，那么系统就会启动它，若此时匹配的APP个数多于一个，系统会弹出一个选择对话框(a "chooser") ，让用户从中选择处理该数据的APP。若您触发该Intent的时候使用了<a href="http://developer.android.com/reference/android/content/Intent.html#createChooser(android.content.Intent, java.lang.CharSequence)">Intent.createChooser()</a> ，那么系统总是会弹出选择对话框(a "chooser") 让用户选择，这么做有如下优点。</p>
<ul>
<li>即使用户以前选择过默认的处理APP，系统还是会弹出选择器让用户可以重新选择</li>
<li>如果没有任何匹配的APP可以处理该数据，那么系统会有提示信息</li>
<li>您可以为选择器对话框设定自定义标题</li>
</ul>
<p>示例代码：</p>
<pre>   Intent sendIntent = new Intent();
   sendIntent.setAction(Intent.ACTION_SEND);
   sendIntent.putExtra(Intent.EXTRA_TEXT, "This is my text to send.");
   sendIntent.setType("text/plain");
   startActivity(Intent.createChooser(sendIntent, getResources().getText(R.string.send_to));</pre>
<p>ACTION_SEND Intent　选择器对话框如下面图示</p>
<p><img alt src="http://farm8.staticflickr.com/7022/6646562279_fd0fc74854_d.jpg" width="200" height="355"/></p>
<p>可选地，您也可以在Intent中设置一些标准的附加参数，比如<a href="http://developer.android.com/reference/android/content/Intent.html#EXTRA_EMAIL">EXTRA_EMAIL</a>, <a href="http://developer.android.com/reference/android/content/Intent.html#EXTRA_CC">EXTRA_CC</a>, <a href="http://developer.android.com/reference/android/content/Intent.html#EXTRA_BCC">EXTRA_BCC</a>,　<a href="http://developer.android.com/reference/android/content/Intent.html#EXTRA_SUBJECT">EXTRA_SUBJECT</a>，除非目标APP会使用该数据，不然也没有什么副作用。通常如果您设计的应用程序可以用来接收和处理该数据，那么应该使用它们。</p>
<p><span style="COLOR: #ff0000">注意</span>：某些Email邮件处理程序，比如Gmail，需要的是一个字符串数组类型（<a href="http://developer.android.com/reference/java/lang/String.html">String[]</a> for extras like <a href="http://developer.android.com/reference/android/content/Intent.html#EXTRA_EMAIL">EXTRA_EMAIL</a> and <a href="http://developer.android.com/reference/android/content/Intent.html#EXTRA_CC">EXTRA_CC</a>），那么您应该使用<a href="http://developer.android.com/reference/android/content/Intent.html#putExtra(java.lang.String, java.lang.String[])">putExtra(String, String[])</a> 来添加附加参数。</p>
<h3>发送二进制数据</h3>
<p>与发送文本格式的数据类似，构造一个Intent其Action为<a href="http://developer.android.com/reference/android/content/Intent.html#ACTION_SEND">ACTION_SEND</a>，设定正确的MIME类型，填充数据的URI 到Intent的键值 <a href="http://developer.android.com/reference/android/content/Intent.html#EXTRA_STREAM">EXTRA_STREAM</a>中，最后触发该Intent就可以了。共享二进制数据最常用的是图片数据的分享，其他任何的二进制数据都可以分享，只要设定了正确的MIME类型。</p>
<p>示例代码：</p>
<pre>   Intent shareIntent = new Intent();
   shareIntent.setAction(Intent.ACTION_SEND);
   shareIntent.putExtra(Intent.EXTRA_STREAM, uriToImage);
   shareIntent.setType("image/jpeg");
   startActivity(Intent.createChooser(shareIntent, getResources().getText(R.string.send_to)));</pre>
<p>注意事项：</p>
<ol>
<li>MIME 类型可以设定为<span style="COLOR: #ff0080">"*/*",</span> 但是这只能匹配能够处理通用数据流的Activities</li>
<li>目标APP应当有读取Uri 指向的数据的权限，解决办法如下：
<ul>
<li>将要共享的数据写到外部/共享的存储设备上，例如SD卡，这样所有的APP都可以读取。通过方法<a href="http://developer.android.com/reference/android/net/Uri.html#fromFile(java.io.File)">Uri.fromFile()</a> 获取该文件的<a href="http://developer.android.com/reference/android/net/Uri.html">Uri</a> 地址，然后设置到Intent中。请记住，不是所有的APP都能正确识别"<span style="COLOR: #ff0080">file://</span> "风格的<a href="http://developer.android.com/reference/android/net/Uri.html">Uri</a> 地址。</li>
<li>通过方法<a href="http://developer.android.com/reference/android/content/Context.html#openFileOutput(java.lang.String, int)">openFileOutput()</a> 将数据保存到自己APP的目录中，权限设置为任何APP可读取（ <a href="http://developer.android.com/reference/android/content/Context.html#MODE_WORLD_READABLE">MODE_WORLD_READABLE</a> ），然后调用方法<a href="http://developer.android.com/reference/android/content/Context.html#getFileStreamPath(java.lang.String)">getFileStreamPath()</a> 会返回一个<a href="http://developer.android.com/reference/java/io/File.html">File</a>的类型，最后同样通过方法　<a href="http://developer.android.com/reference/android/net/Uri.html#fromFile(java.io.File)">Uri.fromFile()</a> 获取到该文件"<span style="COLOR: #ff0080">file://</span> "风格的<a href="http://developer.android.com/reference/android/net/Uri.html">Uri</a> 地址</li>
<li>多媒体类型的文件，比如图片，视频文件和音频文件可以通过方法 <a href="http://developer.android.com/reference/android/media/MediaScannerConnection.html#scanFile(android.content.Context, java.lang.String[], java.lang.String[], android.media.MediaScannerConnection.OnScanCompletedListener)">scanFile()</a>添加到系统的多媒体数据库（<a href="http://developer.android.com/reference/android/provider/MediaStore.html">MediaStore</a> ）， <a href="http://developer.android.com/reference/android/media/MediaScannerConnection.OnScanCompletedListener.html#onScanCompleted(java.lang.String, android.net.Uri)">onScanCompleted()</a> 回调函数就会返回"<span style="COLOR: #ff0080">content://</span> "格式的<a href="http://developer.android.com/reference/android/net/Uri.html">Uri</a> 地址</li>
<li>图片文件也可以通过方法 <a href="http://developer.android.com/reference/android/provider/MediaStore.Images.Media.html#insertImage(android.content.ContentResolver, android.graphics.Bitmap, java.lang.String, java.lang.String)">insertImage()</a> 添加到系统的多媒体数据库（<a href="http://developer.android.com/reference/android/provider/MediaStore.html">MediaStore</a> ），函数调用会返回"<span style="COLOR: #ff0080">content://</span> "格式的<a href="http://developer.android.com/reference/android/net/Uri.html">Uri</a> 地址</li>
<li>使用<a href="http://developer.android.com/reference/android/content/ContentProvider.html">ContentProvider</a>来共享您APP的数据，请确保目标APP拥有访问您APP Provider 的正确权限设置（请参照<a href="http://developer.android.com/guide/topics/security/security.html#uri">per-URI permissions</a>）</li>
</ul>
</li>
</ol>
<h3>发送批量数据</h3>
<p>要想批量发送数据，Intent的Action应设置为<a href="http://developer.android.com/reference/android/content/Intent.html#ACTION_SEND_MULTIPLE">ACTION_SEND_MULTIPLE</a> ，然后再设置批量数据的URIs 地址列表，MIME 类型随着混合数据格式的不同而不同。比如，您发送3张JPEG 格式的图片，那么MIME 类型仍然是"<span style="COLOR: #ff0080">image/jpeg</span>"，如果发送的一系列图片中格式不完全相同，那么MIME 类型就应当设置为通用图片格式"<span style="COLOR: #ff0080">image/*</span>" ，如果您发送的一系列数据中类型都不完全一样，那么MIME 类型就应当设置为通用格式"<span style="COLOR: #ff0080">*/*</span>" 。请注意，"<span style="COLOR: #ff0080">image/*</span>"只匹配能处理任何图片格式的APP，而"<span style="COLOR: #ff0080">*/*</span>" 只能匹配处理通用数据类型的APP。</p>
<p>示例代码</p>
<pre>   ArrayList&lt;Uri&gt; imageUris = new ArrayList&lt;Uri&gt;();
   imageUris.add(imageUri1); // Add your image URIs here
   imageUris.add(imageUri2);

   Intent shareIntent = new Intent();
   shareIntent.setAction(Intent.ACTION_SEND_MULTIPLE);
   shareIntent.putParcelableArrayListExtra(Intent.EXTRA_STREAM, imageUris);
   shareIntent.setType("image/*");
   startActivity(Intent.createChooser(shareIntent, "Share images to.."));</pre>
<p>如上文所提到的，请确保目标APP有足够的权限来访问<a href="http://developer.android.com/reference/android/net/Uri.html">Uris</a>地址指向的数据。</p>
<p><strong>参考文摘：</strong></p>
<p><a href="http://developer.android.com/training/sharing/send.html">http://developer.android.com/training/sharing/send.html</a></p>
<p><a rel="nofollow" href="http://blog.zhourunsheng.com/2012/01/android-%e4%bf%a1%e6%81%af%e5%85%b1%e4%ba%ab%e4%b8%93%e9%a2%98%e4%b9%8b%e5%86%85%e5%ae%b9%e5%8f%91%e5%b8%83/">Android 信息共享专题之内容发布</a>，首发于<a rel="nofollow" href="http://blog.zhourunsheng.com">润物无声</a>。</p>
]]></content:encoded>
			<wfw:commentRss>http://blog.zhourunsheng.com/2012/01/android-%e4%bf%a1%e6%81%af%e5%85%b1%e4%ba%ab%e4%b8%93%e9%a2%98%e4%b9%8b%e5%86%85%e5%ae%b9%e5%8f%91%e5%b8%83/feed/</wfw:commentRss>
		<slash:comments>1</slash:comments>
		</item>
		<item>
		<title>Android 用户管理专题之创建自定义帐户类型</title>
		<link>http://blog.zhourunsheng.com/2012/01/android-%e7%94%a8%e6%88%b7%e7%ae%a1%e7%90%86%e4%b8%93%e9%a2%98%e4%b9%8b%e5%88%9b%e5%bb%ba%e8%87%aa%e5%ae%9a%e4%b9%89%e5%b8%90%e6%88%b7%e7%b1%bb%e5%9e%8b/</link>
		<comments>http://blog.zhourunsheng.com/2012/01/android-%e7%94%a8%e6%88%b7%e7%ae%a1%e7%90%86%e4%b8%93%e9%a2%98%e4%b9%8b%e5%88%9b%e5%bb%ba%e8%87%aa%e5%ae%9a%e4%b9%89%e5%b8%90%e6%88%b7%e7%b1%bb%e5%9e%8b/#comments</comments>
		<pubDate>Thu, 05 Jan 2012 12:39:40 +0000</pubDate>
		<dc:creator><![CDATA[润物无声]]></dc:creator>
				<category><![CDATA[移动开发]]></category>
		<category><![CDATA[Android]]></category>
		<category><![CDATA[Training]]></category>

		<guid isPermaLink="false">http://blog.zhourunsheng.com/?p=1250</guid>
		<description><![CDATA[<p>到目前为止，我们学习和讨论的都是利用Google的账户来访问Google的在线服务，假如您有自己的在线服务，却 [&#8230;]</p>
<p><a rel="nofollow" href="http://blog.zhourunsheng.com/2012/01/android-%e7%94%a8%e6%88%b7%e7%ae%a1%e7%90%86%e4%b8%93%e9%a2%98%e4%b9%8b%e5%88%9b%e5%bb%ba%e8%87%aa%e5%ae%9a%e4%b9%89%e5%b8%90%e6%88%b7%e7%b1%bb%e5%9e%8b/">Android 用户管理专题之创建自定义帐户类型</a>，首发于<a rel="nofollow" href="http://blog.zhourunsheng.com">润物无声</a>。</p>
]]></description>
				<content:encoded><![CDATA[<p><img class="alignleft" title="training-prof" src="http://blog.zhourunsheng.com/wp-content/uploads/2011/12/training-prof-150x150.png" alt="" width="150" height="150" /></p>
<p>到目前为止，我们学习和讨论的都是利用Google的账户来访问Google的在线服务，假如您有自己的在线服务，却没有Google类型的账户，这个时候该怎么办呢？本节课的目的就是来解决上面提到的问题，为自己的在线服务创建一个自定义的账户类型，并且像设备的内置账户一样的方式工作。<br />
<span id="more-1250"></span></p>
<h3>创建自定义账户</h3>
<p>要想创建一个新的自定义类型的账户，首先要做的工作是生成用户凭据（credentials），该过程可以很简单，比如要求用户输入账户名和密码，也可以复杂一些，要求用户设置一次性密码或启用生物特征扫描（如视网膜扫描，指纹识别）。不论是哪种方式，这是您必须要做的工作。<br />
具体包括：</p>
<ol>
<li>收集用户凭据</li>
<li>将用户凭据同在线服务进行身份认证</li>
<li>存储认证过的用户凭据</li>
</ol>
<p>通常这三个步骤可以在一个 activity 中完成，我们可以将这个activity 称之为认证 activity （authenticator activity）。</p>
<p>因为 authenticator activity 需要访问系统的账户管理器 <a href="http://developer.android.com/reference/android/accounts/AccountManager.html">AccountManager</a>，比普通的 activity 有更多的权限要求，Android framework 为了方便应用程序的开发，特意提供了一个基类 <a href="http://developer.android.com/reference/android/accounts/AccountAuthenticatorActivity.html">AccountAuthenticatorActivity</a>，您可以通过继承该类来实现自定义的账户验证器。</p>
<p>前两个步骤（收集凭据和服务验证）如何实现完全取决于您应用程序的设计，最后一个步骤（存储凭据）是一个标准的实现，代码如下：</p>
<pre> final Account account = new Account(mUsername, your_account_type);
 mAccountManager.addAccountExplicitly(account, mPassword, null);</pre>
<h3>智能安全</h3>
<p>需要特别注意的是： <a href="http://developer.android.com/reference/android/accounts/AccountManager.html">AccountManager</a> 本身并不对用户凭据（credentials）做任何的加密处理，以<strong>纯文本</strong>的方式进行存储，在大部分的设备上，这都没有问题，因为 credentials 保存在只有 root 权限才能读取的数据库中，但是对于已经 root 解锁的设备来说，就有可能出现问题，此时任何人可通过 <span style="color: #0000ff;">adb</span> 工具来读取系统中存储的 credentials 信息。</p>
<p>考虑到这些因素，您就不应该简单的把用户的实际密码通过 <a href="http://developer.android.com/reference/android/accounts/AccountManager.html#addAccountExplicitly(android.accounts.Account,%20java.lang.String,%20android.os.Bundle)"> AccountManager.addAccountExplicitly()</a> 来保存，相反，应该存入一个更加安全的加密的安全令牌来抵御安全攻击。如果您的用户凭据用来保护非常有价值的数据，您在设计的时候应该慎重考虑。</p>
<p><strong style="color: #ff0000;">Remember</strong>: When it comes to security code, follow the "Mythbusters" rule: don't try this at home! Consult a security professional before implementing any custom account code.</p>
<p>OK，到此为止安全问题就暂告一段落了，验证器的代码框架我们也已经熟悉了，接下来的工作就是把整套流程衔接起来。</p>
<h3>编写验证器（继承扩展AbstractAccountAuthenticator）</h3>
<p>为了要让系统的账户管理器 <a href="http://developer.android.com/reference/android/accounts/AccountManager.html">AccountManager</a> 来管理您自定义的账户类型，那么您必须要实现 <a href="http://developer.android.com/reference/android/accounts/AccountManager.html">AccountManager</a> 用来管理帐号的一个接口，这个接口就是验证器 <span style="color: #ff00ff;">authenticator class</span>。</p>
<p>创建一个自定义验证器的最简单的方法莫过于继承系统提供的验证器虚类 <a href="http://developer.android.com/reference/android/accounts/AbstractAccountAuthenticator.html">AbstractAccountAuthenticator</a>，然后实现里面的抽象方法 （abstract methods）。如果您已经接触过前面的课程，那么这些抽象方法应该是很熟悉了，他们就是获取账户信息和授权令牌后的回调方法。</p>
<p>实现一个自定义的验证器需要下面几部分的代码，首先您需要实现 <a href="http://developer.android.com/reference/android/accounts/AbstractAccountAuthenticator.html">AbstractAccountAuthenticator</a> 中存在的7个抽象方法，其次您需要在 manifest 文件中加入 <a href="http://developer.android.com/guide/topics/manifest/intent-filter-element.html">intent filter</a> for "<span style="color: #ff0080;">android.accounts.AccountAuthenticator</span>"（下一节课程会讲解），最后，您还需要提供两个 XML 资源文件，除了其他的事项，需要提供自定义账户的类型名称和图标，系统显示账户的时候需要将这两项信息显示在账户的旁边。</p>
<p>您可以参照该 <a href="http://developer.android.com/reference/android/accounts/AbstractAccountAuthenticator.html">AbstractAccountAuthenticator</a> 指导文件一步一步实现一个自定义的验证器，这边还有 Demo 示例 （<a href="http://developer.android.com/resources/samples/SampleSyncAdapter/index.html">SampleSyncAdapter sample app</a>）可供参考。</p>
<p>阅读Demo 示例 （<a href="http://developer.android.com/resources/samples/SampleSyncAdapter/index.html">SampleSyncAdapter sampleapp</a>）的过程中，您可能发现很多方法都在 bundle 中返回一个 intent，这个 intent 同时也用来启动您的自定义验证器 （custom authenticator activity）。如果您需要特殊的初始化参数，那么可以通过调用方法 <a href="http://developer.android.com/reference/android/content/Intent.html#putExtra(java.lang.String,%20android.os.Bundle)">Intent.putExtra（）</a>进行添加。</p>
<h3>创建验证器服务（authenticator service.）</h3>
<p>前面的代码中您已经实现了自定义的验证器类，现在需要一个服务来启动验证过程。因为该验证器能够被多个程序使用，并且后台运行，那么最佳的选择方案莫过于创建一个服务（<a href="http://developer.android.com/reference/android/app/Service.html">Service</a>），我们称之为验证器服务。</p>
<p>验证器服务的实现可以很简单，在服务的<a href="http://developer.android.com/reference/android/app/Service.html#onCreate()">onCreate()</a> 方法中创建一个验证器实例，在<a href="http://developer.android.com/reference/android/app/Service.html#onBind(android.content.Intent)">onBind()</a>方法中调用<a href="http://developer.android.com/reference/android/accounts/AbstractAccountAuthenticator.html#getIBinder()">getIBinder()</a> ，Demo 示例（<a href="http://developer.android.com/resources/samples/SampleSyncAdapter/index.html">SampleSyncAdapter sample app</a>）中可以找到怎么样定义一个验证器服务。</p>
<p>最后要做的工作就是在 manifest 文件中的 <span style="color: #ff00ff;">&lt;service&gt;</span> tag 中添加 intent filter 和声明验证器。</p>
<p>示例代码：</p>
<pre>   &lt;service ...&gt;
   &lt;intent-filter&gt;
       &lt;action android:name="android.accounts.AccountAuthenticator" /&gt;
   &lt;/intent-filter&gt;
   &lt;meta-data android:name="android.accounts.AccountAuthenticator"
        android:resource="@xml/authenticator" /&gt;
   &lt;/service&gt;</pre>
<h3>发布您的服务</h3>
<p>到此为止，工作算是顺利完成了！现在系统的账户管理器可以正常识别您的自定义账户类型了，同系统已有的其他账户类型相似，比如 "Google" 和 "Corporate"。 您可以在系统的 <strong>Accounts &amp;Sync</strong> 设定界面添加账户， 当用户选择账户类型的时候，您新添加的账户类型就可以正常选择了。</p>
<p>当然，所有这些的前提是您的验证器服务已经正确地安装到设备上。假如只有一个APP会使用此服务，肯定不会出现什么问题，APP和该服务绑定即可。但是如果有多个应用需要使用您的验证器服务，情况会变得稍微复杂些，您可不愿意每一个APP都绑定一个此服务的副本，这回占用很多的设备空间。</p>
<p>一种解决方法是将您的验证器服务做成一个独立的小的APK安装程序，当一个APP想要使用您的自定义的账户类型时，先检查一下设备的账户管理器，看是否支持，如果不支持的话，可以引导用户从 Android Market 上面下载和安装。这看起来似乎比较麻烦，但是比较起每次使用APP的时候都需要重复输入账户凭证来说还是好多了。</p>
<p><strong>参考文摘：</strong><br />
<a href="http://developer.android.com/training/id-auth/custom_auth.html">http://developer.android.com/training/id-auth/custom_auth.html</a></p>
<p><a rel="nofollow" href="http://blog.zhourunsheng.com/2012/01/android-%e7%94%a8%e6%88%b7%e7%ae%a1%e7%90%86%e4%b8%93%e9%a2%98%e4%b9%8b%e5%88%9b%e5%bb%ba%e8%87%aa%e5%ae%9a%e4%b9%89%e5%b8%90%e6%88%b7%e7%b1%bb%e5%9e%8b/">Android 用户管理专题之创建自定义帐户类型</a>，首发于<a rel="nofollow" href="http://blog.zhourunsheng.com">润物无声</a>。</p>
]]></content:encoded>
			<wfw:commentRss>http://blog.zhourunsheng.com/2012/01/android-%e7%94%a8%e6%88%b7%e7%ae%a1%e7%90%86%e4%b8%93%e9%a2%98%e4%b9%8b%e5%88%9b%e5%bb%ba%e8%87%aa%e5%ae%9a%e4%b9%89%e5%b8%90%e6%88%b7%e7%b1%bb%e5%9e%8b/feed/</wfw:commentRss>
		<slash:comments>0</slash:comments>
		</item>
		<item>
		<title>Android 用户管理专题之基于OAuth2协议的服务认证</title>
		<link>http://blog.zhourunsheng.com/2012/01/android-%e7%94%a8%e6%88%b7%e7%ae%a1%e7%90%86%e4%b8%93%e9%a2%98%e4%b9%8b%e5%9f%ba%e4%ba%8eoauth2%e5%8d%8f%e8%ae%ae%e7%9a%84%e6%9c%8d%e5%8a%a1%e8%ae%a4%e8%af%81/</link>
		<comments>http://blog.zhourunsheng.com/2012/01/android-%e7%94%a8%e6%88%b7%e7%ae%a1%e7%90%86%e4%b8%93%e9%a2%98%e4%b9%8b%e5%9f%ba%e4%ba%8eoauth2%e5%8d%8f%e8%ae%ae%e7%9a%84%e6%9c%8d%e5%8a%a1%e8%ae%a4%e8%af%81/#comments</comments>
		<pubDate>Wed, 04 Jan 2012 15:34:56 +0000</pubDate>
		<dc:creator><![CDATA[润物无声]]></dc:creator>
				<category><![CDATA[移动开发]]></category>
		<category><![CDATA[Android]]></category>
		<category><![CDATA[Training]]></category>

		<guid isPermaLink="false">http://blog.zhourunsheng.com/?p=1242</guid>
		<description><![CDATA[<p>为了能安全地访问在线服务，必须要验证用户的身份，即需要用户提供证明自己身份的认证信息。 如果一个应用程序要访问 [&#8230;]</p>
<p><a rel="nofollow" href="http://blog.zhourunsheng.com/2012/01/android-%e7%94%a8%e6%88%b7%e7%ae%a1%e7%90%86%e4%b8%93%e9%a2%98%e4%b9%8b%e5%9f%ba%e4%ba%8eoauth2%e5%8d%8f%e8%ae%ae%e7%9a%84%e6%9c%8d%e5%8a%a1%e8%ae%a4%e8%af%81/">Android 用户管理专题之基于OAuth2协议的服务认证</a>，首发于<a rel="nofollow" href="http://blog.zhourunsheng.com">润物无声</a>。</p>
]]></description>
				<content:encoded><![CDATA[<p><img class="alignleft" title="training-prof" src="http://blog.zhourunsheng.com/wp-content/uploads/2011/12/training-prof-150x150.png" alt="training-prof" width="150" height="150" border="0" /></p>
<p>为了能安全地访问在线服务，必须要验证用户的身份，即需要用户提供证明自己身份的认证信息。 如果一个应用程序要访问第三方服务提供商的数据，那么安全验证问题更加复杂，此时不仅需要验证用户的身份信息，还要验证应用程序的信息，以此来保证应用程序只能访问获得了用户授权的那部分服务和数据。</p>
<p>目前用来解决应用程序和第三方服务提供商授权认证的标准协议是 OAuth2，它本身只提供了一个值，即授权令牌（<strong>auth token</strong>）。 它代表了用户的授权和应用程序按照用户授权的范围能够进行的数据访问与操作。 本节课展示了怎样通过 OAuth2 协议来认证和访问 Google 的一个服务（服务方必须支持 OAuth2 协议）， 虽然本节课的例子只是针对 Google Service，其他的任何第三方服务提供商只要支持 OAuth2 协议， 本文中提到的技术解决方案也同样适用。</p>
<p>使用 OAuth2 的优势：</p>
<ul>
<li>通过获得用户授权的账户来访问在线服务</li>
<li>按照用户的意愿进行授权应用程序的访问控制</li>
<li>处理授权和认证错误</li>
</ul>
<p><span id="more-1242"></span></p>
<h3>前期准备</h3>
<p>开始进行 OAuth2 认证之前，您首先需要获得服务方提供的 API 信息，具体包括：</p>
<ul>
<li>服务提供方的 API 请求地址</li>
<li>访问类型（认证范围-<strong>auth scope</strong>），一个字符串类型的数据，用来标志应用程序申请的特定类型的认证。例如，auth scope 是 <span style="color: #ff00ff;">View your tasks</span>，那么就是 read-only（只读）方式访问 Google Tasks 服务， 如果 auth scope 是 <span style="color: #ff00ff;">Manage Your Tasks</span>，那么就是 read-write（读写）方式访问Google Tasks 服务。</li>
<li><strong>client id</strong> 和 <strong>client secret，</strong>同样是字符串类型，用来唯一标志访问在线服务的应用程序， 也就是您自己接下来要发开的应用程序，这些信息可以从服务提供商那里直接获得。Google 提供了一个自助服务器来方便用户申请。您可以通过阅读这篇文章 <a href="http://code.google.com/apis/tasks/articles/oauth-and-tasks-on-android.html" target="_blank">《Getting Started with the Tasks API and OAuth 2.0 on Android》</a> 来了解怎么样申请Google 的 Tasks 服务。</li>
</ul>
<h3>获得访问令牌 （Auth Token）</h3>
<p>获得访问令牌的步骤参照下图：</p>
<p><img src="http://developer.android.com/images/training/oauth_dance.png" alt="oauth_dance" width="277" height="340" border="0" /></p>
<p>在要获取访问令牌之前，您首先要做的是在 manifest 文件中加入账户管理 <a href="http://developer.android.com/reference/android/Manifest.permission.html#ACCOUNT_MANAGER">ACCOUNT_MANAGER</a>  权限。 如果要利用该令牌来访问在线服务，您还需要网络 <a href="http://developer.android.com/reference/android/Manifest.permission.html#INTERNET" target="_blank">INTERNET </a> 权限。<br />
加入的方法参见示例代码：</p>
<pre>     &lt;manifest ... &gt;
        &lt;uses-permission android:name="android.permission.ACCOUNT_MANAGER" /&gt;
        &lt;uses-permission android:name="android.permission.INTERNET" /&gt;
        ...
     &lt;/manifest&gt;</pre>
<p>当权限加入完毕的时候，接下来的工作就是申请令牌了，通过调用方法 <a href="http://developer.android.com/reference/android/accounts/AccountManager.html#getAuthToken(android.accounts.Account, java.lang.String, android.os.Bundle, android.app.Activity, android.accounts.AccountManagerCallback&lt;android.os.Bundle&gt;, android.os.Handler)" target="_blank">AccountManager.getAuthToken() </a>来获取令牌。</p>
<p><strong><span style="color: #ff0000;">特别注意</span></strong>，因为 <a href="http://developer.android.com/reference/android/accounts/AccountManager.html" target="_blank">AccountManager</a> 中的很多方法都涉及到网络连接，所以大部分方法都是异步调用，那么也就意着，您不能只在一个方法中简单按照 OAuth2 的认证流程来编写程序，而是要拆分成一系列的回调函数（callbacks）来实现。<br />
示例代码：</p>
<pre>  AccountManager am = AccountManager.get(this);
  Bundle options = new Bundle();

  am.getAuthToken(
    myAccount_,                     // Account retrieved using getAccountsByType()
    "Manage your tasks",            // Auth scope
    options,                        // Authenticator-specific options
    this,                           // Your activity
    new OnTokenAcquired(),          // Callback called when a token is successfully acquired
    new Handler(new OnError()));    // Callback called if an error occurs</pre>
<p>在上面的例子中，<span style="color: #ff00ff;">OnTokenAcquired</span> 类实现了 <a href="http://developer.android.com/reference/android/accounts/AccountManagerCallback.html" target="_blank">AccountManagerCallback</a> 接口。 <a href="http://developer.android.com/reference/android/accounts/AccountManager.html" target="_blank">AccountManager</a> 会回调 <span style="color: #ff00ff;">OnTokenAcquired</span> 类的 <a href="http://developer.android.com/reference/android/accounts/AccountManagerCallback.html#run(android.accounts.AccountManagerFuture&lt;V&gt;)">run() </a> 方法，并传递一个包含 <a href="http://developer.android.com/reference/android/os/Bundle.html">Bundle </a> 的参数 <a href="http://developer.android.com/reference/android/accounts/AccountManagerFuture.html">AccountManagerFuture</a> ，如果成功获取到令牌，那么该令牌信息就会保存在 <a href="http://developer.android.com/reference/android/os/Bundle.html">Bundle</a> 中。</p>
<p>示例代码展示了如何从 <a href="http://developer.android.com/reference/android/os/Bundle.html">Bundle</a> 中取得已申请的令牌：</p>
<pre>  private class OnTokenAcquired implements AccountManagerCallback&lt;Bundle&gt; {
  @Override
  public void run(AccountManagerFuture&lt;Bundle&gt; result) {
    // Get the result of the operation from the AccountManagerFuture.
    Bundle bundle = result.getResult();

    // The token is a named value in the bundle. The name of the value
    // is stored in the constant AccountManager.KEY_AUTHTOKEN.
    token = bundle.getString(AccountManager.KEY_AUTHTOKEN);
    ...
   }
  }</pre>
<p>如果一切进展顺利，那么  <a href="http://developer.android.com/reference/android/os/Bundle.html">Bundle</a> 中就包含了合法的令牌值，您可以通过键值 <a href="http://developer.android.com/reference/android/accounts/AccountManager.html#KEY_AUTHTOKEN">KEY_AUTHTOKEN</a> 直接拿到。不过，有时事情也没有那么顺利，那么需要我们多做如下工作。</p>
<h3>重新获得访问令牌 （Auth Token）</h3>
<p>第一次申请令牌失败的原因可能有很多，比如：</p>
<ul>
<li>设备本身的问题或者网络的问题导致调用  <a href="http://developer.android.com/reference/android/accounts/AccountManager.html">AccountManager</a> 失败</li>
<li>用户不同意授权您的应用程序访问其账户信息</li>
<li>设备保存的账户凭据（account credentials）信息不足的问题导致无法访问该账户</li>
<li>本地缓存的访问令牌过期了</li>
</ul>
<p>对于前两条错误，您可以通过简单地显示一条失败信息通知用户来优雅的解决。因为如果是网络掉线或者是用户不同意授权，您的应用程序也对此无能为力，所以也不必过多关注。对于后面的两种情况，处理的时候比较复杂，设计良好的应用程序应当能够自动处理此类错误。</p>
<p>第三种错误是缺少足够的账户凭据信息（insufficient credentials），当这种错误出现的时候，<a href="http://developer.android.com/reference/android/accounts/AccountManagerCallback.html">AccountManagerCallback</a> （前面示例代码中实现的 <span style="color: #ff00ff;">OnTokenAcquired</span>  类） 中的  <a href="http://developer.android.com/reference/android/os/Bundle.html">Bundle</a> 包含了 一个键值为 <a href="http://developer.android.com/reference/android/accounts/AccountManager.html#KEY_INTENT">KEY_INTENT</a> 的 <a href="http://developer.android.com/reference/android/content/Intent.html">Intent</a> ，您可以通过查询 <a href="http://developer.android.com/reference/android/os/Bundle.html">Bundle</a> 中是有没有包含此键值来判断是否出现了该错误，如果包含了，那么也就意味着 OAuth2 认证过程还需要用户的账户凭据信息（account credentials）才能继续接下来的令牌获取过程。</p>
<p>出现这种情况可能的原因有很多，或许是用户第一次登陆该账户，也可能是用户的账户登录超时了，需要重新登录， 也有可能是用户的账户凭据信息（account credentials）错误，又或者是用户账户启用了两因素认证（two-factor authentication）机制， 也有可能是需要启动照相机来进行视网膜扫描认证。但是不管是哪种具体类型的失败原因，如果您的应用程序需要获得一个合法的访问令牌，那就必须将该 <a href="http://developer.android.com/reference/android/content/Intent.html">Intent</a> 通知给系统，并且捕获它的返回结果。</p>
<p>示例代码：</p>
<pre>  private class OnTokenAcquired implements AccountManagerCallback&lt;Bundle&gt; {
    @Override
    public void run(AccountManagerFuture&lt;Bundle&gt; result) {
    ...
    Intent launch = (Intent) result.get(AccountManager.KEY_INTENT);

    if (launch != null) {
      startActivityForResult(launch, 0);
      return;
    }
   }
  }</pre>
<p>请注意在上例中使用的是 <a href="http://developer.android.com/reference/android/app/Activity.html#startActivityForResult(android.content.Intent, int)">startActivityForResult()</a>，那么您就可以在自己的 Activity 中通过实现 <a href="http://developer.android.com/reference/android/app/Activity.html#onActivityResult(int, int, android.content.Intent)">onActivityResult()</a> 方法来获得该 <a href="http://developer.android.com/reference/android/content/Intent.html">Intent</a> 的处理结果。 这一点非常重要，如果您不捕获该 <a href="http://developer.android.com/reference/android/content/Intent.html">Intent</a> 的处理结果，就不能知道用户是否进行正常认证了。 假如返回的结果为  <a href="http://developer.android.com/reference/android/app/Activity.html#RESULT_OK">RESULT_OK</a>， 那么账户凭据（account credentials）信息已经更新，现在有足够的凭据信息来进行接下来的获取访问令牌的过程了，您可以通过再次调用 <a href="http://developer.android.com/reference/android/accounts/AccountManager.html#getAuthToken(android.accounts.Account, java.lang.String, android.os.Bundle, android.app.Activity, android.accounts.AccountManagerCallback&lt;android.os.Bundle&gt;, android.os.Handler)">AccountManager.getAuthToken()</a> 方法来获取新的令牌。</p>
<p>对于最后一种情况令牌的过期，准确来说，这并不算是一种  <a href="http://developer.android.com/reference/android/accounts/AccountManager.html">AccountManager</a> 的错误。 出现此种错误的唯一地方是利用该令牌访问在线服务。 如果要通过利用 <a href="http://developer.android.com/reference/android/accounts/AccountManager.html">AccountManager</a> 来依次访问所有的在线服务，并循环检查所有已存储的令牌是否过期的方法是浪费时间的，代价也比较大。所以，好的解决方法是，当您的应用程序在访问在线服务的时候，捕获这个错误，如果发现令牌过期了，重新申请一个新的即可。 具体的申请流程可以参见上面讲解。</p>
<h3>4. 访问在线服务</h3>
<p>下面的示例展示了如何访问 google 在线服务。因为 Google 服务的认证是基于标准的 OAuth2 机制，所以这里讨论的技术是广泛适用的。请记住，虽然每个服务提供方可能各不相同，但只要您按照自己的具体情况略作修改即可。</p>
<p>利用 Google APIs 访问 Google 服务，每一次的Http请求，您都需要提供四个值，分别是 <span style="color: #ff00ff;">API key</span>, <span style="color: #ff00ff;">client ID</span>, <span style="color: #ff00ff;">client secret</span>, 和 <span style="color: #ff00ff;">auth key</span>. 前三个值可以从Google API Console 获得，最后一个值，即访问令牌，通过 <a href="http://developer.android.com/reference/android/accounts/AccountManager.html#getAuthToken(android.accounts.Account, java.lang.String, android.os.Bundle, android.app.Activity, android.accounts.AccountManagerCallback&lt;android.os.Bundle&gt;, android.os.Handler)">AccountManager.getAuthToken()</a> 获得。</p>
<p>You pass these to the Google Server as part of an HTTP request.</p>
<p>示例代码：</p>
<pre>  URL url = new URL("&lt;a href="https://www.googleapis.com/tasks/v1/users/@me/lists?key"&gt;https://www.googleapis.com/tasks/v1/users/@me/lists?key&lt;/a&gt;=" + your_api_key);   URLConnection conn = (HttpURLConnection) url.openConnection();   conn.addRequestProperty("client_id", your client id);   conn.addRequestProperty("client_secret", your client secret);   conn.setRequestProperty("Authorization", "OAuth " + token);</pre>
<p>如果上面的请求结果返回 401 错误，那么表明您的访问令牌（Token）是无效的。 像上面提到的第四种错误类型，很可能是令牌过期了。解决这个问题非常简单，首先调用<a href="http://developer.android.com/reference/android/accounts/AccountManager.html#invalidateAuthToken(java.lang.String, java.lang.String)">AccountManager.invalidateAuthToken()</a> 删除本地缓存的无效令牌，然后调用 <a href="http://developer.android.com/reference/android/accounts/AccountManager.html#getAuthToken(android.accounts.Account, java.lang.String, android.os.Bundle, android.app.Activity, android.accounts.AccountManagerCallback&lt;android.os.Bundle&gt;, android.os.Handler)">AccountManager.getAuthToken()</a> 获取一个新的令牌即可。</p>
<p>因为令牌过期是一个很普遍的情况，解决它也比较容易，很多应用程序只是在每次获取新令牌之前都先将本地令牌设置为过期并清除。对于服务提供商来说，如果申请一个令牌的操作代价比较小，那么您的应用程序可以在第一次调用获取令牌的方法 <a href="http://developer.android.com/reference/android/accounts/AccountManager.html#getAuthToken(android.accounts.Account, java.lang.String, android.os.Bundle, android.app.Activity, android.accounts.AccountManagerCallback&lt;android.os.Bundle&gt;, android.os.Handler)">AccountManager.getAuthToken()</a> 之前，也先调用清除无效令牌的方法 <a href="http://developer.android.com/reference/android/accounts/AccountManager.html#invalidateAuthToken(java.lang.String, java.lang.String)">AccountManager.invalidateAuthToken()</a> 。</p>
<p><strong>参考文摘：</strong><br />
<a href="http://developer.android.com/training/id-auth/authenticate.html">http://developer.android.com/training/id-auth/authenticate.html</a></p>
<p><a rel="nofollow" href="http://blog.zhourunsheng.com/2012/01/android-%e7%94%a8%e6%88%b7%e7%ae%a1%e7%90%86%e4%b8%93%e9%a2%98%e4%b9%8b%e5%9f%ba%e4%ba%8eoauth2%e5%8d%8f%e8%ae%ae%e7%9a%84%e6%9c%8d%e5%8a%a1%e8%ae%a4%e8%af%81/">Android 用户管理专题之基于OAuth2协议的服务认证</a>，首发于<a rel="nofollow" href="http://blog.zhourunsheng.com">润物无声</a>。</p>
]]></content:encoded>
			<wfw:commentRss>http://blog.zhourunsheng.com/2012/01/android-%e7%94%a8%e6%88%b7%e7%ae%a1%e7%90%86%e4%b8%93%e9%a2%98%e4%b9%8b%e5%9f%ba%e4%ba%8eoauth2%e5%8d%8f%e8%ae%ae%e7%9a%84%e6%9c%8d%e5%8a%a1%e8%ae%a4%e8%af%81/feed/</wfw:commentRss>
		<slash:comments>0</slash:comments>
		</item>
		<item>
		<title>Android 用户管理专题之记录用户身份</title>
		<link>http://blog.zhourunsheng.com/2012/01/android-%e7%94%a8%e6%88%b7%e7%ae%a1%e7%90%86%e4%b8%93%e9%a2%98%e4%b9%8b%e8%ae%b0%e5%bd%95%e7%94%a8%e6%88%b7%e8%ba%ab%e4%bb%bd/</link>
		<comments>http://blog.zhourunsheng.com/2012/01/android-%e7%94%a8%e6%88%b7%e7%ae%a1%e7%90%86%e4%b8%93%e9%a2%98%e4%b9%8b%e8%ae%b0%e5%bd%95%e7%94%a8%e6%88%b7%e8%ba%ab%e4%bb%bd/#comments</comments>
		<pubDate>Sun, 01 Jan 2012 02:56:03 +0000</pubDate>
		<dc:creator><![CDATA[润物无声]]></dc:creator>
				<category><![CDATA[移动开发]]></category>
		<category><![CDATA[Android]]></category>
		<category><![CDATA[Training]]></category>

		<guid isPermaLink="false">http://blog.zhourunsheng.com/?p=1215</guid>
		<description><![CDATA[<p>每个人都喜欢自己的名字被他人记住，同样地，如果想让您的应用程序更人性化，为不同的用户提供不同的用户体验，以及自 [&#8230;]</p>
<p><a rel="nofollow" href="http://blog.zhourunsheng.com/2012/01/android-%e7%94%a8%e6%88%b7%e7%ae%a1%e7%90%86%e4%b8%93%e9%a2%98%e4%b9%8b%e8%ae%b0%e5%bd%95%e7%94%a8%e6%88%b7%e8%ba%ab%e4%bb%bd/">Android 用户管理专题之记录用户身份</a>，首发于<a rel="nofollow" href="http://blog.zhourunsheng.com">润物无声</a>。</p>
]]></description>
				<content:encoded><![CDATA[<p><img class="alignleft" title="training-prof" src="http://blog.zhourunsheng.com/wp-content/uploads/2011/12/training-prof-150x150.png" alt="" width="150" height="150" /><br />
每个人都喜欢自己的名字被他人记住，同样地，如果想让您的应用程序更人性化，为不同的用户提供不同的用户体验，以及自定义配置，那么就需要记录用户的个人信息。当用户从旧的设备更新到新的设备，或者从一台设备切换到另一台设备上面（例如用户拥有一台平板电脑和手机，并且经常在两者之间切换），您的应用应当可以准确识别出用户，那么如何才能区分用户和记录用户信息，并且准确地鉴别用户身份，本文的目的就是要解决上面提到的问题。</p>
<p>对于大多数应用来说， 账户管理器 <a href="http://developer.android.com/reference/android/accounts/AccountManager.html" target="_blank">AccountManager </a>就足够了，利用其提供的API，只要获得用户的权限许可，就可以很容易地取得当前设备上已经存储的用户账户名信息。</p>
<p>集成用户帐号的管理，可以让您的应用程序完成如下的功能：</p>
<ol>
<li>表格的自动填写（如用户的 Email 地址信息）</li>
<li>记录和获得与用户身份绑定的ID，而不仅仅是设备的ID</li>
</ol>
<p><span id="more-1215"></span></p>
<h3>账户管理器 AccountManager 是否适合您的应用程序</h3>
<p>应用程序通常使用如下的3种技术来进行账户的管理：</p>
<ol>
<li>要求用户填写账户名称</li>
<li>识别当前应用程序运行的设备信息，记录设备的ID</li>
<li>通过系统提供的 <a href="http://developer.android.com/reference/android/accounts/AccountManager.html" target="_blank">AccountManager  </a>来获取当前设备已记录的用户信息</li>
</ol>
<p>方案（1）的缺点如下：首先，每次启动应用程序之前都需要输入用户名的信息，程序显得不太友好，吸引力不足，其次，没有好的办法来保证用户名的唯一性。</p>
<p>方案（2）较方案（1）来说为用户减轻了一点负担，但是它很难保证用户的准确性。因为您的应用程序仅仅是与具体的设备绑定在了一起，想象一下，用户从一台旧设备升级到了新设备，在新设备上面运行应用的时候，您的应用程序就不再记得该用户了。</p>
<p>方案（3）是首选的解决方案，通过利用系统提供的 AccountManager，您可以获取当前设备上面已经存储的用户身份信息，不论用户拥有多少台设备，只要用户登录了该设备，那么您就可以准确地识别该用户。对于您应用程序的设计来说，也仅仅是加入几个多余的 UI 界面而已。</p>
<h3> 设计不同的账户类型</h3>
<p>Android 设备可以存储不同类型的账户信息，当您通过 <a href="http://developer.android.com/reference/android/accounts/AccountManager.html" target="_blank">AccountManager </a>来进行账户查询的时候，通常需要利用账户类型来进行过滤，账户类型（<span style="color: #ff00ff;">account type</span>）是一个字符串，要求唯一性，用来和系统中已存储的其他类型的账户进行区分，例如，Google 的账户类型是 "com.google",Twitter 的账户类型是 "com.twitter.android.auth.login"，当然可以利用自己的个人域名来进行区分，例如 "com.zhourunsheng.com"。</p>
<h3>申请获得用户账户信息的权限 GET_ACCOUNT</h3>
<p>如果要读取当前设备上面的账户信息，您的应用需要 <a href="http://developer.android.com/reference/android/Manifest.permission.html#GET_ACCOUNTS" target="_blank">GET_ACCOUNTS  </a>权限，在 manifest 文件的<a href="http://developer.android.com/guide/topics/manifest/uses-permission-element.html" target="_blank"> &lt;uses-permission&gt;</a>  tag 中添加</p>
<p>示例代码</p>
<pre>&lt;manifest ... &gt;
&lt;uses-permission android:name="android.permission.GET_ACCOUNTS" /&gt;
...
&lt;/manifest&gt;</pre>
<h3> 用户帐号查询</h3>
<p>一旦确定了账户的类型，您就可以通过 <a href="http://developer.android.com/reference/android/accounts/AccountManager.html" target="_blank">AccountManager  </a>来进行查询了。首先通过调用<a href="http://developer.android.com/reference/android/accounts/AccountManager.html#get(android.content.Context)" target="_blank"> AccountManager.get() </a> 来获得一个 AccountManager 实例对象，然后调用它的成员函数 <a href="http://developer.android.com/reference/android/accounts/AccountManager.html#getAccountsByType(java.lang.String)" target="_blank">getAccountsByType() </a>，通过传递账户类型的参数，就能得到该账户类型下面的所有用户的一个用户列表。</p>
<p>示例代码</p>
<pre>AccountManager am = AccountManager.get(this); // "this" references the current Context

Account[] accounts = am.getAccountsByType("com.google");</pre>
<p>函数调用返回一个包含所有帐号 <a href="http://developer.android.com/reference/android/accounts/Account.html" target="_blank">Account </a>的数组。<br />
假如查询结果多于一个用户，那么在您的应用中应当弹出一个对话框来让用户选择其中一个账户为该应用的当前账户。</p>
<h3>利用用户帐号个性化您的应用程序</h3>
<p>用户帐号 <a href="http://developer.android.com/reference/android/accounts/Account.html" target="_blank">Account  </a>中包含了用户名信息，Google 的账户存储的是用户的 Email 地址，您可以使用这些信息来扩展您的应用,比如：</p>
<ol>
<li>表格自动填写建议和自动填写，这样用户就不需要手动输入账户信息了。</li>
<li>作为您应用程序在线数据库的一个键值，以此来保存和区分不同用户的自定义配置信息。</li>
</ol>
<h3>是否需要账户的验证机制来处理私有数据</h3>
<p>通过用户帐号 <a href="http://developer.android.com/reference/android/accounts/Account.html" target="_blank">Account </a>可以很方便的区分用户，但是 Account 本身并不提供任何数据的保护机制，如果您的应用中需要用户访问在线数据库的私有数据，那么需要另一种访问控制方式： <span style="color: #ff00ff;">authentication</span> （验证），这会在下一节课中讲到，通过编写一个自定义的验证机制来访问您应用程序的在线私有数据库。</p>
<p><strong>参考文摘：</strong><br />
<a href="http://developer.android.com/training/id-auth/identify.html" target="_blank"> http://developer.android.com/training/id-auth/identify.html</a></p>
<p><a rel="nofollow" href="http://blog.zhourunsheng.com/2012/01/android-%e7%94%a8%e6%88%b7%e7%ae%a1%e7%90%86%e4%b8%93%e9%a2%98%e4%b9%8b%e8%ae%b0%e5%bd%95%e7%94%a8%e6%88%b7%e8%ba%ab%e4%bb%bd/">Android 用户管理专题之记录用户身份</a>，首发于<a rel="nofollow" href="http://blog.zhourunsheng.com">润物无声</a>。</p>
]]></content:encoded>
			<wfw:commentRss>http://blog.zhourunsheng.com/2012/01/android-%e7%94%a8%e6%88%b7%e7%ae%a1%e7%90%86%e4%b8%93%e9%a2%98%e4%b9%8b%e8%ae%b0%e5%bd%95%e7%94%a8%e6%88%b7%e8%ba%ab%e4%bb%bd/feed/</wfw:commentRss>
		<slash:comments>1</slash:comments>
		</item>
	</channel>
</rss>
