天天看點

Android基于Cling開發DLNA應用

# Android基于Cling開發DLNA應用

<a href="http://www.dlna.org/" target="_blank">## DLNA</a>

DLNA,Digital Living Network Alliance的簡稱,即數字生活網絡聯盟。其由消費性電子、行動電話以及電腦廠商組成。目标在于建立一套可以使得各廠商的産品互相連接配接,互相适應的工業标準,進而為消費者實作數字化生活。

更多資料:

<a href="http://4thline.org/projects/cling" target="_blank">## Cling</a>

UPnP/DLNA library for Java and Android。

GitHub最多關注,目前仍在維護,許可協定為LGPL或CDDL。

以下為中文譯文:

## 5. Android上的Cling 

Cling Core為Android應用提供了UPnP棧。由于如今大部分Android系統都是小型手持裝置,是以通常你需要寫控制端應用。然而你也可以寫Android上的UPnP服務應用,其所有特性Cling Core都支援。 

``` 

Android模拟器上的Cling 

在寫此時,Android模拟器還不支援接收UDP多點傳播。不過,可以發送UDP多點傳播。你能夠發送一個多點傳播UPnP搜尋,并接收UDP單點傳播回應,繼而發現正運作的裝置。你發現不了在搜尋後新開啟的裝置,并且在裝置關閉時也收不到消息。另外,其他在你網絡的控制端應用,則不能發現你本地的Android裝置或服務。在你測試應用時,所有這些情況都會使你感到困惑,是以除非你真得了解哪些有作用、哪些沒有,不然你應當使用一個真正的裝置。 

這章闡述了你如何整合Cling到你的Android應用,使其成為一個共享的部件。 

### 5.1. 配置應用服務 

你可以在Android應用主activity中執行個體化Cling UpnpService。另一方面,如果你好些activities都要要求通路UPnP棧,那麼最好采用背景服務,android.app.Service。之後,任何想要通路UPnP棧的activity,都能夠在需要時綁定或解綁該服務。 

該服務元件的接口是org.teleal.cling.android.AndroidUpnpService: 

public interface AndroidUpnpService { 

    public UpnpService get(); 

    public UpnpServiceConfiguration getConfiguration(); 

    public Registry getRegistry(); 

    public ControlPoint getControlPoint(); 

activity通常通路已知UPnP裝置的系統資料庫,或者通過ControlPoint查詢和控制UPnP裝置。 

你必須在AndroidManifest.xml内配置内建的服務實作: 

&lt;manifest ...&gt; 

    &lt;uses-permission android:name="android.permission.INTERNET"/&gt; 

    &lt;uses-permission android:name="android.permission.ACCESS_WIFI_STATE"/&gt; 

    &lt;uses-permission android:name="android.permission.CHANGE_WIFI_MULTICAST_STATE"/&gt; 

    &lt;uses-permission android:name="android.permission.ACCESS_NETWORK_STATE"/&gt; 

    &lt;application ...&gt; 

        &lt;activity ...&gt; 

            ... 

        &lt;/activity&gt; 

        &lt;service android:name="org.teleal.cling.android.AndroidUpnpServiceImpl"/&gt; 

    &lt;/application&gt; 

&lt;/manifest&gt; 

此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類的骨架: 

import android.app.ListActivity; 

import android.content.ComponentName; 

import android.content.Context; 

import android.content.Intent; 

import android.content.ServiceConnection; 

import android.os.Bundle; 

import android.os.IBinder; 

import android.view.Menu; 

import android.view.MenuItem; 

import android.widget.ArrayAdapter; 

import android.widget.Toast; 

import org.teleal.cling.android.AndroidUpnpService; 

import org.teleal.cling.android.AndroidUpnpServiceImpl; 

import org.teleal.cling.model.meta.Device; 

import org.teleal.cling.model.meta.LocalDevice; 

import org.teleal.cling.model.meta.RemoteDevice; 

import org.teleal.cling.registry.DefaultRegistryListener; 

import org.teleal.cling.registry.Registry; 

public class UpnpBrowser extends ListActivity { 

    private ArrayAdapter&lt;DeviceDisplay&gt; listAdapter; 

    private AndroidUpnpService upnpService; 

    private ServiceConnection serviceConnection = ... 

    private RegistryListener registryListener = new BrowseRegistryListener(); 

    @Override 

    public void onCreate(Bundle savedInstanceState) { 

        super.onCreate(savedInstanceState); 

        listAdapter = 

            new ArrayAdapter( 

                this, 

                android.R.layout.simple_list_item_1 

            ); 

        setListAdapter(listAdapter); 

        getApplicationContext().bindService( 

            new Intent(this, AndroidUpnpServiceImpl.class), 

            serviceConnection, 

            Context.BIND_AUTO_CREATE 

        ); 

    } 

    protected void onDestroy() { 

        super.onDestroy(); 

        if (upnpService != null) { 

            upnpService.getRegistry().removeListener(registryListener); 

        } 

        getApplicationContext().unbindService(serviceConnection); 

    ... 

我們采用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處理綁定和解綁服務: 

private ServiceConnection serviceConnection = new ServiceConnection() { 

    public void onServiceConnected(ComponentName className, IBinder service) { 

        upnpService = (AndroidUpnpService) service; 

        // Refresh the list with all known devices 

        listAdapter.clear(); 

        for (Device device : upnpService.getRegistry().getDevices()) { 

            registryListener.deviceAdded(device); 

        // Getting ready for future device advertisements 

        upnpService.getRegistry().addListener(registryListener); 

        // Search asynchronously for all devices 

        upnpService.getControlPoint().search(); 

    public void onServiceDisconnected(ComponentName className) { 

        upnpService = null; 

}; 

首先,所有已知的UPnP裝置能夠被查詢和顯示(如果UPnP服務剛開啟且到到目前還沒有裝置通告它的存在)。 

然後,給UPnP服務的系統資料庫增加一個監聽者。該監聽者将會處理在你網絡上發現的裝置的增加和移除,并在更新在使用者界面清單内顯示的項目。當activity銷毀時,BrowseRegistryListener會被移除。 

最後,通過發送一個搜尋消息給所有UPnP裝置,你會開啟異步搜尋,此時這些裝置将通告它們的存在。注意這個搜尋消息不是每次連接配接服務都需要的。這隻需一次,在當主activity和應用啟動時,其會将已知裝置寫入系統資料庫。 

以下是BrowseRegistryListener,他的任務就是更新清單項的顯示: 

class BrowseRegistryListener extends DefaultRegistryListener { 

    public void remoteDeviceDiscoveryStarted(Registry registry, RemoteDevice device) { 

        deviceAdded(device); 

    public void remoteDeviceDiscoveryFailed(Registry registry, final RemoteDevice device, final Exception ex) { 

        runOnUiThread(new Runnable() { 

            public void run() { 

                Toast.makeText( 

                        BrowseActivity.this, 

                        "Discovery failed of '" + device.getDisplayString() + "': " + 

                                (ex != null ? ex.toString() : "Couldn't retrieve device/service descriptors"), 

                        Toast.LENGTH_LONG 

                ).show(); 

            } 

        }); 

        deviceRemoved(device); 

    public void remoteDeviceAdded(Registry registry, RemoteDevice device) { 

    public void remoteDeviceRemoved(Registry registry, RemoteDevice device) { 

    public void localDeviceAdded(Registry registry, LocalDevice device) { 

    public void localDeviceRemoved(Registry registry, LocalDevice device) { 

    public void deviceAdded(final Device device) { 

                DeviceDisplay d = new DeviceDisplay(device); 

                int position = listAdapter.getPosition(d); 

                if (position &gt;= 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) { 

                listAdapter.remove(new DeviceDisplay(device)); 

鑒于性能的原因,當發現裝置時,我們會直到一個完整的hydrated(所有裝置被檢索和驗證)裝置中繼資料模型可用時才執行等待。我們響應盡可能得快,同時隻當remoteDeviceAdded()方法被調用時才去等待。甚至當搜尋仍在運作時,我們仍舊顯示所有裝置。在台式電腦上你通常不需要關心這個,不過,Android手持裝置效率慢,并且UPnP使用好些臃腫的XML描述符來交換關于裝置和服務的中繼資料。有時,在裝置和它的服務完全可用前,這可能會花費數秒鐘。而remoteDeviceDiscoveryStarted()和remoteDeviceDiscoveryFailed()方法在搜尋處理時會盡快被調用。順便說一句,如果裝置有相同的UDN就表示相等的(a.equal(b)),但它們可能不會完全一緻(a==b)。 

注意系統資料庫将會在分開的線程中調用監聽者方法。你必須在UI線程中更新顯示清單資料。 

activity中以下兩個方法增加了用來搜尋的菜單,如此使用者才能手動的重新整理清單: 

@Override 

public boolean onCreateOptionsMenu(Menu menu) { 

    menu.add(0, 0, 0, R.string.search_lan) 

        .setIcon(android.R.drawable.ic_menu_search); 

    return true; 

public boolean onOptionsItemSelected(MenuItem item) { 

    if (item.getItemId() == 0 &amp;&amp; upnpService != null) { 

        upnpService.getRegistry().removeAllRemoteDevices(); 

    return false; 

最後,DeviceDisplay類是一個非常簡單的JavaBean,隻提供一個toString()方法來呈現清單資訊。通過修改此方法,你能夠顯示任何關于UPnP裝置的資訊: 

class DeviceDisplay { 

    Device device; 

    public DeviceDisplay(Device device) { 

        this.device = device; 

    public Device getDevice() { 

        return device; 

    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); 

    public int hashCode() { 

        return device.hashCode(); 

    public String toString() { 

        // Display a little star while the device is being loaded 

        return device.isFullyHydrated() ? device.getDisplayString() : device.getDisplayString() + " *"; 

還有我們必須覆寫相等操作,這樣我們才可以用DeviceDisplay執行個體作為便捷的處理,從清單中手動地移除和增加裝置。 

### 5.3. 優化服務行為 

UPnP服務運作時會消耗記憶體和CPU。盡管通常在一個正常的機器上沒有什麼問題,但在Android手持裝置上就可能會有了。如果你禁用Cling UPnP服務的某些功能,或者設定暫停且在合适時恢複它,你可以留有更多的記憶體和電量。 

#### 5.3.1. 調整系統資料庫維護 

當服務運作時,背景有好些東西在執行。首先,有一個服務的系統資料庫和其維護線程。如果你寫一個控制端,背景系統資料庫維護者将會定期從遠端服務更新你對外的GENA訂閱。當沒有通知斷開網絡時,它也會到期并移除任何遠端服務。如果你正提供服務,你的裝置通告将被系統資料庫維護者重新整理,并在GENA訂閱沒及時更新時移除它。系統資料庫維護者為了有效得防止UPnP網絡上的過時狀态,是以所有參與者會實時更新其他參與者的視圖等等。 

預設情況下,系統資料庫維護者會每秒運作并檢查是否有事要做(當然,大多數情況下沒事做)。然而預設的Android配置有5秒的間隔休眠,是以這已經花費了更少的背景CPU占用時間 — 不過你的應用可能會暴露稍微過時的資訊。在UpnpServiceConfiguration你可以通過覆寫getRegistryMaintenanceIntervalMillis()進一步的調整設定。在Android上,你必須子類化服務實作來提供一個新的配置。 

public class MyUpnpService extends AndroidUpnpServiceImpl { 

    protected AndroidUpnpServiceConfiguration createConfiguration(WifiManager wifiManager) { 

        return new AndroidUpnpServiceConfiguration(wifiManager) { 

            @Override 

            public int getRegistryMaintenanceIntervalMillis() { 

                return 7000; 

        }; 

此時不要忘了在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訂閱續期,或者消失的遠端裝置。 

#### 5.3.3. 配置搜尋 

最有效的優化是UPnP裝置有選擇性的搜尋。盡管UPnP服務的網絡傳輸層在背景會保持運作(線程正等待且socket被綁定),這個特性允許你有選擇且快速的丢棄搜尋資訊。 

舉例來說,如果你正在寫一個控制端,且不通告你想要控制的服務(對其他裝置沒興趣),那麼你可以丢棄所有接收的搜尋資訊。另一方面,如果你隻提供裝置和服務,所有搜尋資訊(除了你自身服務的搜尋資訊)可能都可以被丢棄,你對其他遠端裝置和其服務一點都不會有興趣。 

一旦UDP資料包内容可用,該搜尋資訊就會被Cling選擇并偷偷的丢棄,是以不需要進一步得解析和處理,同時CPU時間和記憶體消耗顯著得減少,即使當你在Android手持裝置上背景持續運作UPnP服務。 

為了配置你的控制端應用支援哪些服務,需要覆寫前面章節展示的服務接口并提供一組ServiceType執行個體: 

            public ServiceType[] getExclusiveServiceTypes() { 

                return new ServiceType[] { 

                        new UDAServiceType("SwitchPower") 

                }; 

這個配置将會忽略所有不通告chemas-upnp-org:SwitchPower:1的任何通告。這是我們控制端要處理的,不需要其他任何東西了。如果你傳回一個空的數組(預設行為),所有服務和裝置将會發現以及沒有通告會被丢棄。 

如果你正在寫一個控制端應用而不是服務應用,你可以讓getExclusiveServiceTypes()方法傳回null。這将會完全禁用搜尋,此時所有裝置和服務的通告一接收就會被丢棄。 

     本文轉自winorlose2000 51CTO部落格,原文連結:http://blog.51cto.com/vaero/1143278,如需轉載請自行聯系原作者

繼續閱讀