package com.lemoulinstudio.bikefriend;
import android.content.SharedPreferences;
import android.os.AsyncTask;
import android.util.Log;
import com.google.android.gms.maps.model.LatLngBounds;
import com.j256.ormlite.dao.Dao;
import com.j256.ormlite.misc.TransactionManager;
import com.j256.ormlite.stmt.DeleteBuilder;
import com.lemoulinstudio.bikefriend.db.BikeStation;
import com.lemoulinstudio.bikefriend.db.DataSourceEnum;
import com.lemoulinstudio.bikefriend.parser.ParsingException;
import com.lemoulinstudio.bikefriend.preference.BikefriendPreferences_;
import java.io.IOException;
import java.io.InputStream;
import java.net.HttpURLConnection;
import java.net.URL;
import java.net.URLConnection;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Date;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.concurrent.Callable;
public class BikeStationProviderImpl implements BikeStationProvider,
SharedPreferences.OnSharedPreferenceChangeListener {
protected final DataSourceEnum dataSource;
protected final Dao<BikeStation, String> bikeStationDao;
protected final Collection<BikeStation> bikeStations;
protected LatLngBounds bounds;
protected Date lastUpdateDate;
protected BikefriendPreferences_ preferences;
protected long autoRefreshMinPeriod;
public BikeStationProviderImpl(
DataSourceEnum dataSource,
BikefriendPreferences_ preferences,
Dao<BikeStation, String> bikeStationDao) {
this.dataSource = dataSource;
this.bikeStationDao = bikeStationDao;
this.bikeStations = new ArrayList<BikeStation>();
this.preferences = preferences;
this.autoRefreshMinPeriod = Long.parseLong(preferences.autoRefreshMinPeriod().get());
preferences.getSharedPreferences().registerOnSharedPreferenceChangeListener(this);
this.lastUpdateDate = new Date(0);
new LoadStationsFromDbAsyncTask().execute(this);
}
@Override
public void onSharedPreferenceChanged(SharedPreferences sharedPreferences, String key) {
if (key.equals(preferences.autoRefreshMinPeriod().key())) {
autoRefreshMinPeriod = Long.parseLong(preferences.autoRefreshMinPeriod().get());
//Log.i(BikefriendApplication.TAG, "autoRefreshMinPeriod = " + autoRefreshMinPeriod);
}
}
private void updateMemFromDb() throws SQLException {
Log.d(BikefriendApplication.TAG, dataSource.name() + " updateMemFromDb()");
List<BikeStation> dbBikeStations = bikeStationDao.queryForEq("dataSource", dataSource);
synchronized (bikeStations) {
updateMemFrom(dbBikeStations, true);
}
}
private void updateDbFromMem() throws SQLException {
Log.d(BikefriendApplication.TAG, dataSource.name() + " updateDbFromMem()");
TransactionManager.callInTransaction(bikeStationDao.getConnectionSource(),
new Callable<Void>() {
@Override
public Void call() throws Exception {
// Wipe the previous stations of this data source from the DB.
DeleteBuilder<BikeStation, String> deleteBuilder = bikeStationDao.deleteBuilder();
deleteBuilder.where().eq("dataSource", dataSource);
deleteBuilder.delete();
// Write the stations from the memory to the DB.
synchronized (bikeStations) {
for (BikeStation bikeStation : bikeStations) {
bikeStationDao.createOrUpdate(bikeStation);
}
}
return null;
}
});
}
private void updateMemFromServers(Collection<BikeStation> otherStations) {
updateMemFrom(otherStations, false);
}
private void updateMemFrom(Collection<BikeStation> otherStations, boolean isFromDb) {
Log.d(BikefriendApplication.TAG, dataSource.name() + " updateMemFromServers()");
for (BikeStation otherStation : otherStations) {
BikeStation memStation = findBikeStationFromId(otherStation.id, bikeStations);
if (memStation == null) {
// This is a new station, we add it to the list.
bikeStations.add(otherStation);
} else {
// We update the state of the memStation from the attributes of the netStation.
memStation.updateFrom(otherStation, isFromDb);
}
}
for (BikeStation memStation : bikeStations) {
BikeStation otherStation = findBikeStationFromId(memStation.id, otherStations);
if (otherStation == null) {
// This station is no longer there, we can delete it.
bikeStations.remove(memStation);
}
}
}
private BikeStation findBikeStationFromId(String id, Collection<BikeStation> stations) {
if (id == null) {
return null;
}
for (BikeStation station : stations) {
if (id.equals(station.id)) {
return station;
}
}
return null;
}
protected Set<BikeStationListener> listeners = new HashSet<BikeStationListener>();
@Override
public void addListener(BikeStationListener listener) {
listeners.add(listener);
}
@Override
public void removeListener(BikeStationListener listener) {
listeners.remove(listener);
}
private void fireOnServerNotReachable() {
for (BikeStationListener listener : listeners) {
listener.onServerNotReachable(this);
}
}
private void fireOnParseError() {
for (BikeStationListener listener : listeners) {
listener.onParseError(this);
}
}
private void fireOnBikeStationUpdated() {
for (BikeStationListener listener : listeners) {
listener.onBikeStationUpdated(this);
}
}
@Override
public DataSourceEnum getDataSourceEnum() {
return dataSource;
}
@Override
public LatLngBounds getBounds() {
return bounds == null ? dataSource.bounds : bounds;
}
@Override
public Date getLastUpdateDate() {
return lastUpdateDate;
}
@Override
public void notifyStationsAreWatched() {
long now = System.currentTimeMillis();
long lastUpdate = lastUpdateDate == null ? 0 : lastUpdateDate.getTime();
long duration = now - lastUpdate;
if (duration >= autoRefreshMinPeriod && duration >= dataSource.noReloadDuration) {
//Log.d(BikefriendApplication.TAG, dataSource.name() + " duration = " + duration);
updateData();
}
}
private volatile boolean isFetchingStations;
@Override
public synchronized void updateData() {
if (!isFetchingStations) {
Log.d(BikefriendApplication.TAG, dataSource.name() + " updateData()");
isFetchingStations = true;
new DownloadStationDataAsyncTask().execute(this);
}
}
@Override
public Collection<BikeStation> getBikeStations() {
return bikeStations;
}
private class LoadStationsFromDbAsyncTask extends AsyncTask<BikeStationProvider, Void, Void> {
@Override
protected Void doInBackground(BikeStationProvider... params) {
try {
updateMemFromDb();
}
catch (SQLException e) {
Log.e(BikefriendApplication.TAG, "SQL exception", e);
}
return null;
}
@Override
protected void onPostExecute(Void arg) {
fireOnBikeStationUpdated();
}
}
private class DownloadStationDataAsyncTask extends AsyncTask<BikeStationProvider, Void, List<BikeStation>> {
private volatile boolean networkProblem;
private volatile boolean parsingProblem;
protected InputStream getDataStream() throws IOException {
URL url = dataSource.urlProvider.getUrl();
int nbRedirect = 0;
int responseCode;
HttpURLConnection connection;
while (true) {
connection = (HttpURLConnection) url.openConnection();
connection.setInstanceFollowRedirects(false); // turned off because implementation is buggy
responseCode = connection.getResponseCode();
if (responseCode >= 300 && responseCode < 400) {
nbRedirect++;
if (nbRedirect == 5) {
throw new IOException("Too many redirections (" + nbRedirect + ").");
}
String location = connection.getHeaderField("Location");
//Log.d(BikefriendApplication.TAG, "Redirection to " + location);
url = new URL(location);
connection.disconnect();
}
else {
break;
}
}
connection.connect();
return connection.getInputStream();
}
@Override
protected List<BikeStation> doInBackground(BikeStationProvider... stationProviders) {
try {
InputStream dataStream = getDataStream();
if (dataStream == null) {
throw new IOException();
}
List<BikeStation> stations = dataSource.parser.parse(dataStream);
try {
synchronized (bikeStations) {
updateMemFromServers(stations);
updateDbFromMem();
}
}
catch (SQLException e) {
Log.e(BikefriendApplication.TAG, "SQL exception", e);
}
bounds = Utils.computeBounds(bikeStations);
lastUpdateDate = new Date();
isFetchingStations = false;
return stations;
}
catch (IOException e) {
//Log.d(BikefriendApplication.TAG, "Network problem on " + dataSource.name(), e);
networkProblem = true;
}
catch (ParsingException e) {
Log.e(BikefriendApplication.TAG, "Problem while parsing " + dataSource.name() + ": " + e.getMessage(), e);
parsingProblem = true;
}
return null;
}
@Override
protected void onPostExecute(List<BikeStation> stations) {
//Log.d(BikefriendApplication.TAG, dataSource.name() + " DownloadStationDataAsyncTask.onPostExecute()");
if (networkProblem) {
fireOnServerNotReachable();
}
else if (parsingProblem) {
fireOnParseError();
}
else if (stations != null) {
fireOnBikeStationUpdated();
}
}
}
}