/*
* 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.morningstar;
import android.content.Context;
import android.text.TextUtils;
import com.money.manager.ex.MoneyManagerApplication;
import com.money.manager.ex.R;
import com.money.manager.ex.core.UIHelper;
import com.money.manager.ex.datalayer.StockHistoryRepositorySql;
import com.money.manager.ex.datalayer.StockRepositorySql;
import com.money.manager.ex.investment.ISecurityPriceUpdater;
import com.money.manager.ex.investment.PriceUpdaterBase;
import com.money.manager.ex.investment.events.AllPricesDownloadedEvent;
import com.money.manager.ex.investment.events.PriceDownloadedEvent;
import com.money.manager.ex.utils.MmxDate;
import com.money.manager.ex.utils.MmxDateTimeUtils;
import com.squareup.sqlbrite.BriteDatabase;
import org.apache.commons.lang3.tuple.Pair;
import org.greenrobot.eventbus.EventBus;
import org.jsoup.Jsoup;
import org.jsoup.nodes.Document;
import java.util.Date;
import java.util.List;
import javax.inject.Inject;
import dagger.Lazy;
import info.javaperformance.money.Money;
import info.javaperformance.money.MoneyFactory;
import retrofit2.Retrofit;
import retrofit2.adapter.rxjava.RxJavaCallAdapterFactory;
import retrofit2.converter.scalars.ScalarsConverterFactory;
import rx.Observable;
import rx.Subscriber;
import rx.android.schedulers.AndroidSchedulers;
import rx.functions.Action1;
import rx.functions.Func1;
import rx.schedulers.Schedulers;
import rx.subscriptions.CompositeSubscription;
import timber.log.Timber;
/**
* Quote provider: Morningstar
*/
public class MorningstarPriceUpdater
extends PriceUpdaterBase
implements ISecurityPriceUpdater {
@Inject
public MorningstarPriceUpdater(Context context) {
super(context);
MoneyManagerApplication.getApp().iocComponent.inject(this);
}
/**
* Tracks the number of records to update. Used to close progress binaryDialog when all done.
*/
private int mCounter;
private int mTotalRecords;
private CompositeSubscription compositeSubscription;
@Inject Lazy<StockRepositorySql> stockRepository;
@Inject Lazy<StockHistoryRepositorySql> stockHistoryRepository;
private SymbolConverter symbolConverter;
private IMorningstarService service;
@Override
public void downloadPrices(List<String> symbols) {
if (symbols == null || symbols.isEmpty()) return;
mTotalRecords = symbols.size();
if (mTotalRecords == 0) return;
showProgressDialog(mTotalRecords);
service = getMorningstarService();
compositeSubscription = new CompositeSubscription();
symbolConverter = new SymbolConverter();
// processSequentially(symbols);
processInParallel(symbols);
}
private void processSequentially(List<String> symbols) {
compositeSubscription.add(Observable.from(symbols)
.subscribeOn(Schedulers.io())
.map(new Func1<String, String>() {
@Override
public String call(String s) {
// get a Morningstar symbol
return symbolConverter.convert(s);
}
})
.observeOn(Schedulers.io()) // Observe the network call on IO thread!
.flatMap(new Func1<String, Observable<Pair<String, String>>>() {
@Override
public Observable<Pair<String, String>> call(final String s) {
// download the price
return service.getPrice(s)
.doOnError(new Action1<Throwable>() {
@Override
public void call(Throwable throwable) {
mCounter++;
setProgress(mCounter);
// report to the UI
Timber.e(throwable, "fetching %s", s);
}
})
.onErrorResumeNext(Observable.<String>empty())
.map(new Func1<String, Pair<String, String>>() {
@Override
public Pair<String, String> call(String price) {
return Pair.of(s, price);
}
});
}
})
.map(new Func1<Pair<String, String>, PriceDownloadedEvent>() {
@Override
public PriceDownloadedEvent call(Pair<String, String> s) {
// parse the price
return parse(s.getLeft(), s.getRight());
}
})
.doOnNext(new Action1<PriceDownloadedEvent>() {
@Override
public void call(PriceDownloadedEvent priceDownloadedEvent) {
// update to database
savePrice(priceDownloadedEvent);
}
})
.observeOn(AndroidSchedulers.mainThread())
.subscribe(new Subscriber<PriceDownloadedEvent>() {
@Override
public void onCompleted() {
closeProgressDialog();
new UIHelper(getContext()).showToast(R.string.download_complete);
compositeSubscription.unsubscribe();
// send event to reload the data.
EventBus.getDefault().post(new AllPricesDownloadedEvent());
}
@Override
public void onError(Throwable e) {
closeProgressDialog();
Timber.e(e, "downloading prices");
}
@Override
public void onNext(PriceDownloadedEvent event) {
mCounter++;
setProgress(mCounter);
// todo: update price in the UI?
// todo: remove the progress bar in that case.
}
})
);
}
private void processInParallel(List<String> symbols) {
for(int i = 0; i < symbols.size(); i++) {
final String symbol = symbols.get(i);
final String morningstarSymbol = symbolConverter.convert(symbol);
compositeSubscription.add(
service.getPrice(morningstarSymbol)
.subscribeOn(Schedulers.io())
.doOnNext(new Action1<String>() {
@Override
public void call(String s) {
PriceDownloadedEvent event = parse(morningstarSymbol, s);
savePrice(event);
}
})
.observeOn(AndroidSchedulers.mainThread())
.subscribe(new Subscriber<String>() {
@Override
public void onCompleted() {
finishIfAllDone();
}
@Override
public void onError(Throwable e) {
mCounter++;
setProgress(mCounter);
Timber.e(e, "error downloading price %s", symbol);
finishIfAllDone();
}
@Override
public void onNext(String s) {
mCounter++;
setProgress(mCounter);
}
})
);
}
// unsubscribe if the user navigates away while downloading prices?
}
/**
* Parse Morningstar response into price information.
* @param symbol Morningstar symbol
* @param html Result
* @return An object containing price details
*/
private PriceDownloadedEvent parse(String symbol, String html) {
Document doc = Jsoup.parse(html);
// symbol
String yahooSymbol = symbolConverter.getYahooSymbol(symbol);
// price
String priceString = doc.body().getElementById("last-price-value").text();
if (TextUtils.isEmpty(priceString)) {
throw new RuntimeException("No price available for " + symbol);
}
Money price = MoneyFactory.fromString(priceString);
// currency
String currency = doc.body().getElementById("curency").text();
if (currency.equals("GBX")) {
price = price.divide(100, MoneyFactory.MAX_ALLOWED_PRECISION);
}
// date
String dateString = doc.body().getElementById("asOfDate").text();
String dateFormat = "MM/dd/yyyy HH:mm:ss";
// DateTimeFormatter formatter = DateTimeFormat.forPattern(dateFormat);
// the time zone is EST
// DateTime date = formatter.withZone(DateTimeZone.forID("America/New_York"))
// .parseDateTime(dateString)
// .withZone(DateTimeZone.forID("Europe/Vienna"));
// convert time zone
MmxDate dateTime = new MmxDate(dateString, dateFormat)
.setTimeZone("America/New_York")
.inTimeZone("Europe/Vienna");
// todo: should this be converted to the exchange time?
return new PriceDownloadedEvent(yahooSymbol, price, dateTime.toDate());
}
private synchronized void finishIfAllDone() {
if (mCounter != mTotalRecords) return;
compositeSubscription.unsubscribe();
closeProgressDialog();
// Notify user that all the prices have been downloaded.
new UIHelper(getContext()).showToast(R.string.download_complete);
// fire an event so that the data can be reloaded.
EventBus.getDefault().post(new AllPricesDownloadedEvent());
}
private void savePrice(PriceDownloadedEvent event) {
BriteDatabase.Transaction tx = stockRepository.get().database.newTransaction();
// update the current price of the stock.
stockRepository.get().updateCurrentPrice(event.symbol, event.price);
// update price history record.
stockHistoryRepository.get().addStockHistoryRecord(event.symbol,
event.price, event.date);
tx.markSuccessful();
tx.end();
}
private IMorningstarService getMorningstarService() {
String BASE_URL = "http://quotes.morningstar.com";
Retrofit retrofit = new Retrofit.Builder()
.addConverterFactory(ScalarsConverterFactory.create())
.addCallAdapterFactory(RxJavaCallAdapterFactory.create())
.baseUrl(BASE_URL)
.build();
return retrofit.create(IMorningstarService.class);
}
}