5.2. Accessing the service from an activity
The lifecycle of service components in Android is well defined. The first activity which binds to a
service will start the service if it is not already running. When no activity is bound to the
service any more, the operating system will destroy the service.
Let's write a simple UPnP browsing activity. It shows all devices on your network in a list and it
has a menu option which triggers a search action. The activity connects to the UPnP service and then
listens to any device additions or removals in the Registry
, so the displayed list of
devices is kept up-to-date:
public class BrowserActivity extends ListActivity {
private ArrayAdapter<DeviceDisplay> listAdapter;
private BrowseRegistryListener registryListener = new BrowseRegistryListener();
private AndroidUpnpService upnpService;
private ServiceConnection serviceConnection = new ServiceConnection() {
public void onServiceConnected(ComponentName className, IBinder service) {
upnpService = (AndroidUpnpService) service;
// Clear the list
listAdapter.clear();
// Get ready for future device advertisements
upnpService.getRegistry().addListener(registryListener);
// Now add all devices to the list we already know about
for (Device device : upnpService.getRegistry().getDevices()) {
registryListener.deviceAdded(device);
}
// Search asynchronously for all devices, they will respond soon
upnpService.getControlPoint().search();
}
public void onServiceDisconnected(ComponentName className) {
upnpService = null;
}
};
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
// Fix the logging integration between java.util.logging and Android internal logging
org.seamless.util.logging.LoggingUtil.resetRootHandler(
new FixedAndroidLogHandler()
);
// Now you can enable logging as needed for various categories of Cling:
// Logger.getLogger("org.fourthline.cling").setLevel(Level.FINEST);
listAdapter = new ArrayAdapter<>(this, android.R.layout.simple_list_item_1);
setListAdapter(listAdapter);
// This will start the UPnP service if it wasn't already started
getApplicationContext().bindService(
new Intent(this, AndroidUpnpServiceImpl.class),
serviceConnection,
Context.BIND_AUTO_CREATE
);
}
@Override
protected void onDestroy() {
super.onDestroy();
if (upnpService != null) {
upnpService.getRegistry().removeListener(registryListener);
}
// This will stop the UPnP service if nobody else is bound to it
getApplicationContext().unbindService(serviceConnection);
}
// ...
}
We utilize the default layout provided by the Android runtime and the ListActivity
superclass. Note that this activity can be your applications main activity, or further up
in the stack of a task. The listAdapter
is the glue between the device additions
and removals on the Cling Registry
and the list of items shown in the user interface.
Debug logging on Android
Cling uses the standard JDK logging, java.util.logging
. Unfortunately, by default
on Android you will not see FINE
, FINER
, and FINEST
log
messages, as their built-in log handler is broken (or, so badly designed that it might as well
be broken). The easiest workaround is to set a custom log handler available in the
FixedAndroidLogHandler
class.
The upnpService
variable is null
when no backend service is bound to this
activity. Binding and unbinding occurs in the onCreate()
and onDestroy()
callbacks, so the activity is bound to the service as long as it is alive.
Binding and unbinding the service is handled with the ServiceConnection
:
On connect, first a listener is added to the Registry
of the UPnP service. This
listener will process additions and removals of devices as they are discovered on your network, and
update the items shown in the user interface list. The BrowseRegistryListener
is
removed when the activity is destroyed.
Then any already discovered devices are added manually to the user interface, passing them
through the listener. (There might be none if the UPnP service was just started and no device
has so far announced its presence.) Finally, you start asynchronous discovery by sending a
search message to all UPnP devices, so they will announce themselves. This search message is
NOT required every time you connect to the service. It is only necessary once, to populate the
registry with all known devices when your (main) activity and application starts.
This is the BrowseRegistryListener
, its only job is to update the
displayed list items:
protected class BrowseRegistryListener extends DefaultRegistryListener {
/* Discovery performance optimization for very slow Android devices! */
@Override
public void remoteDeviceDiscoveryStarted(Registry registry, RemoteDevice device) {
deviceAdded(device);
}
@Override
public void remoteDeviceDiscoveryFailed(Registry registry, final RemoteDevice device, final Exception ex) {
runOnUiThread(new Runnable() {
public void run() {
Toast.makeText(
BrowserActivity.this,
"Discovery failed of '" + device.getDisplayString() + "': "
+ (ex != null ? ex.toString() : "Couldn't retrieve device/service descriptors"),
Toast.LENGTH_LONG
).show();
}
});
deviceRemoved(device);
}
/* End of optimization, you can remove the whole block if your Android handset is fast (>= 600 Mhz) */
@Override
public void remoteDeviceAdded(Registry registry, RemoteDevice device) {
deviceAdded(device);
}
@Override
public void remoteDeviceRemoved(Registry registry, RemoteDevice device) {
deviceRemoved(device);
}
@Override
public void localDeviceAdded(Registry registry, LocalDevice device) {
deviceAdded(device);
}
@Override
public void localDeviceRemoved(Registry registry, LocalDevice device) {
deviceRemoved(device);
}
public void deviceAdded(final Device device) {
runOnUiThread(new Runnable() {
public void run() {
DeviceDisplay d = new DeviceDisplay(device);
int position = listAdapter.getPosition(d);
if (position >= 0) {
// Device already in the list, re-set new value at same position
listAdapter.remove(d);
listAdapter.insert(d, position);
} else {
listAdapter.add(d);
}
}
});
}
public void deviceRemoved(final Device device) {
runOnUiThread(new Runnable() {
public void run() {
listAdapter.remove(new DeviceDisplay(device));
}
});
}
}
For performance reasons, when a new device has been discovered, we don't wait until a fully hydrated
(all services retrieved and validated) device metadata model is available. We react as quickly as
possible and don't wait until the remoteDeviceAdded()
method will be called. We display
any device even while discovery is still running. You'd usually not care about this on a desktop
computer, however, Android handheld devices are slow and UPnP uses several bloated XML descriptors
to exchange metadata about devices and services. Sometimes it can take several seconds before a
device and its services are fully available. The remoteDeviceDiscoveryStarted()
and
remoteDeviceDiscoveryFailed()
methods are called as soon as possible in the discovery
process. On modern fast Android handsets, and unless you have to deal with dozens of UPnP devices
on a LAN, you don't need this optimization.
By the way, devices are equal (a.equals(b)
) if they have the same UDN, they might not
be identical (a==b
).
The Registry
will call the listener methods in a separate thread. You have to update
the displayed list data in the thread of the user interface.
The following methods on the activity add a menu with a search action, so a user can refresh the
list manually:
public class BrowserActivity extends ListActivity {
@Override
public boolean onCreateOptionsMenu(Menu menu) {
menu.add(0, 0, 0, R.string.searchLAN).setIcon(android.R.drawable.ic_menu_search);
return true;
}
@Override
public boolean onOptionsItemSelected(MenuItem item) {
switch (item.getItemId()) {
case 0:
if (upnpService == null)
break;
Toast.makeText(this, R.string.searchingLAN, Toast.LENGTH_SHORT).show();
upnpService.getRegistry().removeAllRemoteDevices();
upnpService.getControlPoint().search();
break;
}
return false;
}
// ...
}
Finally, the DeviceDisplay
class is a very simple JavaBean that only provides a
toString()
method for rendering the list. You can display any information about UPnP
devices by changing this method:
protected class DeviceDisplay {
Device device;
public DeviceDisplay(Device device) {
this.device = device;
}
public Device getDevice() {
return device;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
DeviceDisplay that = (DeviceDisplay) o;
return device.equals(that.device);
}
@Override
public int hashCode() {
return device.hashCode();
}
@Override
public String toString() {
String name =
getDevice().getDetails() != null && getDevice().getDetails().getFriendlyName() != null
? getDevice().getDetails().getFriendlyName()
: getDevice().getDisplayString();
// Display a little star while the device is being loaded (see performance optimization earlier)
return device.isFullyHydrated() ? name : name + " *";
}
}
We have to override the equality operations as well, so we can remove and add devices from the list
manually with the DeviceDisplay
instance as a convenient handle.
So far we have implemented a UPnP control point, next we create a UPnP device with services.