/* * Copyright (C) 2012-2016 The Android Money Manager Ex Project Team * * 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 com.money.manager.ex.investment.yql; import android.content.Context; import com.google.gson.JsonArray; import com.google.gson.JsonElement; import com.google.gson.JsonNull; import com.google.gson.JsonObject; import com.money.manager.ex.R; import com.money.manager.ex.core.UIHelper; import com.money.manager.ex.log.ExceptionHandler; import com.money.manager.ex.core.NumericHelper; import com.money.manager.ex.investment.ISecurityPriceUpdater; import com.money.manager.ex.investment.PriceUpdaterBase; import com.money.manager.ex.investment.SecurityPriceModel; import com.money.manager.ex.investment.events.PriceDownloadedEvent; import com.money.manager.ex.utils.MmxDate; import com.money.manager.ex.utils.MmxDateTimeUtils; import org.greenrobot.eventbus.EventBus; import java.util.ArrayList; import java.util.Date; import java.util.List; import info.javaperformance.money.Money; import info.javaperformance.money.MoneyFactory; import retrofit2.Call; import retrofit2.Callback; import retrofit2.Response; import retrofit2.Retrofit; import retrofit2.converter.gson.GsonConverterFactory; import timber.log.Timber; /** * Updates security prices from Yahoo Finance using YQL. Using Retrofit for network access. */ public class YqlSecurityPriceUpdaterRetrofit extends PriceUpdaterBase implements ISecurityPriceUpdater { /** * * @param context Executing context */ public YqlSecurityPriceUpdaterRetrofit(Context context) { super(context); } // https://query.yahooapis.com/v1/public/yql // ?q=... url escaped // &format=json // &diagnostics=true // &env=store%3A%2F%2Fdatatables.org%2Falltableswithkeys // &callback= /** * Update prices for all the symbols in the list. */ public void downloadPrices(List<String> symbols) { if (symbols == null) return; int items = symbols.size(); if (items == 0) return; showProgressDialog(items); YqlQueryGenerator queryGenerator = new YqlQueryGenerator(); String query = queryGenerator.getQueryFor(symbols); IYqlService yql = getYqlService(); // Async response handler. Callback<JsonElement> callback = new Callback<JsonElement>() { @Override public void onResponse(Call<JsonElement> call, Response<JsonElement> response) { onContentDownloaded(response.body()); } @Override public void onFailure(Call<JsonElement> call, Throwable t) { Timber.e(t, "fetching price"); closeProgressDialog(); } }; try { // This would be the synchronous call. // prices = yql.getPrices(query).execute().body(); yql.getPrices(query).enqueue(callback); } catch (Exception e) { Timber.e(e, "fetching prices"); } } /** * Called when the file is downloaded and the contents read. * Here we have all the prices. */ public void onContentDownloaded(JsonElement response) { UIHelper uiHelper = new UIHelper(getContext()); if (response == null) { uiHelper.showToast(R.string.error_updating_rates); closeProgressDialog(); return; } // parse Json results List<SecurityPriceModel> pricesList = getPricesFromJson(response.getAsJsonObject()); if (pricesList == null) { uiHelper.showToast(R.string.error_no_price_found_for_symbol); } else { // Send the parsed price data to the listener(s). for (SecurityPriceModel model : pricesList) { // Notify the caller. EventBus.getDefault().post(new PriceDownloadedEvent(model.symbol, model.price, model.date)); } } closeProgressDialog(); // Notify user that all the prices have been downloaded. uiHelper.showToast(R.string.download_complete); } private List<SecurityPriceModel> getPricesFromJson(JsonObject root) { ArrayList<SecurityPriceModel> result = new ArrayList<>(); // check whether there is only one item or more JsonElement results = root.get("query").getAsJsonObject() .get("results"); if (results == null) return null; JsonObject resultsJson = results.getAsJsonObject(); if (resultsJson == null) return null; JsonElement quoteElement = resultsJson.get("quote"); if (quoteElement instanceof JsonArray) { JsonArray quotes = quoteElement.getAsJsonArray(); for (int i = 0; i < quotes.size(); i++) { JsonObject quote = quotes.get(i).getAsJsonObject(); // process individual quote SecurityPriceModel priceModel = getSecurityPriceFor(quote); if (priceModel == null) continue; result.add(priceModel); } } else { // Single quote JsonObject quote = quoteElement.getAsJsonObject(); SecurityPriceModel priceModel = getSecurityPriceFor(quote); if (priceModel != null) { result.add(priceModel); } } return result; } private SecurityPriceModel getSecurityPriceFor(JsonObject quote) { SecurityPriceModel priceModel = new SecurityPriceModel(); priceModel.symbol = quote.get("symbol").getAsString(); ExceptionHandler handler = new ExceptionHandler(getContext(), this); // Price JsonElement priceElement = quote.get("LastTradePriceOnly"); if (priceElement == JsonNull.INSTANCE) { handler.showMessage(getContext().getString(R.string.error_no_price_found_for_symbol) + " " + priceModel.symbol); return null; } String priceString = priceElement.getAsString(); if (!NumericHelper.isNumeric(priceString)) { handler.showMessage(getContext().getString(R.string.error_no_price_found_for_symbol) + " " + priceModel.symbol); return null; } priceModel.price = readPrice(priceString, quote); // Date Date date = new MmxDate().toDate(); JsonElement dateElement = quote.get("LastTradeDate"); if (dateElement != JsonNull.INSTANCE) { // Sometimes the date is not available. For now we will use today's date. date = new MmxDate(dateElement.getAsString(), "MM/dd/yyyy").toDate(); } priceModel.date = date; return priceModel; } public IYqlService getYqlService() { String BASE_URL = "https://query.yahooapis.com"; Retrofit retrofit = new Retrofit.Builder() .addConverterFactory(GsonConverterFactory.create()) .baseUrl(BASE_URL) .build(); return retrofit.create(IYqlService.class); } private Money readPrice(String priceString, JsonObject quote) { UIHelper ui = new UIHelper(getContext()); Money price = MoneyFactory.fromString(priceString); /** LSE stocks are expressed in GBp (pence), not Pounds. From stockspanel.cpp, line 785: if (StockQuoteCurrency == "GBp") dPrice = dPrice / 100; */ JsonElement currencyElement = quote.get("Currency"); // validation if (currencyElement == null || currencyElement.isJsonNull()) { ui.showToast(R.string.error_downloading_symbol); return MoneyFactory.fromDouble(0); } String currency; try { currency = currencyElement.getAsString(); } catch (UnsupportedOperationException ex) { Timber.e(ex, "reading currency from downloaded price"); currency = ""; } if (currency.equals("GBp")) { price = price.divide(100, MoneyFactory.MAX_ALLOWED_PRECISION); } return price; } }