注:本文翻译自Google官方的Android Developers Training文档,译者技术一般,由于喜爱安卓而产生了翻译的念头,纯属个人兴趣爱好。
原文链接: http://developer.android.com/training/basics/network-ops/managing.html
这节课将讲解如何写一个对使用网络资源具有细粒度控制的应用。如果你的应用要执行很多网络操作,你需要提供用户设置,使得用户可以控制你的应用处理数据的行为,比如你的应用同步数据的频率,是仅在有Wi-Fi连接的情况下上传/下载,在漫游时是否处理数据等。当用户可以进行这些设置时,用户就不太会阻止你的应用访问后台数据了,因为他们能精确地控制应用能处理的数据范围。
有关一些通用的关于如何编写最小化影响电池寿命的应用程序,相关的引导可以阅读:
Optimizing Battery Life
和
Transferring Data Without Draining the Battery
。
一). 检查设备的网络连接
一个设备可以有许多种网络连接。这节课关注于使用Wi-Fi连接或者移动网络连接。完整的连接类型列表,可以阅读: ConnectivityManager 。
一般而言,Wi-Fi速度更快。另外移动数量一般按照流量计费,所以会很昂贵。通常的策略是只在有Wi-Fi连接的情况下获取大数据。
在你执行网络操作之前,检查网络的连接状态时一个好的习惯。这可以防止你的应用错误地使用网络连接方式。如果网络连接无法获取,你的应用应该恰当地做出响应。要检查网络连接状态,通常你会使用下列类:
- ConnectivityManager - 响应网络连接状态的查询。它也在网络连接放生改变时,通知应用。
- NetworkInfo - 对给定的类型(移动数据或者Wi-Fi)描绘网络接口的状态。
这个代码检测Wi-Fi和移动数据的网络连接。它确定这些网络接口是否可用(网络连接是否可以获取)或者是否已连接(也就是说,网络连接是否存在或者是否可能建立套接字并传输数据):
private
static
final
String DEBUG_TAG = "NetworkStatusExample"
;
...
ConnectivityManager connMgr
=
(ConnectivityManager)
getSystemService(Context.CONNECTIVITY_SERVICE);
NetworkInfo networkInfo
=
connMgr.getNetworkInfo(ConnectivityManager.TYPE_WIFI);
boolean
isWifiConn =
networkInfo.isConnected();
networkInfo
=
connMgr.getNetworkInfo(ConnectivityManager.TYPE_MOBILE);
boolean
isMobileConn =
networkInfo.isConnected();
Log.d(DEBUG_TAG,
"Wifi connected: " +
isWifiConn);
Log.d(DEBUG_TAG,
"Mobile connected: " + isMobileConn);
注意,在做决策时不可以仅根据网络是否可获得来进行。应该在每次进行网络操作之间检查
isConnected()
,因为
isConnected()
能够处理诸如片状移动网络(flaky mobile networks),飞行模式,以及后台限制的数据。
一个更简洁的方法是检查是否有一个可获得的网络连接接口,如下所示。 getActiveNetworkInfo() 方法返回一个 NetworkInfo 实例,它代表它能发现的第一个已连接的网络接口,或者在没有已连接的网络接口时返回 null (意味着无法获取一个网络连接):
public
boolean
isOnline() {
ConnectivityManager connMgr
=
(ConnectivityManager)
getSystemService(Context.CONNECTIVITY_SERVICE);
NetworkInfo networkInfo
=
connMgr.getActiveNetworkInfo();
return
(networkInfo !=
null
&&
networkInfo.isConnected());
}
要查询更详细的连接状态,你可以使用 NetworkInfo.DetailedState ,但一般来说这是不必要的。
二). 管理网络的使用
你可以实现一个用以对应用进行设置的activity,使得用户可以对应用对网络资源的使用进行控制。例如:
- 你可以只允许用户在连接了Wi-Fi的情况下上传视频;
- 你可以根据多个标准,如网络可获得否,时间间隔,等等,进行同步(或不进行)。
要写一个支持网络访问和管理网络使用的应用,你的应用需要有相应的权限和intent过滤器。
-
下面摘录的的配置清单包含下列权限:
- android.permission.INTERNET - 允许应用打开网络套接字。
- android.permission.ACCESS_NETWORK_STATE - 允许应用获取有关网络的信息。
- 你可以声明针对于 ACTION_MANAGE_NETWORK_USAGE (该action自Android 4.0开始引入)的intent过滤器,它用来表明你的应用定义了一个可以对数据使用进行控制的activity。 ACTION_MANAGE_NETWORK_USAGE 展示了对某个特定应用的数据使用管理的设置。当你的应用有允许用户设置网络使用的activity时,你应该为该activity配置此intent过滤器。在样例代码中,该action由 SettingsActivity 捕获并处理,它显示一个配置界面,让用户来选择何时下载:
<?
xml version="1.0" encoding="utf-8"
?>
<
manifest
xmlns:android
="http://schemas.android.com/apk/res/android"
package
="com.example.android.networkusage"
...
>
<
uses-sdk
android:minSdkVersion
="4"
android:targetSdkVersion
="14"
/>
<
uses-permission
android:name
="android.permission.INTERNET"
/>
<
uses-permission
android:name
="android.permission.ACCESS_NETWORK_STATE"
/>
<
application
...
>
...
<
activity
android:label
="SettingsActivity"
android:name
=".SettingsActivity"
>
<
intent-filter
>
<
action
android:name
="android.intent.action.MANAGE_NETWORK_USAGE"
/>
<
category
android:name
="android.intent.category.DEFAULT"
/>
</
intent-filter
>
</
activity
>
</
application
>
</
manifest
>
三). 实现一个配置Activity
如同你所看到的上述清单文件,样例中的activity: SettingsActivity有一个针对于 ACTION_MANAGE_NETWORK_USAGE 的intent过滤器。 SettingsActivity是 PreferenceActivity 的一个子类。它显示一个配置界面(如图1所示)让用户可以进行下列控制:
- 是否显示每一个XML源条目的摘要,或仅对每个条目显示其链接;
- 是否在任何情况下下载XML源,或只在连接了Wi-Fi的情况下。
图1. 配置activity
下面的代码是 SettingsActivity ,注意它实现了 OnSharedPreferenceChangeListener 。当用户进行了设置的变更时,会激活 onSharedPreferenceChanged() ,在该方法中会把 refreshDisplay 变为 true ,这会导致当用户回到主activity时发生显示刷新:
public
class
SettingsActivity
extends
PreferenceActivity
implements
OnSharedPreferenceChangeListener {
@Override
protected
void
onCreate(Bundle savedInstanceState) {
super
.onCreate(savedInstanceState);
//
Loads the XML preferences file
addPreferencesFromResource(R.xml.preferences);
}
@Override
protected
void
onResume() {
super
.onResume();
//
Registers a listener whenever a key changes
getPreferenceScreen().getSharedPreferences().registerOnSharedPreferenceChangeListener(
this
);
}
@Override
protected
void
onPause() {
super
.onPause();
//
Unregisters the listener set in onResume().
//
It's best practice to unregister listeners when your app isn't using them to cut down on
//
unnecessary system overhead. You do this in onPause().
getPreferenceScreen().getSharedPreferences().unregisterOnSharedPreferenceChangeListener(
this
);
}
//
When the user changes the preferences selection,
//
onSharedPreferenceChanged() restarts the main activity as a new
//
task. Sets the refreshDisplay flag to "true" to indicate that
//
the main activity should update its display.
//
The main activity queries the PreferenceManager to get the latest settings.
@Override
public
void
onSharedPreferenceChanged(SharedPreferences sharedPreferences, String key) {
//
Sets refreshDisplay to true so that when the user returns to the main
//
activity, the display refreshes to reflect the new settings.
NetworkActivity.refreshDisplay =
true
;
}
}
四). 响应配置变更
当用户在配置界面改变了设置,它会改变应用的行为。在下面的代码片段中,应用在
onStart()
方法内检查各项设置。如果在设置和设备网络连接之间能够匹配的上(例如,设置是“Wi-Fi”且应用具有一个可使用的Wi-Fi连接),那么应用会下载数据源并刷新页面:
public
class
NetworkActivity
extends
Activity {
public
static
final
String WIFI = "Wi-Fi"
;
public
static
final
String ANY = "Any"
;
private
static
final
String URL = "http://stackoverflow.com/feeds/tag?tagnames=android&sort=newest"
;
//
Whether there is a Wi-Fi connection.
private
static
boolean
wifiConnected =
false
;
//
Whether there is a mobile connection.
private
static
boolean
mobileConnected =
false
;
//
Whether the display should be refreshed.
public
static
boolean
refreshDisplay =
true
;
//
The user's current network preference setting.
public
static
String sPref =
null
;
//
The BroadcastReceiver that tracks network connectivity changes.
private
NetworkReceiver receiver =
new
NetworkReceiver();
@Override
public
void
onCreate(Bundle savedInstanceState) {
super
.onCreate(savedInstanceState);
//
Registers BroadcastReceiver to track network connection changes.
IntentFilter filter =
new
IntentFilter(ConnectivityManager.CONNECTIVITY_ACTION);
receiver
=
new
NetworkReceiver();
this
.registerReceiver(receiver, filter);
}
@Override
public
void
onDestroy() {
super
.onDestroy();
//
Unregisters BroadcastReceiver when app is destroyed.
if
(receiver !=
null
) {
this
.unregisterReceiver(receiver);
}
}
//
Refreshes the display if the network connection and the
//
pref settings allow it.
@Override
public
void
onStart () {
super
.onStart();
//
Gets the user's network preference settings
SharedPreferences sharedPrefs = PreferenceManager.getDefaultSharedPreferences(
this
);
//
Retrieves a string value for the preferences. The second parameter
//
is the default value to use if a preference value is not found.
sPref = sharedPrefs.getString("listPref", "Wi-Fi"
);
updateConnectedFlags();
if
(refreshDisplay){
loadPage();
}
}
//
Checks the network connection and sets the wifiConnected and mobileConnected
//
variables accordingly.
public
void
updateConnectedFlags() {
ConnectivityManager connMgr
=
(ConnectivityManager)
getSystemService(Context.CONNECTIVITY_SERVICE);
NetworkInfo activeInfo
=
connMgr.getActiveNetworkInfo();
if
(activeInfo !=
null
&&
activeInfo.isConnected()) {
wifiConnected
= activeInfo.getType() ==
ConnectivityManager.TYPE_WIFI;
mobileConnected
= activeInfo.getType() ==
ConnectivityManager.TYPE_MOBILE;
}
else
{
wifiConnected
=
false
;
mobileConnected
=
false
;
}
}
//
Uses AsyncTask subclass to download the XML feed from stackoverflow.com.
public
void
loadPage() {
if
(((sPref.equals(ANY)) && (wifiConnected ||
mobileConnected))
|| ((sPref.equals(WIFI)) &&
(wifiConnected))) {
//
AsyncTask subclass
new
DownloadXmlTask().execute(URL);
}
else
{
showErrorPage();
}
}
...
}
五). 检测连接变更
最后一部分内容是关于 NetworkReceiver , BroadcastReceiver 的子类。当设备的网络连接发生了变化, NetworkReceiver 会拦截 CONNECTIVITY_ACTION ,确定当前的连接状态时什么,并相应地将 wifiConnected 和 mobileConnected 设置为true/false。产生的结果是,下一次用户回到应用时,应用会仅下载最后一个源并在 NetworkActivity.refreshDisplay 为true的情况下刷新页面。
配置一个会被不必要地调用的
BroadcastReceiver会消耗系统资源。样例中,在
onCreate()
方法中注册了
BroadcastReceiver
NetworkReceiver,并在
onDestroy()
中销毁它。一种更轻量级的解决方法是在清单文件中声明一个
<receiver>
。当你这样做了之后,它会在任何时间唤起你的应用,甚至是你已经好几周没有运行该应用了。如果在主
activity
中注册或注销
NetworkReceiver,
用户在离开应用后就不会再被唤起了。如果你在清单文件中声明了一个
<receiver>,并且你明确的知道你在什么地方会需要它,那么你可以使用
setComponentEnabledSetting()
在恰当地时机启用或禁用它。
下面是 NetworkReceiver:
public
class
NetworkReceiver
extends
BroadcastReceiver {
@Override
public
void
onReceive(Context context, Intent intent) {
ConnectivityManager conn
=
(ConnectivityManager)
context.getSystemService(Context.CONNECTIVITY_SERVICE);
NetworkInfo networkInfo
=
conn.getActiveNetworkInfo();
//
Checks the user prefs and the network connection. Based on the result, decides whether
//
to refresh the display or keep the current display.
//
If the userpref is Wi-Fi only, checks to see if the device has a Wi-Fi connection.
if
(WIFI.equals(sPref) && networkInfo !=
null
&& networkInfo.getType() ==
ConnectivityManager.TYPE_WIFI) {
//
If device has its Wi-Fi connection, sets refreshDisplay
//
to true. This causes the display to be refreshed when the user
//
returns to the app.
refreshDisplay =
true
;
Toast.makeText(context, R.string.wifi_connected, Toast.LENGTH_SHORT).show();
//
If the setting is ANY network and there is a network connection
//
(which by process of elimination would be mobile), sets refreshDisplay to true.
}
else
if
(ANY.equals(sPref) && networkInfo !=
null
) {
refreshDisplay
=
true
;
//
Otherwise, the app can't download content--either because there is no network
//
connection (mobile or Wi-Fi), or because the pref setting is WIFI, and there
//
is no Wi-Fi connection.
//
Sets refreshDisplay to false.
}
else
{
refreshDisplay
=
false
;
Toast.makeText(context, R.string.lost_connection, Toast.LENGTH_SHORT).show();
}
}

