DLNA,Digital Living Network Alliance的简称,即数字生活网络联盟。其由消费性电子、移动电话以及电脑厂商组成。目标在于创建一套可以使得各厂商的产品互相连接,互相适应的工业标准,从而为消费者实现数字化生活。
UPnP/DLNA library for Java and Android。
GitHub最多关注,当前仍在维护,许可协议为LGPL或CDDL。
以下为中文译文:
## 5. Android 上的Cling
1.
public
interface
AndroidUpnpService {
2.
public
UpnpService get();
3.
public
UpnpServiceConfiguration getConfiguration();
4.
public
Registry getRegistry();
5.
public
ControlPoint getControlPoint();
6.
}
activity通常访问已知UPnP设备的注册表,或者通过ControlPoint查询和控制UPnP设备。
你必须在AndroidManifest.xml内配置内建的服务实现:
01.
<
manifest
...>
02.
03.
<
uses-permission
android:name
=
"android.permission.INTERNET"
/>
04.
<
uses-permission
android:name
=
"android.permission.ACCESS_WIFI_STATE"
/>
05.
<
uses-permission
android:name
=
"android.permission.CHANGE_WIFI_MULTICAST_STATE"
/>
06.
<
uses-permission
android:name
=
"android.permission.ACCESS_NETWORK_STATE"
/>
07.
08.
<
application
...>
09.
10.
<
activity
...>
11.
...
12.
</
activity
>
13.
14.
<
service
android:name
=
"org.teleal.cling.android.AndroidUpnpServiceImpl"
/>
15.
16.
</
application
>
17.
18.
</
manifest
>
此Cling UPnP服务要求设备WiFi接口的访问权限,事实上其也只将会绑定网络接口。
此服务将会自动检测WiFi接口的关闭,并优雅地处理这种情形:任何客户端操作都会导致"no response from server"状态,而你的代码必须预料并处理该状态。
当服务组件创建或销毁时,会相应开启和关闭UPnP系统。这依赖于在你的activities里是如何访问此组件的。
### 5.2 activity如何访问服务
service的生命周期在Android中很好的被定义了。如果service还没启动的话,第一个绑定服务的activity将会启动它。当不再有activity绑定到service上时,操作系统将会销毁此service。
让我们写一个简单的UPnP浏览activity。它用于将所有你网络内的设备显示在一个列表内,并有一个菜单选项来触发搜寻。activity连接UPnP服务之后,会一直监听注册表内设备的增加和删除,所以显示的设备列表会实时更新。
以下是activity类的骨架:
01.
import
android.app.ListActivity;
02.
import
android.content.ComponentName;
03.
import
android.content.Context;
04.
import
android.content.Intent;
05.
import
android.content.ServiceConnection;
06.
import
android.os.Bundle;
07.
import
android.os.IBinder;
08.
import
android.view.Menu;
09.
import
android.view.MenuItem;
10.
import
android.widget.ArrayAdapter;
11.
import
android.widget.Toast;
12.
import
org.teleal.cling.android.AndroidUpnpService;
13.
import
org.teleal.cling.android.AndroidUpnpServiceImpl;
14.
import
org.teleal.cling.model.meta.Device;
15.
import
org.teleal.cling.model.meta.LocalDevice;
16.
import
org.teleal.cling.model.meta.RemoteDevice;
17.
import
org.teleal.cling.registry.DefaultRegistryListener;
18.
import
org.teleal.cling.registry.Registry;
19.
20.
public
class
UpnpBrowser
extends
ListActivity {
21.
22.
private
ArrayAdapter<DeviceDisplay> listAdapter;
23.
24.
private
AndroidUpnpService upnpService;
25.
26.
private
ServiceConnection serviceConnection = ...
27.
28.
private
RegistryListener registryListener =
new
BrowseRegistryListener();
29.
30.
@Override
31.
public
void
onCreate(Bundle savedInstanceState) {
32.
super
.onCreate(savedInstanceState);
33.
34.
listAdapter =
35.
new
ArrayAdapter(
36.
this
,
37.
android.R.layout.simple_list_item_1
38.
);
39.
setListAdapter(listAdapter);
40.
41.
getApplicationContext().bindService(
42.
new
Intent(
this
, AndroidUpnpServiceImpl.
class
),
43.
serviceConnection,
44.
Context.BIND_AUTO_CREATE
45.
);
46.
}
47.
48.
@Override
49.
protected
void
onDestroy() {
50.
super
.onDestroy();
51.
if
(upnpService !=
null
) {
52.
upnpService.getRegistry().removeListener(registryListener);
53.
}
54.
getApplicationContext().unbindService(serviceConnection);
55.
}
56.
57.
...
58.
59.
}
60.
```
我们采用Android运行时默认提供的布局和ListActivity父类。注意这个类可以是你应用的主activity,或者进一步上升进任务的堆栈。listAdapter黏合了Cling Registry上设备的增加移除事件与展示在用户界面的列表项目。
当没有后台服务绑定到该activity时,upnpService变量为null。绑定和解绑发生在onCreate()和onDestroy()回调,所以activity绑定服务和它的生存周期一样长。
```
暂停后台的UPnP服务
当一个activity不再活动时(停止或暂停状态),它仍会绑定着UPnP服务。UPnP服务将会持续运行,即使应用不再可见。由于UPnP服务的注册表一定会定期维护发现的设备、刷新本地设备的通告、删除过期的GENA事件订阅等,将会消耗你设备的CPU和电量。当activity onPause()或onStop()方法被调用时,你可以调用Registry#pause()来通知UPnP服务不再维护注册表。之后,你可以通过Registry#resume()来恢复后台服务,或同时用Registry#isPaused()检查状态。请阅读这些方法的Javadoc了解详细信息,以及暂停注册表维护对于设备、服务和GENA订阅的意义。
```
以下是使用ServiceConnection处理绑定和解绑服务:
01.
private
ServiceConnection serviceConnection =
new
ServiceConnection() {
02.
03.
public
void
onServiceConnected(ComponentName className, IBinder service) {
04.
upnpService = (AndroidUpnpService) service;
05.
06.
// Refresh the list with all known devices
07.
listAdapter.clear();
08.
for
(Device device : upnpService.getRegistry().getDevices()) {
09.
registryListener.deviceAdded(device);
10.
}
11.
12.
// Getting ready for future device advertisements
13.
upnpService.getRegistry().addListener(registryListener);
14.
15.
// Search asynchronously for all devices
16.
upnpService.getControlPoint().search();
17.
}
18.
19.
public
void
onServiceDisconnected(ComponentName className) {
20.
upnpService =
null
;
21.
}
22.
};
23.
```
首先,所有已知的UPnP设备能够被查询和显示(如果UPnP服务刚开启且到到目前还没有设备通告它的存在)。
然后,给UPnP服务的注册表增加一个监听者。该监听者将会处理在你网络上发现的设备的增加和移除,并在更新在用户界面列表内显示的项目。当activity销毁时,BrowseRegistryListener会被移除。
最后,通过发送一个搜寻消息给所有UPnP设备,你会开启异步搜索,此时这些设备将通告它们的存在。注意这个搜寻消息不是每次连接服务都需要的。这只需一次,在当主activity和应用启动时,其会将已知设备写入注册表。
以下是BrowseRegistryListener,他的任务就是更新列表项的显示:
01.
class
BrowseRegistryListener
extends
DefaultRegistryListener {
02.
03.
@Override
04.
public
void
remoteDeviceDiscoveryStarted(Registry registry, RemoteDevice device) {
05.
deviceAdded(device);
06.
}
07.
08.
@Override
09.
public
void
remoteDeviceDiscoveryFailed(Registry registry,
final
RemoteDevice device,
final
Exception ex) {
10.
runOnUiThread(
new
Runnable() {
11.
public
void
run() {
12.
Toast.makeText(
13.
BrowseActivity.
this
,
14.
"Discovery failed of '"
+ device.getDisplayString() +
"': "
+
15.
(ex !=
null
? ex.toString() :
"Couldn't retrieve device/service descriptors"
),
16.
Toast.LENGTH_LONG
17.
).show();
18.
}
19.
});
20.
deviceRemoved(device);
21.
}
22.
23.
@Override
24.
public
void
remoteDeviceAdded(Registry registry, RemoteDevice device) {
25.
deviceAdded(device);
26.
}
27.
28.
@Override
29.
public
void
remoteDeviceRemoved(Registry registry, RemoteDevice device) {
30.
deviceRemoved(device);
31.
}
32.
33.
@Override
34.
public
void
localDeviceAdded(Registry registry, LocalDevice device) {
35.
deviceAdded(device);
36.
}
37.
38.
@Override
39.
public
void
localDeviceRemoved(Registry registry, LocalDevice device) {
40.
deviceRemoved(device);
41.
}
42.
43.
public
void
deviceAdded(
final
Device device) {
44.
runOnUiThread(
new
Runnable() {
45.
public
void
run() {
46.
DeviceDisplay d =
new
DeviceDisplay(device);
47.
int
position = listAdapter.getPosition(d);
48.
if
(position >=
0
) {
49.
// Device already in the list, re-set new value at same position
50.
listAdapter.remove(d);
51.
listAdapter.insert(d, position);
52.
}
else
{
53.
listAdapter.add(d);
54.
}
55.
}
56.
});
57.
}
58.
59.
public
void
deviceRemoved(
final
Device device) {
60.
runOnUiThread(
new
Runnable() {
61.
public
void
run() {
62.
listAdapter.remove(
new
DeviceDisplay(device));
63.
}
64.
});
65.
}
66.
}
67.
```
鉴于性能的原因,当发现设备时,我们会直到一个完整的hydrated(所有设备被检索和验证)设备元数据模型可用时才执行等待。我们响应尽可能得快,同时只当remoteDeviceAdded()方法被调用时才去等待。甚至当搜索仍在运行时,我们仍旧显示所有设备。在台式电脑上你通常不需要关心这个,不过,Android手持设备效率慢,并且UPnP使用好些臃肿的XML描述符来交换关于设备和服务的元数据。有时,在设备和它的服务完全可用前,这可能会花费数秒钟。而remoteDeviceDiscoveryStarted()和remoteDeviceDiscoveryFailed()方法在搜索处理时会尽快被调用。顺便说一句,如果设备有相同的UDN就表示相等的(a.equal(b)),但它们可能不会完全一致(a==b)。
注意注册表将会在分开的线程中调用监听者方法。你必须在UI线程中更新显示列表数据。
activity中以下两个方法增加了用来搜寻的菜单,如此用户才能手动的刷新列表:
01.
```
02.
@Override
03.
public
boolean
onCreateOptionsMenu(Menu menu) {
04.
menu.add(
0
,
0
,
0
, R.string.search_lan)
05.
.setIcon(android.R.drawable.ic_menu_search);
06.
return
true
;
07.
}
08.
09.
@Override
10.
public
boolean
onOptionsItemSelected(MenuItem item) {
11.
if
(item.getItemId() ==
0
&& upnpService !=
null
) {
12.
upnpService.getRegistry().removeAllRemoteDevices();
13.
upnpService.getControlPoint().search();
14.
}
15.
return
false
;
16.
}
17.
```
最后,DeviceDisplay类是一个非常简单的JavaBean,只提供一个toString()方法来呈现列表信息。通过修改此方法,你能够显示任何关于UPnP设备的信息:
01.
class
DeviceDisplay {
02.
Device device;
03.
04.
public
DeviceDisplay(Device device) {
05.
this
.device = device;
06.
}
07.
08.
public
Device getDevice() {
09.
return
device;
10.
}
11.
12.
@Override
13.
public
boolean
equals(Object o) {
14.
if
(
this
== o)
return
true
;
15.
if
(o ==
null
|| getClass() != o.getClass())
return
false
;
16.
DeviceDisplay that = (DeviceDisplay) o;
17.
return
device.equals(that.device);
18.
}
19.
20.
@Override
21.
public
int
hashCode() {
22.
return
device.hashCode();
23.
}
24.
25.
@Override
26.
public
String toString() {
27.
// Display a little star while the device is being loaded
28.
return
device.isFullyHydrated() ? device.getDisplayString() : device.getDisplayString() +
" *"
;
29.
}
30.
}
31.
```
还有我们必须覆盖相等操作,这样我们才可以用DeviceDisplay实例作为便捷的处理,从列表中手动地移除和增加设备。
### 5.3. 优化服务行为
UPnP服务运行时会消耗内存和CPU。尽管通常在一个正常的机器上没有什么问题,但在Android手持设备上就可能会有了。如果你禁用Cling UPnP服务的某些功能,或者设置暂停且在合适时恢复它,你可以留有更多的内存和电量。
#### 5.3.1. 调整注册表维护
当服务运行时,后台有好些东西在执行。首先,有一个服务的注册表和其维护线程。如果你写一个控制端,后台注册表维护者将会定期从远程服务更新你对外的GENA订阅。当没有通知断开网络时,它也会到期并移除任何远程服务。如果你正提供服务,你的设备通告将被注册表维护者刷新,并在GENA订阅没及时更新时移除它。注册表维护者为了有效得防止UPnP网络上的过时状态,所以所有参与者会实时更新其他参与者的视图等等。
默认情况下,注册表维护者会每秒运行并检查是否有事要做(当然,大多数情况下没事做)。然而默认的Android配置有5秒的间隔休眠,所以这已经花费了更少的后台CPU占用时间 — 不过你的应用可能会暴露稍微过时的信息。在UpnpServiceConfiguration你可以通过覆盖getRegistryMaintenanceIntervalMillis()进一步的调整设置。在Android上,你必须子类化服务实现来提供一个新的配置。
01.
```
02.
public
class
MyUpnpService
extends
AndroidUpnpServiceImpl {
03.
04.
@Override
05.
protected
AndroidUpnpServiceConfiguration createConfiguration(WifiManager wifiManager) {
06.
return
new
AndroidUpnpServiceConfiguration(wifiManager) {
07.
08.
@Override
09.
public
int
getRegistryMaintenanceIntervalMillis() {
10.
return
7000
;
11.
}
12.
13.
};
14.
}
15.
}
16.
```
此时不要忘了在AndroidManifest.xml内配置MyUpnpService,而不是原先的实现。当在你的activities里绑定服务时,也必须使用该类型。
#### 5.3.2. 暂停和恢复注册表维护
另外一个更有效同时也不是很复杂的优化是,每当你的activites不再需要UPnP服务时,暂停和恢复注册表。这通常发生在当activity不在前台(暂停),甚至不再显示(停止)时。默认情况下,activity状态改变对UPnP服务没有影响,除非你在activities生命周期的回调内绑定和解绑服务。
除了绑定和解绑服务,你也可以在activity onPause()或onStop()方法被调用时,通过调用Registry#pause()来暂停注册表。之后,你可以通过Registry#resume()来恢复后台服务,或同时用Registry#isPaused()检查状态。
请阅读这些方法的Javadoc了解详细信息,以及暂停注册表维护对于设备、服务和GENA订阅的意义。根据你的应用要做什么,否则这种小的优化可能不值得处理这些效果。另一方面,你的应用应当能够处理失败的GENA订阅续期,或者消失的远程设备。 www.it165.net
#### 5.3.3. 配置搜索
最有效的优化是UPnP设备有选择性的搜索。尽管UPnP服务的网络传输层在后台会保持运行(线程正等待且socket被绑定),这个特性允许你有选择且快速的丢弃搜索信息。
举例来说,如果你正在写一个控制端,且不通告你想要控制的服务(对其他设备没兴趣),那么你可以丢弃所有接收的搜索信息。另一方面,如果你只提供设备和服务,所有搜索信息(除了你自身服务的搜索信息)可能都可以被丢弃,你对其他远程设备和其服务一点都不会有兴趣。
一旦UDP数据包内容可用,该搜索信息就会被Cling选择并偷偷的丢弃,所以不需要进一步得解析和处理,同时CPU时间和内存消耗显著得减少,即使当你在Android手持设备上后台持续运行UPnP服务。
为了配置你的控制端应用支持哪些服务,需要覆盖前面章节展示的服务接口并提供一组ServiceType实例:
01.
```
02.
public
class
MyUpnpService
extends
AndroidUpnpServiceImpl {
03.
04.
@Override
05.
protected
AndroidUpnpServiceConfiguration createConfiguration(WifiManager wifiManager) {
06.
return
new
AndroidUpnpServiceConfiguration(wifiManager) {
07.
08.
@Override
09.
public
ServiceType[] getExclusiveServiceTypes() {
10.
return
new
ServiceType[] {
11.
new
UDAServiceType(
"SwitchPower"
)
12.
};
13.
}
14.
15.
};
16.
}
17.
}
18.
```
这个配置将会忽略所有不通告chemas-upnp-org:SwitchPower:1的任何通告。这是我们控制端要处理的,不需要其他任何东西了。如果你返回一个空的数组(默认行为),所有服务和设备将会发现以及没有通告会被丢弃。
如果你正在写一个控制端应用而不是服务应用,你可以让getExclusiveServiceTypes()方法返回null。这将会完全禁用搜索,此时所有设备和服务的通告一接收就会被丢弃。