package com.papagiannis.tuberun.cyclehire; import java.io.StringReader; import java.util.ArrayList; import java.util.Date; import java.util.concurrent.atomic.AtomicBoolean; import javax.xml.parsers.DocumentBuilder; import javax.xml.parsers.DocumentBuilderFactory; import org.w3c.dom.Document; import org.w3c.dom.Element; import org.w3c.dom.Node; import org.w3c.dom.NodeList; import org.xml.sax.InputSource; import android.location.Location; import android.os.AsyncTask; import android.os.AsyncTask.Status; import android.util.Log; import com.papagiannis.tuberun.Station; import com.papagiannis.tuberun.fetchers.HttpCallback; import com.papagiannis.tuberun.fetchers.NearbyFetcher; import com.papagiannis.tuberun.fetchers.RequestTask; public class StationsCycleHireFetcher extends NearbyFetcher<CycleHireStation> { private static final long serialVersionUID = 1L; private static final String URL = "http://www.tfl.gov.uk/tfl/syndication/feeds/cycle-hire/livecyclehireupdates.xml"; //This serves as a quick flag to allow only a single active request at a time private final AtomicBoolean isFirst = new AtomicBoolean(true); //Since the state of this fetcher may be accessed by multiple subscribers while a request is active //(so the state may be accessed by the request as well), //mutating operations lock on *this* to prevent concurrent edits. private final StationsCycleHireFetcher self = this; //THIS IS THE STATE OF THE FETCHER private Location userLocation; private String errors; // This fetcher uses 3 ASyncTasks for async processing private RequestTask requestTask; //TASK 1: first fetch the data private XMLDeserialiserTask deserialiserTask; //TASK 2: then deserialize it private GetNearbyStationsTask nearbyStationsTask; //TASK 3: then calculate the nearest stations // These are the results private static Date lastUpdate = new Date(0); private static ArrayList<CycleHireStation> all_stations = new ArrayList<CycleHireStation>(); //FETCHER STATE ENDS HERE public StationsCycleHireFetcher() { super(); } @Override public void update() { setErrors(""); boolean first = isFirst.compareAndSet(true, false); if (!first) { // somebody else is doing the job for me return; } if (isRecent()) { calculateNearestStations(); } else { fetchCurrentStations(); } } private synchronized boolean isRecent() { Date now = new Date(); return now.after(lastUpdate) && now.getTime() - lastUpdate.getTime() <= (3 * 60 * 1000); } /* * TASK 1: This issues the HTTP requsest */ private synchronized void fetchCurrentStations() { requestTask = new RequestTask(new HttpCallback() { public void onReturn(String s) { //The RequestTask checks isCancelled before invoking this method handleHTTPResponse(s); } }); requestTask.execute(URL); } private void handleHTTPResponse(String response) { try { if (response == null || response.equals("")) throw new Exception( "The TFL server did not respond to your request (3)"); synchronized (self) { deserialiserTask=new XMLDeserialiserTask(); deserialiserTask.execute(response); } } catch (Exception e) { setErrors(getErrors() + e.getMessage()); Log.w(getClass().toString(), e); isFirst.set(true); notifyClients(); } } // TASK 2: Parse the XML response private class XMLDeserialiserTask extends AsyncTask<String, Integer, ArrayList<CycleHireStation>> { @Override protected ArrayList<CycleHireStation> doInBackground( String... params) { ArrayList<CycleHireStation> res; try { res = parseXMLResponse(params[0]); } catch (Exception e) { Log.w("CycleHireFetcher", e); res = new ArrayList<CycleHireStation>(); } return res; } private Date resultDate; private ArrayList<CycleHireStation> parseXMLResponse( String response) { ArrayList<CycleHireStation> result = new ArrayList<CycleHireStation>(); resultDate = new Date(0); if (response == null || response.equals("")) return result; try { DocumentBuilderFactory factory = DocumentBuilderFactory .newInstance(); DocumentBuilder builder = factory.newDocumentBuilder(); Document dom = builder.parse(new InputSource( new StringReader(response))); Element root = dom.getDocumentElement(); String age = root.getAttribute("lastUpdate"); if (age == null) return result; resultDate = new Date(Long.parseLong(age)); NodeList stationsList = root.getChildNodes(); if (stationsList != null && stationsList.getLength() > 0) { for (int i = 0; i < stationsList.getLength(); i++) { Node station = stationsList.item(i); CycleHireStation csStation = new CycleHireStation(); NodeList stationDetails = station .getChildNodes(); try { for (int j = 0; j < stationDetails .getLength(); j++) { Node detail = stationDetails.item(j); if (detail == null) break; String name = detail.getNodeName(); NodeList children = detail .getChildNodes(); if (children == null || children.getLength() == 0) continue; String value = children.item(0) .getNodeValue(); if (name == null || value == null) continue; if (name.equals("id")) csStation.setId(Integer .parseInt(value)); else if (name.equals("name")) csStation.setName(value); else if (name.equals("lat")) csStation.setLatitude(Double .parseDouble(value)); else if (name.equals("long")) csStation.setLongtitude(Double .parseDouble(value)); else if (name.equals("installed")) csStation.setInstalled(Boolean .parseBoolean(value)); else if (name.equals("locked")) csStation.setLocked(Boolean .parseBoolean(value)); else if (name.equals("nbBikes")) csStation .setnAvailableBikes(Integer .parseInt(value)); else if (name.equals("nbEmptyDocks")) csStation.setnEmptyDocks(Integer .parseInt(value)); else if (name.equals("nbDocks")) csStation.setnTotalDocks(Integer .parseInt(value)); } } catch (Exception e) { Log.w("CycleHireFetcher", e); } if (csStation.isValid()) result.add(csStation); } } } catch (Exception e) { Log.w("CycleHireFetcher", e); } if (result.size()==0) resultDate = new Date(0); return result; } protected void onPostExecute(ArrayList<CycleHireStation> result) { if (isCancelled()) return; synchronized (self) { lastUpdate.setTime(resultDate.getTime()); all_stations = result; calculateNearestStations(); } } } /* * TASK 3: This calculates the nearest stations asynchronously */ private void calculateNearestStations() { nearbyStationsTask = new GetNearbyStationsTask(); nearbyStationsTask.execute(userLocation); } private class GetNearbyStationsTask extends AsyncTask<Location, Integer, ArrayList<? extends Station>> { ArrayList<CycleHireStation> result = new ArrayList<CycleHireStation>(); @Override protected ArrayList<? extends Station> doInBackground(Location... at) { synchronized (self) { return getNearbyStations(userLocation, all_stations); } } @Override protected void onPostExecute(ArrayList<? extends Station> res) { if (isCancelled()) return; synchronized (self) { result = new ArrayList<CycleHireStation>(); for (Station s : res) result.add((CycleHireStation) s); } isFirst.set(true); notifyClients(); } public ArrayList<CycleHireStation> getResult() { return result; } } @Override public synchronized Date getUpdateTime() { return new Date(lastUpdate.getTime()); } public synchronized void setLocation(Location l) { if (nearbyStationsTask!=null && nearbyStationsTask.getStatus()==Status.RUNNING) { //Abort TASK3 and make sure that the next update() will ignore TASKs 1&2. lastUpdate=new Date(); //NOW! isFirst.set(true); nearbyStationsTask.cancel(true); } //If TASK 1 or 2 execute, then there is no need to cancel anything //because the existing fetcher will get the job done //The new update() will observe isFirst==false and not proceed. //but TASK 2 will pick up the new location before launching TASK 3. this.userLocation = l; } public synchronized ArrayList<CycleHireStation> getResult() { return nearbyStationsTask.getResult(); } public synchronized void abort() { lastUpdate=new Date(0); isFirst.set(true); if (nearbyStationsTask != null) nearbyStationsTask.cancel(true); if (requestTask != null) requestTask.cancel(true); if (deserialiserTask!=null) deserialiserTask.cancel(true); } public synchronized String getErrors() { return errors; } public synchronized void setErrors(String errors) { this.errors = errors; } }