/*
* Copyright 2011-2014 the original author or authors.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package devcoin.wallet;
import android.content.ContentProvider;
import android.content.ContentValues;
import android.database.Cursor;
import android.database.MatrixCursor;
import android.net.Uri;
import android.provider.BaseColumns;
import android.text.format.DateUtils;
import com.google.devcoin.core.Utils;
import devcoin.wallet.util.GenericUtils;
import devcoin.wallet.util.Io;
import org.json.JSONObject;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.annotation.CheckForNull;
import javax.annotation.Nonnull;
import java.io.BufferedInputStream;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.Reader;
import java.math.BigInteger;
import java.net.HttpURLConnection;
import java.net.MalformedURLException;
import java.net.URL;
import java.util.*;
/**
* @author Andreas Schildbach
*/
public class ExchangeRatesProvider extends ContentProvider
{
public static class ExchangeRate
{
public ExchangeRate(@Nonnull final String currencyCode, @Nonnull final BigInteger rate, @Nonnull final String source)
{
this.currencyCode = currencyCode;
this.rate = rate;
this.source = source;
}
public final String currencyCode;
public final BigInteger rate;
public final String source;
@Override
public String toString()
{
return getClass().getSimpleName() + '[' + currencyCode + ':' + GenericUtils.formatValue(rate, Constants.BTC_MAX_PRECISION, 0) + ']';
}
}
public static final String KEY_CURRENCY_CODE = "currency_code";
private static final String KEY_RATE = "rate";
private static final String KEY_SOURCE = "source";
@CheckForNull
private Map<String, ExchangeRate> exchangeRates = null;
private Map<String, ExchangeRate> exchangeRatesDVC = null;
private long lastUpdated = 0;
private static final URL BITCOINAVERAGE_URL;
private static final String[] BITCOINAVERAGE_FIELDS = new String[] { "24h_avg" };
private static final URL BITCOINCHARTS_URL;
private static final String[] BITCOINCHARTS_FIELDS = new String[] { "24h", "7d", "30d" };
private static final URL BLOCKCHAININFO_URL;
private static final String[] BLOCKCHAININFO_FIELDS = new String[] { "15m" };
private static final URL VIRCUREX_URL;
private static final String[] VIRCUREX_FIELDS = new String[] { "last_trade" };
private static final URL CRYPTOTRADE_URL;
private static final String[] CRYPTOTRADE_FIELDS = new String[] { "last" };
// https://bitmarket.eu/api/ticker
static
{
try
{
BITCOINAVERAGE_URL = new URL("https://api.bitcoinaverage.com/ticker/all");
BITCOINCHARTS_URL = new URL("http://api.bitcoincharts.com/v1/weighted_prices.json");
BLOCKCHAININFO_URL = new URL("https://blockchain.info/ticker");
VIRCUREX_URL = new URL("https://vircurex.com/api/get_info_for_1_currency.json?base=DVC&alt=BTC");
CRYPTOTRADE_URL = new URL("https://crypto-trade.com/api/1/ticker/dvc_btc");
}
catch (final MalformedURLException x)
{
throw new RuntimeException(x); // cannot happen
}
}
private static final long UPDATE_FREQ_MS = 10 * DateUtils.MINUTE_IN_MILLIS;
private static final Logger log = LoggerFactory.getLogger(ExchangeRatesProvider.class);
@Override
public boolean onCreate()
{
return true;
}
public static Uri contentUri(@Nonnull final String packageName)
{
return Uri.parse("content://" + packageName + '.' + "exchange_rates");
}
@Override
public Cursor query(final Uri uri, final String[] projection, final String selection, final String[] selectionArgs, final String sortOrder)
{
final long now = System.currentTimeMillis();
if (exchangeRates == null || now - lastUpdated > UPDATE_FREQ_MS)
{
Map<String, ExchangeRate> newExchangeRates = null;
Map<String, ExchangeRate> newExchangeRatesDVC = null;
newExchangeRatesDVC = requestVircurexExchangeRates(null, VIRCUREX_URL, VIRCUREX_FIELDS);
if(newExchangeRatesDVC == null || newExchangeRatesDVC.values().toArray().length <= 0)
newExchangeRatesDVC = requestExchangeRates(null, CRYPTOTRADE_URL, CRYPTOTRADE_FIELDS);
if(newExchangeRatesDVC != null && newExchangeRatesDVC.values().toArray().length > 0)
{
exchangeRatesDVC = newExchangeRatesDVC;
ExchangeRate DVCRate = (ExchangeRate)(exchangeRatesDVC.values().toArray())[0];
if(DVCRate != null)
{
newExchangeRates = requestExchangeRates(DVCRate, BITCOINAVERAGE_URL, BITCOINAVERAGE_FIELDS);
if (newExchangeRates == null)
newExchangeRates = requestExchangeRates(DVCRate, BITCOINCHARTS_URL, BITCOINCHARTS_FIELDS);
if (newExchangeRates == null)
newExchangeRates = requestExchangeRates(DVCRate, BLOCKCHAININFO_URL, BLOCKCHAININFO_FIELDS);
}
}
if (newExchangeRates != null)
{
ExchangeRate DVCRate = (ExchangeRate)(exchangeRatesDVC.values().toArray())[0];
exchangeRates = newExchangeRates;
exchangeRates.put("BTC", DVCRate);
lastUpdated = now;
}
}
if (exchangeRates == null)
return null;
final MatrixCursor cursor = new MatrixCursor(new String[] { BaseColumns._ID, KEY_CURRENCY_CODE, KEY_RATE, KEY_SOURCE });
if (selection == null)
{
for (final Map.Entry<String, ExchangeRate> entry : exchangeRates.entrySet())
{
final ExchangeRate rate = entry.getValue();
cursor.newRow().add(rate.currencyCode.hashCode()).add(rate.currencyCode).add(rate.rate.longValue()).add(rate.source);
}
}
else if (selection.equals(KEY_CURRENCY_CODE))
{
final String selectedCode = selectionArgs[0];
ExchangeRate rate = selectedCode != null ? exchangeRates.get(selectedCode) : null;
if (rate == null)
{
final String defaultCode = defaultCurrencyCode();
rate = defaultCode != null ? exchangeRates.get(defaultCode) : null;
if (rate == null)
{
rate = exchangeRates.get(Constants.DEFAULT_EXCHANGE_CURRENCY);
if (rate == null)
return null;
}
}
cursor.newRow().add(rate.currencyCode.hashCode()).add(rate.currencyCode).add(rate.rate.longValue()).add(rate.source);
}
return cursor;
}
private String defaultCurrencyCode()
{
try
{
return Currency.getInstance(Locale.getDefault()).getCurrencyCode();
}
catch (final IllegalArgumentException x)
{
return null;
}
}
public static ExchangeRate getExchangeRate(@Nonnull final Cursor cursor)
{
final String currencyCode = cursor.getString(cursor.getColumnIndexOrThrow(ExchangeRatesProvider.KEY_CURRENCY_CODE));
final BigInteger rate = BigInteger.valueOf(cursor.getLong(cursor.getColumnIndexOrThrow(ExchangeRatesProvider.KEY_RATE)));
final String source = cursor.getString(cursor.getColumnIndexOrThrow(ExchangeRatesProvider.KEY_SOURCE));
return new ExchangeRate(currencyCode, rate, source);
}
@Override
public Uri insert(final Uri uri, final ContentValues values)
{
throw new UnsupportedOperationException();
}
@Override
public int update(final Uri uri, final ContentValues values, final String selection, final String[] selectionArgs)
{
throw new UnsupportedOperationException();
}
@Override
public int delete(final Uri uri, final String selection, final String[] selectionArgs)
{
throw new UnsupportedOperationException();
}
@Override
public String getType(final Uri uri)
{
throw new UnsupportedOperationException();
}
private static Map<String, ExchangeRate> requestExchangeRates( ExchangeRate exchangeRatesBaseDVC, final URL url, final String... fields)
{
final long start = System.currentTimeMillis();
HttpURLConnection connection = null;
Reader reader = null;
try
{
connection = (HttpURLConnection) url.openConnection();
connection.setConnectTimeout(Constants.HTTP_TIMEOUT_MS);
connection.setReadTimeout(Constants.HTTP_TIMEOUT_MS);
connection.connect();
final int responseCode = connection.getResponseCode();
if (responseCode == HttpURLConnection.HTTP_OK)
{
reader = new InputStreamReader(new BufferedInputStream(connection.getInputStream(), 1024), Constants.UTF_8);
final StringBuilder content = new StringBuilder();
Io.copy(reader, content);
final Map<String, ExchangeRate> rates = new TreeMap<String, ExchangeRate>();
final JSONObject head = new JSONObject(content.toString());
for (final Iterator<String> i = head.keys(); i.hasNext();)
{
String currencyCode = i.next();
boolean found = false;
if (!"timestamp".equals(currencyCode) && !"status".equals(currencyCode) )
{
final JSONObject o = head.getJSONObject(currencyCode);
for (final String field : fields)
{
final String rateStr = o.optString(field, null);
if (rateStr != null)
{
try
{
BigInteger rate = GenericUtils.toNanoCoins(rateStr, 0);
if (rate.signum() > 0)
{
if(exchangeRatesBaseDVC != null)
{
long value = (long)(rate.floatValue() * (exchangeRatesBaseDVC.rate.floatValue()/ Utils.COIN.floatValue()));
rate = BigInteger.valueOf(value);
}
else
{
currencyCode = "BTC";
}
rates.put(currencyCode, new ExchangeRate(currencyCode, rate, url.getHost()));
break;
}
}
catch (final ArithmeticException x)
{
log.warn("problem fetching exchange rate: " + currencyCode, x);
}
}
}
}
}
log.info("fetched exchange rates from " + url + ", took " + (System.currentTimeMillis() - start) + " ms");
return rates;
}
else
{
log.warn("http status " + responseCode + " when fetching " + url);
}
}
catch (final Exception x)
{
log.warn("problem fetching exchange rates", x);
}
finally
{
if (reader != null)
{
try
{
reader.close();
}
catch (final IOException x)
{
// swallow
}
}
if (connection != null)
connection.disconnect();
}
return null;
}
private static Map<String, ExchangeRate> requestVircurexExchangeRates( ExchangeRate exchangeRatesBaseDVC, final URL url, final String... fields)
{
final long start = System.currentTimeMillis();
HttpURLConnection connection = null;
Reader reader = null;
try
{
connection = (HttpURLConnection) url.openConnection();
connection.setConnectTimeout(Constants.HTTP_TIMEOUT_MS);
connection.setReadTimeout(Constants.HTTP_TIMEOUT_MS);
connection.connect();
final int responseCode = connection.getResponseCode();
if (responseCode == HttpURLConnection.HTTP_OK)
{
reader = new InputStreamReader(new BufferedInputStream(connection.getInputStream(), 1024), Constants.UTF_8);
final StringBuilder content = new StringBuilder();
Io.copy(reader, content);
final Map<String, ExchangeRate> rates = new TreeMap<String, ExchangeRate>();
final JSONObject head = new JSONObject(content.toString());
for (final Iterator<String> i = head.keys(); i.hasNext();)
{
String currencyCode = i.next();
boolean found = false;
for (final String field : fields)
{
final String rateStr = head.optString(field, null);
if (rateStr != null)
{
try
{
BigInteger rate = GenericUtils.toNanoCoins(rateStr, 0);
if (rate.signum() > 0)
{
if(exchangeRatesBaseDVC != null)
{
long value = (long)(rate.floatValue() * (exchangeRatesBaseDVC.rate.floatValue()/ Utils.COIN.floatValue()));
rate = BigInteger.valueOf(value);
}
else
{
currencyCode = "BTC";
}
rates.put(currencyCode, new ExchangeRate(currencyCode, rate, url.getHost()));
break;
}
}
catch (final ArithmeticException x)
{
log.warn("problem fetching exchange rate: " + currencyCode, x);
}
}
}
}
log.info("fetched exchange rates from " + url + ", took " + (System.currentTimeMillis() - start) + " ms");
return rates;
}
else
{
log.warn("http status " + responseCode + " when fetching " + url);
}
}
catch (final Exception x)
{
log.warn("problem fetching exchange rates", x);
}
finally
{
if (reader != null)
{
try
{
reader.close();
}
catch (final IOException x)
{
// swallow
}
}
if (connection != null)
connection.disconnect();
}
return null;
}
}