/*
* SSDPDiscoveryProvider
* Connect SDK
*
* Copyright (c) 2014 LG Electronics.
* Created by Hyun Kook Khang on 19 Jan 2014
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.connectsdk.discovery.provider;
import android.content.Context;
import android.util.Log;
import com.connectsdk.core.Util;
import com.connectsdk.discovery.DiscoveryFilter;
import com.connectsdk.discovery.DiscoveryProvider;
import com.connectsdk.discovery.DiscoveryProviderListener;
import com.connectsdk.discovery.provider.ssdp.SSDPClient;
import com.connectsdk.discovery.provider.ssdp.SSDPDevice;
import com.connectsdk.discovery.provider.ssdp.SSDPPacket;
import com.connectsdk.service.config.ServiceDescription;
import org.xml.sax.SAXException;
import java.io.IOException;
import java.net.InetAddress;
import java.net.URL;
import java.net.UnknownHostException;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
import java.util.Timer;
import java.util.TimerTask;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import javax.xml.parsers.ParserConfigurationException;
public class SSDPDiscoveryProvider implements DiscoveryProvider {
Context context;
boolean needToStartSearch = false;
private CopyOnWriteArrayList<DiscoveryProviderListener> serviceListeners;
ConcurrentHashMap<String, ServiceDescription> foundServices = new ConcurrentHashMap<String, ServiceDescription>();
ConcurrentHashMap<String, ServiceDescription> discoveredServices = new ConcurrentHashMap<String, ServiceDescription>();
List<DiscoveryFilter> serviceFilters;
private SSDPClient ssdpClient;
private Timer scanTimer;
private Pattern uuidReg;
private Thread responseThread;
private Thread notifyThread;
boolean isRunning = false;
public SSDPDiscoveryProvider(Context context) {
this.context = context;
uuidReg = Pattern.compile("(?<=uuid:)(.+?)(?=(::)|$)");
serviceListeners = new CopyOnWriteArrayList<DiscoveryProviderListener>();
serviceFilters = new CopyOnWriteArrayList<DiscoveryFilter>();
}
private void openSocket() {
if (ssdpClient != null && ssdpClient.isConnected())
return;
try {
InetAddress source = Util.getIpAddress(context);
if (source == null)
return;
ssdpClient = createSocket(source);
} catch (UnknownHostException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
}
protected SSDPClient createSocket(InetAddress source) throws IOException {
return new SSDPClient(source);
}
@Override
public void start() {
if (isRunning)
return;
isRunning = true;
openSocket();
scanTimer = new Timer();
scanTimer.schedule(new TimerTask() {
@Override
public void run() {
sendSearch();
}
}, 100, RESCAN_INTERVAL);
responseThread = new Thread(mResponseHandler);
notifyThread = new Thread(mRespNotifyHandler);
responseThread.start();
notifyThread.start();
}
public void sendSearch() {
List<String> killKeys = new ArrayList<String>();
long killPoint = new Date().getTime() - TIMEOUT;
for (String key : foundServices.keySet()) {
ServiceDescription service = foundServices.get(key);
if (service == null || service.getLastDetection() < killPoint) {
killKeys.add(key);
}
}
for (String key : killKeys) {
final ServiceDescription service = foundServices.get(key);
if (service != null) {
notifyListenersOfLostService(service);
}
if (foundServices.containsKey(key))
foundServices.remove(key);
}
rescan();
}
@Override
public void stop() {
isRunning = false;
if (scanTimer != null) {
scanTimer.cancel();
scanTimer = null;
}
if (responseThread != null) {
responseThread.interrupt();
responseThread = null;
}
if (notifyThread != null) {
notifyThread.interrupt();
notifyThread = null;
}
if (ssdpClient != null) {
ssdpClient.close();
ssdpClient = null;
}
}
@Override
public void restart() {
stop();
start();
}
@Override
public void reset() {
stop();
foundServices.clear();
discoveredServices.clear();
}
@Override
public void rescan() {
for (DiscoveryFilter searchTarget : serviceFilters) {
final String message = SSDPClient.getSSDPSearchMessage(searchTarget.getServiceFilter());
Timer timer = new Timer();
/* Send 3 times like WindowsMedia */
for (int i = 0; i < 3; i++) {
TimerTask task = new TimerTask() {
@Override
public void run() {
try {
if (ssdpClient != null)
ssdpClient.send(message);
} catch (IOException e) {
e.printStackTrace();
}
}
};
timer.schedule(task, i * 1000);
}
}
}
@Override
public void addDeviceFilter(DiscoveryFilter filter) {
if (filter.getServiceFilter() == null) {
Log.e(Util.T, "This device filter does not have ssdp filter info");
} else {
serviceFilters.add(filter);
}
}
@Override
public void removeDeviceFilter(DiscoveryFilter filter) {
serviceFilters.remove(filter);
}
@Override
public void setFilters(List<DiscoveryFilter> filters) {
serviceFilters = filters;
}
@Override
public boolean isEmpty() {
return serviceFilters.size() == 0;
}
private Runnable mResponseHandler = new Runnable() {
@Override
public void run() {
while (ssdpClient != null) {
try {
handleSSDPPacket(new SSDPPacket(ssdpClient.responseReceive()));
} catch (IOException e) {
e.printStackTrace();
break;
} catch (RuntimeException e) {
e.printStackTrace();
break;
}
}
}
};
private Runnable mRespNotifyHandler = new Runnable() {
@Override
public void run() {
while (ssdpClient != null) {
try {
handleSSDPPacket(new SSDPPacket(ssdpClient.multicastReceive()));
} catch (IOException e) {
e.printStackTrace();
break;
} catch (RuntimeException e) {
e.printStackTrace();
break;
}
}
}
};
private void handleSSDPPacket(SSDPPacket ssdpPacket) {
// Debugging stuff
// Util.runOnUI(new Runnable() {
//
// @Override
// public void run() {
// Log.d("Connect SDK Socket", "Packet received | type = " + ssdpPacket.type);
//
// for (String key : ssdpPacket.data.keySet()) {
// Log.d("Connect SDK Socket", " " + key + " = " + ssdpPacket.data.get(key));
// }
// Log.d("Connect SDK Socket", "__________________________________________");
// }
// });
// End Debugging stuff
if (ssdpPacket == null || ssdpPacket.getData().size() == 0 || ssdpPacket.getType() == null)
return;
String serviceFilter = ssdpPacket.getData().get(ssdpPacket.getType().equals(SSDPClient.NOTIFY) ? "NT" : "ST");
if (serviceFilter == null || SSDPClient.MSEARCH.equals(ssdpPacket.getType()) || !isSearchingForFilter(serviceFilter))
return;
String usnKey = ssdpPacket.getData().get("USN");
if (usnKey == null || usnKey.length() == 0)
return;
Matcher m = uuidReg.matcher(usnKey);
if (!m.find())
return;
String uuid = m.group();
if (SSDPClient.BYEBYE.equals(ssdpPacket.getData().get("NTS"))) {
final ServiceDescription service = foundServices.get(uuid);
if (service != null) {
foundServices.remove(uuid);
notifyListenersOfLostService(service);
}
} else {
String location = ssdpPacket.getData().get("LOCATION");
if (location == null || location.length() == 0)
return;
ServiceDescription foundService = foundServices.get(uuid);
ServiceDescription discoverdService = discoveredServices.get(uuid);
boolean isNew = foundService == null && discoverdService == null;
if (isNew) {
foundService = new ServiceDescription();
foundService.setUUID(uuid);
foundService.setServiceFilter(serviceFilter);
foundService.setIpAddress(ssdpPacket.getDatagramPacket().getAddress().getHostAddress());
foundService.setPort(3001);
discoveredServices.put(uuid, foundService);
getLocationData(location, uuid, serviceFilter);
}
if (foundService != null)
foundService.setLastDetection(new Date().getTime());
}
}
public void getLocationData(final String location, final String uuid, final String serviceFilter) {
try {
getLocationData(new URL(location), uuid, serviceFilter);
} catch (IOException e) {
e.printStackTrace();
}
}
public void getLocationData(final URL location, final String uuid, final String serviceFilter) {
Util.runInBackground(new Runnable() {
@Override
public void run() {
SSDPDevice device = null;
try {
device = new SSDPDevice(location, serviceFilter);
} catch (IOException e) {
e.printStackTrace();
} catch (ParserConfigurationException e) {
e.printStackTrace();
} catch (SAXException e) {
e.printStackTrace();
}
if (device != null) {
device.UUID = uuid;
boolean hasServices = containsServicesWithFilter(device, serviceFilter);
if (hasServices) {
final ServiceDescription service = discoveredServices.get(uuid);
if (service != null) {
service.setServiceFilter(serviceFilter);
service.setFriendlyName(device.friendlyName);
service.setModelName(device.modelName);
service.setModelNumber(device.modelNumber);
service.setModelDescription(device.modelDescription);
service.setManufacturer(device.manufacturer);
service.setApplicationURL(device.applicationURL);
service.setServiceList(device.serviceList);
service.setResponseHeaders(device.headers);
service.setLocationXML(device.locationXML);
service.setServiceURI(device.serviceURI);
service.setPort(device.port);
foundServices.put(uuid, service);
notifyListenersOfNewService(service);
}
}
}
discoveredServices.remove(uuid);
}
}, true);
}
private void notifyListenersOfNewService(ServiceDescription service) {
List<String> serviceIds = serviceIdsForFilter(service.getServiceFilter());
for (String serviceId : serviceIds) {
ServiceDescription _newService = service.clone();
_newService.setServiceID(serviceId);
final ServiceDescription newService = _newService;
Util.runOnUI(new Runnable() {
@Override
public void run() {
for (DiscoveryProviderListener listener : serviceListeners) {
listener.onServiceAdded(SSDPDiscoveryProvider.this, newService);
}
}
});
}
}
private void notifyListenersOfLostService(ServiceDescription service) {
List<String> serviceIds = serviceIdsForFilter(service.getServiceFilter());
for (String serviceId : serviceIds) {
ServiceDescription _newService = service.clone();
_newService.setServiceID(serviceId);
final ServiceDescription newService = _newService;
Util.runOnUI(new Runnable() {
@Override
public void run() {
for (DiscoveryProviderListener listener : serviceListeners) {
listener.onServiceRemoved(SSDPDiscoveryProvider.this, newService);
}
}
});
}
}
public List<String> serviceIdsForFilter(String filter) {
ArrayList<String> serviceIds = new ArrayList<String>();
for (DiscoveryFilter serviceFilter : serviceFilters) {
String ssdpFilter = serviceFilter.getServiceFilter();
if (ssdpFilter.equals(filter)) {
String serviceId = serviceFilter.getServiceId();
if (serviceId != null)
serviceIds.add(serviceId);
}
}
return serviceIds;
}
public boolean isSearchingForFilter(String filter) {
for (DiscoveryFilter serviceFilter : serviceFilters) {
String ssdpFilter = serviceFilter.getServiceFilter();
if (ssdpFilter.equals(filter))
return true;
}
return false;
}
public boolean containsServicesWithFilter(SSDPDevice device, String filter) {
// List<String> servicesRequired = new ArrayList<String>();
//
// for (JSONObject serviceFilter : serviceFilters) {
// }
// TODO Implement this method. Not sure why needs to happen since there are now required services.
return true;
}
@Override
public void addListener(DiscoveryProviderListener listener) {
serviceListeners.add(listener);
}
@Override
public void removeListener(DiscoveryProviderListener listener) {
serviceListeners.remove(listener);
}
}