1 package jp.osdn.gokigen.gokigenassets.camera.vendor.sony.wrapper.connection
3 import android.content.Context
4 import android.util.Log
5 import jp.osdn.gokigen.gokigenassets.camera.interfaces.ICameraStatusReceiver
6 import jp.osdn.gokigen.gokigenassets.camera.vendor.sony.wrapper.ISonyCamera
7 import jp.osdn.gokigen.gokigenassets.camera.vendor.sony.wrapper.SonyCameraDeviceProvider
8 import jp.osdn.gokigen.gokigenassets.constants.ICameraConstantConvert
9 import jp.osdn.gokigen.gokigenassets.utils.communication.SimpleHttpClient
10 import jp.osdn.gokigen.gokigenassets.utils.communication.XmlElement
11 import java.net.DatagramPacket
12 import java.net.DatagramSocket
13 import java.net.InetSocketAddress
14 import java.nio.charset.Charset
16 class SonySsdpClient(private val context: Context, private val callback: ISearchResultCallback, private val cameraStatusReceiver: ICameraStatusReceiver, private var sendRepeatCount: Int = 0)
20 private val TAG = SonySsdpClient::class.java.simpleName
21 private const val SEND_TIMES_DEFAULT = 3
22 private const val SEND_WAIT_DURATION_MS = 300
23 private const val SSDP_RECEIVE_TIMEOUT = 4 * 1000 // msec
24 private const val PACKET_BUFFER_SIZE = 4096
25 private const val SSDP_PORT = 1900
26 private const val SSDP_MX = 2
27 private const val SSDP_ADDR = "239.255.255.250"
28 private const val SSDP_ST = "urn:schemas-sony-com:service:ScalarWebAPI:1"
29 private const val SSDP_ST2 = "urn:schemas-upnp-org:service:ContentDirectory:1"
30 private const val SSDP_ST3 = "urn:schemas-upnp-org:device:MediaServer:1"
33 private val ssdpRequest: String
34 private val useSmartphoneTransfer = false
38 this.sendRepeatCount = if (sendRepeatCount > 0) sendRepeatCount else SEND_TIMES_DEFAULT
39 ssdpRequest = "M-SEARCH * HTTP/1.1\r\nHOST: $SSDP_ADDR:$SSDP_PORT\r\nMAN: \"ssdp:discover\"\r\nMX: $SSDP_MX\r\nST: $SSDP_ST\r\n\r\n"
44 val sendData = ssdpRequest.toByteArray()
46 var socket: DatagramSocket? = null
47 var receivePacket: DatagramPacket
48 val packet: DatagramPacket
53 socket = DatagramSocket()
54 socket.reuseAddress = true
55 val iAddress = InetSocketAddress(SSDP_ADDR, SSDP_PORT)
56 packet = DatagramPacket(sendData, sendData.size, iAddress)
59 for (loop in 1..sendRepeatCount)
61 cameraStatusReceiver.onStatusNotify(context.getString(ICameraConstantConvert.ID_STRING_CONNECT_CAMERA_SEARCH_REQUEST) + " " + loop)
63 Thread.sleep(SEND_WAIT_DURATION_MS.toLong())
65 Log.v(TAG, " SSDP : SEND")
69 if (socket != null && !socket.isClosed)
76 callback.onErrorFinished(detailString + " : " + e.localizedMessage)
81 val startTime = System.currentTimeMillis()
82 var currentTime = System.currentTimeMillis()
83 val foundDevices: MutableList<String?> = ArrayList()
84 val array = ByteArray(PACKET_BUFFER_SIZE)
88 cameraStatusReceiver.onStatusNotify(context.getString(ICameraConstantConvert.ID_STRING_CONNECT_WAIT_REPLY_CAMERA))
89 while (currentTime - startTime < SSDP_RECEIVE_TIMEOUT)
91 receivePacket = DatagramPacket(array, array.size)
92 socket.soTimeout = SSDP_RECEIVE_TIMEOUT
93 socket.receive(receivePacket)
94 val ssdpReplyMessage = String(receivePacket.getData(), 0, receivePacket.length, Charset.forName("UTF-8"))
96 if (ssdpReplyMessage.contains("HTTP/1.1 200"))
98 ddUsn = findParameterValue(ssdpReplyMessage, "USN")
99 cameraStatusReceiver.onStatusNotify(context.getString(ICameraConstantConvert.ID_STRING_CONNECT_CAMERA_RECEIVED_REPLY))
100 if (!foundDevices.contains(ddUsn))
102 val ddLocation = findParameterValue(ssdpReplyMessage, "LOCATION")
103 foundDevices.add(ddUsn)
105 //// Fetch Device Description XML and parse it.
106 if (ddLocation != null)
108 cameraStatusReceiver.onStatusNotify("LOCATION : $ddLocation")
109 val device: ISonyCamera? = searchSonyCameraDevice(ddLocation)
110 if ((device != null)&&(device.hasApiService("camera")))
112 cameraStatusReceiver.onStatusNotify(context.getString(ICameraConstantConvert.ID_STRING_CONNECT_CAMERA_FOUND) + " " + device.getFriendlyName())
113 callback.onDeviceFound(device)
114 // カメラが見つかった場合は breakする
119 if (useSmartphoneTransfer && device != null)
121 cameraStatusReceiver.onStatusNotify(context.getString(ICameraConstantConvert.ID_STRING_CONNECT_CAMERA_FOUND) + " " + device.getFriendlyName() + " (Smartphone Transfer)")
122 Log.v(TAG, " SMARTPHONE TRANSFER : $ssdpReplyMessage")
123 callback.onDeviceFound(device)
128 cameraStatusReceiver.onStatusNotify(context.getString(ICameraConstantConvert.ID_STRING_CAMERA_NOT_FOUND))
134 Log.v(TAG, "Already received. : $ddUsn")
139 Log.v(TAG, " SSDP REPLY MESSAGE (ignored) : $ssdpReplyMessage")
141 currentTime = System.currentTimeMillis()
149 callback.onErrorFinished(detailString + " : " + e.localizedMessage)
156 if (!socket.isClosed())
161 catch (ee: Exception)
166 callback.onFinished()
169 private fun findParameterValue(ssdpMessage: String, paramName: String): String?
172 if (!name.endsWith(":"))
176 var start = ssdpMessage.indexOf(name)
177 val end = ssdpMessage.indexOf("\r\n", start)
178 if (start != -1 && end != -1)
183 return ssdpMessage.substring(start, end).trim { it <= ' ' }
193 private fun searchSonyCameraDevice(ddUrl: String): ISonyCamera?
195 val httpClient = SimpleHttpClient()
196 var device: SonyCameraDeviceProvider? = null
200 ddXml = httpClient.httpGet(ddUrl, -1)
201 Log.d(TAG, "fetch () httpGet done. : " + ddXml.length)
202 if (ddXml.length < 2)
205 Log.v(TAG, "NO BODY")
209 catch (e: java.lang.Exception)
216 //Log.v(TAG, "ddXml : " + ddXml);
217 val rootElement = XmlElement.parse(ddXml)
220 if ("root" == rootElement.tagName)
223 val deviceElement = rootElement.findChild("device")
224 val friendlyName = deviceElement.findChild("friendlyName").value
225 val modelName = deviceElement.findChild("modelName").value
226 val udn = deviceElement.findChild("UDN").value
230 val iconListElement = deviceElement.findChild("iconList")
231 val iconElements = iconListElement.findChildren("icon")
232 for (iconElement in iconElements) {
233 // Choose png icon to show Android UI.
234 if ("image/png" == iconElement.findChild("mimetype").value) {
235 val uri = iconElement.findChild("url").value
236 val hostUrl = toSchemeAndHost(ddUrl)
237 iconUrl = hostUrl + uri
240 device = SonyCameraDeviceProvider(ddUrl, friendlyName, modelName, udn, iconUrl)
242 // "av:X_ScalarWebAPI_DeviceInfo"
243 val wApiElement = deviceElement.findChild("X_ScalarWebAPI_DeviceInfo")
244 val wApiServiceListElement = wApiElement.findChild("X_ScalarWebAPI_ServiceList")
245 val wApiServiceElements =
246 wApiServiceListElement.findChildren("X_ScalarWebAPI_Service")
247 for (wApiServiceElement in wApiServiceElements) {
249 wApiServiceElement.findChild("X_ScalarWebAPI_ServiceType").value
251 wApiServiceElement.findChild("X_ScalarWebAPI_ActionList_URL").value
252 device.addApiService(serviceName, actionUrl)
255 } catch (e: java.lang.Exception) {
258 Log.d(TAG, "fetch () parsing XML done.")
259 if (device == null) {
260 Log.v(TAG, "device is null.")
265 private fun toSchemeAndHost(url: String): String {
266 val i = url.indexOf("://") // http:// or https://
270 val j = url.indexOf("/", i + 3)
271 return if (j == -1) {
273 } else url.substring(0, j)
276 private fun toHost(url: String): String
278 val i = url.indexOf("://") // http:// or https://
282 val j = url.indexOf(":", i + 3)
283 return if (j == -1) {
285 } else url.substring(i + 3, j)
292 interface ISearchResultCallback
294 fun onDeviceFound(cameraDevice: ISonyCamera) // デバイスが見つかった!
295 fun onFinished() // 通常の終了をしたとき
296 fun onErrorFinished(reason: String?) // エラーが発生して応答したとき