package com.mygeopay.wallet; /* * 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/>. */ import android.content.ContentProvider; import android.content.ContentValues; import android.content.Context; import android.database.Cursor; import android.database.MatrixCursor; import android.net.ConnectivityManager; import android.net.NetworkInfo; import android.net.Uri; import android.preference.PreferenceManager; import android.provider.BaseColumns; import com.mygeopay.core.coins.CoinID; import com.mygeopay.core.coins.CoinType; import com.mygeopay.core.coins.FiatValue; import com.mygeopay.core.coins.Value; import com.mygeopay.core.util.ExchangeRateBase; import com.mygeopay.wallet.util.NetworkUtils; import com.google.common.collect.ImmutableList; import com.squareup.okhttp.OkHttpClient; import com.squareup.okhttp.Request; import com.squareup.okhttp.Response; import org.json.JSONException; import org.json.JSONObject; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.io.IOException; import java.net.MalformedURLException; import java.net.URL; import java.util.Iterator; import java.util.List; import java.util.Map; import java.util.TreeMap; import javax.annotation.Nonnull; import javax.annotation.Nullable; /** * @author Andreas Schildbach */ public class ExchangeRatesProvider extends ContentProvider { public static class ExchangeRate { @Nonnull public final ExchangeRateBase rate; public final String currencyCodeId; @Nullable public final String source; public ExchangeRate(@Nonnull final ExchangeRateBase rate, final String currencyCodeId, @Nullable final String source) { this.rate = rate; this.currencyCodeId = currencyCodeId; this.source = source; } @Override public String toString() { return getClass().getSimpleName() + '[' + rate.value1 + " ~ " + rate.value2 + ']'; // TODO Add feature to display 0.00 value for coins with no currency return } } public static final String KEY_CURRENCY_ID = "currency_id"; private static final String KEY_RATE_COIN = "rate_coin"; private static final String KEY_RATE_FIAT = "rate_fiat"; private static final String KEY_RATE_COIN_CODE = "rate_coin_code"; private static final String KEY_RATE_FIAT_CODE = "rate_fiat_code"; private static final String KEY_SOURCE = "source"; private static final String QUERY_PARAM_OFFLINE = "offline"; private ConnectivityManager connManager; private Configuration config; private Map<String, ExchangeRate> localToCryptoRates = null; private long localToCryptoLastUpdated = 0; private String lastLocalCurrency = null; private Map<String, ExchangeRate> cryptoToLocalRates = null; private long cryptoToLocalLastUpdated = 0; private String lastCryptoCurrency = null; private static final String BASE_URL = "https://ticker.coinomi.net/simple"; private static final String TO_LOCAL_URL = BASE_URL + "/to-local/%s"; private static final String TO_CRYPTO_URL = BASE_URL + "/to-crypto/%s"; private static final String COINOMI_SOURCE = "coinomi.com"; private static final String UBERPAY_BASE_URL = "https://ticker.geodex.info/simple"; private static final String UBER_TO_LOCAL_URL = UBERPAY_BASE_URL + "/uber-local/%s"; private static final String UBER_TO_CRYPTO_URL = UBERPAY_BASE_URL + "/uber-crypto/%s"; private static final String UBERPAY_SOURCE = "geodex.info"; // TODO Add alternative exchange for altcoins private static final Logger log = LoggerFactory.getLogger(ExchangeRatesProvider.class); @Override public boolean onCreate() { final Context context = getContext(); connManager = (ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE); config = new Configuration(PreferenceManager.getDefaultSharedPreferences(context)); lastLocalCurrency = config.getCachedExchangeLocalCurrency(); if (lastLocalCurrency != null) { localToCryptoRates = parseExchangeRates( config.getCachedExchangeRatesJson(), lastLocalCurrency, true); localToCryptoLastUpdated = 0; } return true; } private static Uri.Builder contentUri(@Nonnull final String packageName, final boolean offline) { final Uri.Builder builder = Uri.parse("content://" + packageName + ".exchange_rates").buildUpon(); if (offline) builder.appendQueryParameter(QUERY_PARAM_OFFLINE, "1"); return builder; } public static Uri contentUriToLocal(@Nonnull final String packageName, @Nonnull final String coinSymbol, final boolean offline) { final Uri.Builder uri = contentUri(packageName, offline); uri.appendPath("to-local").appendPath(coinSymbol); return uri.build(); } public static Uri contentUriToCrypto(@Nonnull final String packageName, @Nonnull final String localSymbol, final boolean offline) { final Uri.Builder uri = contentUri(packageName, offline); uri.appendPath("to-crypto").appendPath(localSymbol); return uri.build(); } @Nullable public static ExchangeRate getRate(final Context context, @Nonnull final String coinSymbol, @Nonnull String localSymbol) { ExchangeRate rate = null; if (context != null) { final Uri uri = contentUriToCrypto(context.getPackageName(), localSymbol, true); final Cursor cursor = context.getContentResolver().query(uri, null, ExchangeRatesProvider.KEY_CURRENCY_ID, new String[]{coinSymbol}, null); if (cursor != null) { if (cursor.moveToFirst()) { rate = getExchangeRate(cursor); } cursor.close(); } } return rate; } public static List<ExchangeRate> getRates(final Context context, @Nonnull String localSymbol) { ImmutableList.Builder<ExchangeRate> builder = ImmutableList.builder(); if (context != null) { final Uri uri = contentUriToCrypto(context.getPackageName(), localSymbol, true); final Cursor cursor = context.getContentResolver().query(uri, null, null, new String[]{null}, null); if (cursor != null && cursor.getCount() > 0) { cursor.moveToFirst(); do { builder.add(getExchangeRate(cursor)); } while (cursor.moveToNext()); cursor.close(); } } return builder.build(); } @Override public Cursor query(final Uri uri, final String[] projection, final String selection, final String[] selectionArgs, final String sortOrder) { final long now = System.currentTimeMillis(); final List<String> pathSegments = uri.getPathSegments(); if (pathSegments.size() != 2) { throw new IllegalArgumentException("Unrecognized URI: " + uri); } final boolean offline = uri.getQueryParameter(QUERY_PARAM_OFFLINE) != null; long lastUpdated; // TODO Add rate values for other Altcoins final String symbol; final boolean isLocalToCrypto; if (pathSegments.get(0).equals("to-crypto")) { isLocalToCrypto = true; symbol = pathSegments.get(1); lastUpdated = symbol.equals(lastLocalCurrency) ? localToCryptoLastUpdated : 0; } else if (pathSegments.get(0).equals("to-local")) { isLocalToCrypto = false; symbol = pathSegments.get(1); lastUpdated = symbol.equals(lastCryptoCurrency) ? cryptoToLocalLastUpdated : 0; } else { throw new IllegalArgumentException("Unrecognized URI path: " + uri); } // TODO Add rate values for other Altcoins if (!offline && (lastUpdated == 0 || now - lastUpdated > Constants.RATE_UPDATE_FREQ_MS)) { URL url; try { if (isLocalToCrypto) { url = new URL(String.format(TO_CRYPTO_URL, symbol)); } else { url = new URL(String.format(TO_LOCAL_URL, symbol)); } } catch (final MalformedURLException x) { throw new RuntimeException(x); // Should not happen } JSONObject newExchangeRatesJson = requestExchangeRatesJson(url); Map<String, ExchangeRate> newExchangeRates = parseExchangeRates(newExchangeRatesJson, symbol, isLocalToCrypto); if (newExchangeRates != null) { if (isLocalToCrypto) { localToCryptoRates = newExchangeRates; localToCryptoLastUpdated = now; lastLocalCurrency = symbol; config.setCachedExchangeRates(lastLocalCurrency, newExchangeRatesJson); } else { cryptoToLocalRates = newExchangeRates; cryptoToLocalLastUpdated = now; lastCryptoCurrency = symbol; } } } Map<String, ExchangeRate> exchangeRates = isLocalToCrypto ? localToCryptoRates : cryptoToLocalRates; if (exchangeRates == null) return null; final MatrixCursor cursor = new MatrixCursor(new String[]{BaseColumns._ID, KEY_CURRENCY_ID, KEY_RATE_COIN, KEY_RATE_COIN_CODE, KEY_RATE_FIAT, KEY_RATE_FIAT_CODE, KEY_SOURCE}); if (selection == null) { for (final Map.Entry<String, ExchangeRate> entry : exchangeRates.entrySet()) { final ExchangeRate exchangeRate = entry.getValue(); addRow(cursor, exchangeRate); } } else if (selection.equals(KEY_CURRENCY_ID)) { final ExchangeRate exchangeRate = exchangeRates.get(selectionArgs[0]); if (exchangeRate != null) { addRow(cursor, exchangeRate); } } return cursor; } private void addRow(MatrixCursor cursor, ExchangeRate exchangeRate) { final ExchangeRateBase rate = exchangeRate.rate; final String codeId = exchangeRate.currencyCodeId; cursor.newRow().add(codeId.hashCode()).add(codeId) .add(rate.value1.value).add(rate.value1.type.getSymbol()) .add(rate.value2.value).add(rate.value2.type.getSymbol()) .add(exchangeRate.source); } public static String getCurrencyCodeId(@Nonnull final Cursor cursor) { return cursor.getString(cursor.getColumnIndexOrThrow(ExchangeRatesProvider.KEY_CURRENCY_ID)); } public static ExchangeRate getExchangeRate(@Nonnull final Cursor cursor) { final String codeId = getCurrencyCodeId(cursor); final CoinType type = CoinID.typeFromSymbol(cursor.getString(cursor.getColumnIndexOrThrow(ExchangeRatesProvider.KEY_RATE_COIN_CODE))); final Value rateCoin = Value.valueOf(type, cursor.getLong(cursor.getColumnIndexOrThrow(ExchangeRatesProvider.KEY_RATE_COIN))); final String fiatCode = cursor.getString(cursor.getColumnIndexOrThrow(ExchangeRatesProvider.KEY_RATE_FIAT_CODE)); final Value rateFiat = FiatValue.valueOf(fiatCode, cursor.getLong(cursor.getColumnIndexOrThrow(ExchangeRatesProvider.KEY_RATE_FIAT))); final String source = cursor.getString(cursor.getColumnIndexOrThrow(ExchangeRatesProvider.KEY_SOURCE)); ExchangeRateBase rate = new ExchangeRateBase(rateCoin, rateFiat); return new ExchangeRate(rate, codeId, 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(); } @Nullable private JSONObject requestExchangeRatesJson(final URL url) { // Return null if no connection final NetworkInfo activeInfo = connManager.getActiveNetworkInfo(); if (activeInfo == null || !activeInfo.isConnected()) return null; final long start = System.currentTimeMillis(); OkHttpClient client = NetworkUtils.getHttpClient(getContext().getApplicationContext()); Request request = new Request.Builder().url(url).build(); try { Response response = client.newCall(request).execute(); if (response.isSuccessful()) { log.info("fetched exchange rates from {} ({}), {} chars, took {} ms", url, System.currentTimeMillis() - start); return new JSONObject(response.body().string()); } else { log.warn("Error HTTP code '{}' when fetching exchange rates from {}", response.code(), url); } } catch (IOException e) { log.warn("Error '{}' when fetching exchange rates from {}", e.getMessage(), url); } catch (JSONException e) { log.warn("Could not parse exchange rates JSON: {}", e.getMessage()); } return null; } private Map<String, ExchangeRate> parseExchangeRates(JSONObject json, String fromSymbol, boolean isLocalToCrypto) { if (json == null) return null; final Map<String, ExchangeRate> rates = new TreeMap<String, ExchangeRate>(); try { CoinType type = isLocalToCrypto ? null : CoinID.typeFromSymbol(fromSymbol); for (final Iterator<String> i = json.keys(); i.hasNext(); ) { final String toSymbol = i.next(); // Skip extras field if (!"extras".equals(toSymbol)) { final String rateStr = json.optString(toSymbol, null); if (rateStr != null) { try { if (isLocalToCrypto) type = CoinID.typeFromSymbol(toSymbol); String localSymbol = isLocalToCrypto ? fromSymbol : toSymbol; final Value rateCoin = type.oneCoin(); final Value rateLocal = FiatValue.parse(localSymbol, rateStr); ExchangeRateBase rate = new ExchangeRateBase(rateCoin, rateLocal); rates.put(toSymbol, new ExchangeRate(rate, toSymbol, COINOMI_SOURCE)); } catch (final Exception x) { log.debug("ignoring {}/{}: {}", toSymbol, fromSymbol, x.getMessage()); } } } } } catch (Exception e) { log.warn("problem parsing exchange rates: {}", e.getMessage()); } if (rates.isEmpty()) { return null; } else { return rates; } } }