/*
* 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.watchlist;
import android.content.DialogInterface;
import android.content.Intent;
import android.database.Cursor;
import android.os.Bundle;
import android.support.annotation.NonNull;
import android.support.v4.app.Fragment;
import android.support.v4.app.FragmentTransaction;
import android.support.v7.app.ActionBar;
import android.support.v7.app.AppCompatActivity;
import android.text.TextUtils;
import android.view.LayoutInflater;
import android.view.Menu;
import android.view.MenuInflater;
import android.view.MenuItem;
import android.view.View;
import android.view.View.OnClickListener;
import android.view.ViewGroup;
import android.widget.AdapterView;
import android.widget.ImageView;
import android.widget.SimpleCursorAdapter;
import android.widget.Spinner;
import android.widget.Toast;
import com.afollestad.materialdialogs.DialogAction;
import com.afollestad.materialdialogs.MaterialDialog;
import com.crashlytics.android.answers.Answers;
import com.crashlytics.android.answers.CustomEvent;
import com.mikepenz.google_material_typeface_library.GoogleMaterial;
import com.mikepenz.iconics.IconicsDrawable;
import com.mikepenz.mmex_icon_font_typeface_library.MMXIconFont;
import com.money.manager.ex.Constants;
import com.money.manager.ex.account.AccountEditActivity;
import com.money.manager.ex.R;
import com.money.manager.ex.core.AnswersEvents;
import com.money.manager.ex.core.UIHelper;
import com.money.manager.ex.datalayer.AccountRepository;
import com.money.manager.ex.datalayer.StockFields;
import com.money.manager.ex.datalayer.StockHistoryRepository;
import com.money.manager.ex.log.ErrorRaisedEvent;
import com.money.manager.ex.datalayer.StockRepository;
import com.money.manager.ex.domainmodel.Account;
import com.money.manager.ex.investment.ISecurityPriceUpdater;
import com.money.manager.ex.investment.PriceCsvExport;
import com.money.manager.ex.investment.QuoteProviders;
import com.money.manager.ex.investment.SecurityPriceUpdaterFactory;
import com.money.manager.ex.investment.events.AllPricesDownloadedEvent;
import com.money.manager.ex.investment.events.PriceDownloadedEvent;
import com.money.manager.ex.investment.events.PriceUpdateRequestEvent;
import com.money.manager.ex.servicelayer.AccountService;
import com.money.manager.ex.settings.InvestmentSettings;
import com.money.manager.ex.sync.SyncManager;
import com.shamanland.fonticon.FontIconDrawable;
import org.greenrobot.eventbus.EventBus;
import org.greenrobot.eventbus.Subscribe;
import org.parceler.Parcels;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Date;
import java.util.List;
import info.javaperformance.money.Money;
import timber.log.Timber;
/**
* The main fragment for the watchlist. Contains the list and everything else.
* Not sure why it was done in two fragments. Probably because the list can not have additional items?
*/
public class WatchlistFragment
extends Fragment {
private static final String KEY_ACCOUNT_ID = "WatchlistFragment:AccountId";
private static final String KEY_ACCOUNT = "WatchlistFragment:Account";
/**
* @param accountId ID Account to be display
* @return instance of Watchlist fragment with transactions for the given account.
*/
public static WatchlistFragment newInstance(int accountId) {
WatchlistFragment fragment = new WatchlistFragment();
Bundle args = new Bundle();
args.putInt(KEY_ACCOUNT_ID, accountId);
fragment.setArguments(args);
fragment.setFragmentName(WatchlistFragment.class.getSimpleName() + "_" + Integer.toString(accountId));
return fragment;
}
private WatchlistItemsFragment mDataFragment;
private String mFragmentName;
private StockRepository mStockRepository;
private Account mAccount;
// price update counter. Used to know when all the prices are done downloading.
private int mUpdateCounter;
private int mToUpdateTotal;
private WatchlistViewHolder viewHolder;
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
loadAccount();
if ((savedInstanceState != null)) {
mAccount = Parcels.unwrap(savedInstanceState.getParcelable(KEY_ACCOUNT));
}
mUpdateCounter = 0;
Answers.getInstance().logCustom(new CustomEvent(AnswersEvents.Watchlist.name()));
}
@Override
public void onStart() {
super.onStart();
EventBus.getDefault().register(this);
}
@Override
public void onStop() {
super.onStop();
EventBus.getDefault().unregister(this);
}
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
if ((savedInstanceState != null)) {
mAccount = Parcels.unwrap(savedInstanceState.getParcelable(KEY_ACCOUNT));
}
if (container == null) return null;
View view = inflater.inflate(R.layout.fragment_account_transactions, container, false);
if (mAccount == null) {
loadAccount();
}
this.viewHolder = new WatchlistViewHolder();
initializeListHeader(inflater);
// manage fragment
FragmentTransaction transaction = getChildFragmentManager().beginTransaction();
mDataFragment = WatchlistItemsFragment.newInstance();
// set arguments and preferences of fragment
Bundle arguments = new Bundle();
arguments.putInt(WatchlistItemsFragment.KEY_ACCOUNT_ID, getAccountId());
mDataFragment.setArguments(arguments);
mDataFragment.setListHeader(this.viewHolder.mListHeader);
mDataFragment.setAutoStarLoader(false);
// add fragment
transaction.replace(R.id.fragmentMain, mDataFragment, getFragmentName());
transaction.commit();
// refresh user interface
if (mAccount != null) {
setImageViewFavorite();
}
setHasOptionsMenu(true);
// initializeSwipeToRefresh(view);
return view;
}
@Override
public void onActivityCreated(Bundle savedInstanceState) {
super.onActivityCreated(savedInstanceState);
// hide the title
ActionBar actionBar = getActionBar();
if (actionBar != null) {
actionBar.setDisplayShowTitleEnabled(false);
}
}
@Override
public void onResume() {
super.onResume();
initializeAccountsSelector();
selectCurrentAccount();
// restart loader
reloadData();
}
// Menu
/**
* Called once when the menu is being created.
*/
@Override
public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) {
super.onCreateOptionsMenu(menu, inflater);
// add options menu for watchlist
inflater.inflate(R.menu.menu_watchlist, menu);
// custom icons
UIHelper uiHelper = new UIHelper(getActivity());
IconicsDrawable icon;
MenuItem menuItem;
menuItem = menu.findItem(R.id.menu_update_prices);
if (menuItem != null) {
icon = uiHelper.getIcon(GoogleMaterial.Icon.gmd_file_download);
menuItem.setIcon(icon);
}
menuItem = menu.findItem(R.id.menu_export_prices);
if (menuItem != null) {
icon = uiHelper.getIcon(GoogleMaterial.Icon.gmd_share);
menuItem.setIcon(icon);
}
menuItem = menu.findItem(R.id.menu_purge_history);
if (menuItem != null) {
icon = uiHelper.getIcon(GoogleMaterial.Icon.gmd_content_cut);
menuItem.setIcon(icon);
}
// call create option menu of fragment
mDataFragment.onCreateOptionsMenu(menu, inflater);
}
/**
* Handle menu item click.
* Update prices.
* @param item Menu item selected
* @return indicator whether the selection was handled
*/
@Override
public boolean onOptionsItemSelected(MenuItem item) {
int itemId = item.getItemId();
switch (itemId) {
case R.id.menu_update_prices:
confirmPriceUpdate();
break;
case R.id.menu_export_prices:
exportPrices();
break;
case R.id.menu_purge_history:
purgePriceHistory();
break;
case R.id.menu_change_provider:
changePriceProvider();
break;
default:
break;
}
return super.onOptionsItemSelected(item);
}
@Override
public void onSaveInstanceState(Bundle outState) {
super.onSaveInstanceState(outState);
outState.putParcelable(KEY_ACCOUNT, Parcels.wrap(mAccount));
}
// Events
@Subscribe
public void onEvent(AllPricesDownloadedEvent event) {
reloadData();
}
@Subscribe
public void onEvent(PriceDownloadedEvent event) {
onPriceDownloaded(event.symbol, event.price, event.date);
}
@Subscribe
public void onEvent(PriceUpdateRequestEvent event) {
onPriceUpdateRequested(event.symbol);
}
@Subscribe
public void onEvent(ErrorRaisedEvent event) {
new UIHelper(getActivity()).showToast(event.message);
}
/**
* Start Loader to retrieve data
*/
public void reloadData() {
if (mDataFragment != null) {
mDataFragment.reloadData();
}
}
public String getFragmentName() {
return mFragmentName;
}
public void setFragmentName(String fragmentName) {
this.mFragmentName = fragmentName;
}
// Private
/**
* Called from asynchronous task when a first price is downloaded.
* @param symbol Stock symbol
* @param price Stock price
* @param date Date of the price
*/
private void onPriceDownloaded(String symbol, Money price, Date date) {
// prices updated.
if (TextUtils.isEmpty(symbol)) return;
// update the current price of the stock.
StockRepository repo = getStockRepository();
repo.updateCurrentPrice(symbol, price);
// update price history record.
StockHistoryRepository historyRepo = mDataFragment.getStockHistoryRepository();
historyRepo.addStockHistoryRecord(symbol, price, date);
mUpdateCounter += 1;
if (mUpdateCounter == mToUpdateTotal) {
completePriceUpdate();
}
}
/**
* Price update requested from the securities list context menu.
* @param symbol Stock symbol for which to fetch the price.
*/
private void onPriceUpdateRequested(String symbol) {
// reset counter & max.
mToUpdateTotal = 1;
mUpdateCounter = 0;
// http://stackoverflow.com/questions/1005073/initialization-of-an-arraylist-in-one-line
List<String> symbols = new ArrayList<>();
symbols.add(symbol);
ISecurityPriceUpdater updater = SecurityPriceUpdaterFactory.getUpdaterInstance(getActivity());
updater.downloadPrices(symbols);
// result received via onEvent.
}
/**
* refresh UI, show favorite icon
*/
private void setImageViewFavorite() {
if (mAccount.getFavorite()) {
this.viewHolder.imgAccountFav.setBackgroundResource(R.drawable.ic_star);
} else {
this.viewHolder.imgAccountFav.setBackgroundResource(R.drawable.ic_star_outline);
}
}
private void changePriceProvider() {
// show the list with provider choices. Preselect the current one.
InvestmentSettings settings = new InvestmentSettings(getActivity());
QuoteProviders currentProvider = settings.getQuoteProvider();
int currentIndex = QuoteProviders.indexOf(currentProvider);
new MaterialDialog.Builder(getActivity())
.title(R.string.quote_provider)
.items(QuoteProviders.names())
.itemsCallbackSingleChoice(currentIndex, new MaterialDialog.ListCallbackSingleChoice() {
@Override
public boolean onSelection(MaterialDialog dialog, View itemView, int which, CharSequence text) {
//change provider
QuoteProviders newProvider = QuoteProviders.valueOf(text.toString());
InvestmentSettings settings = new InvestmentSettings(getActivity());
settings.setQuoteProvider(newProvider);
return true;
}
})
.show();
}
private void completePriceUpdate() {
// this call is made from async task so have to get back to the main thread.
getActivity().runOnUiThread(new Runnable() {
@Override
public void run() {
// refresh the data.
mDataFragment.reloadData();
// notify about db file change.
new SyncManager(getActivity()).dataChanged();
}
});
}
private StockRepository getStockRepository() {
if (mStockRepository == null) {
mStockRepository = new StockRepository(getActivity());
}
return mStockRepository;
}
private String[] getAllShownSymbols() {
int itemCount = mDataFragment.getListAdapter().getCount();
String[] result = new String[itemCount];
for(int i = 0; i < itemCount; i++) {
Cursor cursor = (Cursor) mDataFragment.getListAdapter().getItem(i);
String symbol = cursor.getString(cursor.getColumnIndex(StockFields.SYMBOL));
result[i] = symbol;
}
return result;
}
private void exportPrices() {
PriceCsvExport export = new PriceCsvExport(getActivity());
boolean result = false;
try {
String prefix;
if (mAccount != null) {
prefix = mAccount.getName();
} else {
prefix = getActivity().getString(R.string.all_accounts);
}
result = export.exportPrices(mDataFragment.getListAdapter(), prefix);
} catch (IOException ex) {
Timber.e(ex, "exporting stock prices");
}
// todo: e result. (?)
}
private void confirmPriceUpdate() {
new MaterialDialog.Builder(getContext())
.title(R.string.download)
.icon(FontIconDrawable.inflate(getContext(), R.xml.ic_question))
.content(R.string.confirm_price_download)
.positiveText(android.R.string.ok)
.onPositive(new MaterialDialog.SingleButtonCallback() {
@Override
public void onClick(@NonNull MaterialDialog dialog, @NonNull DialogAction which) {
// get the list of symbols
String[] symbols = getAllShownSymbols();
mToUpdateTotal = symbols.length;
mUpdateCounter = 0;
// update security prices
ISecurityPriceUpdater updater = SecurityPriceUpdaterFactory
.getUpdaterInstance(getContext());
updater.downloadPrices(Arrays.asList(symbols));
// results received via event
dialog.dismiss();
}
})
.negativeText(android.R.string.cancel)
.onNegative(new MaterialDialog.SingleButtonCallback() {
@Override
public void onClick(@NonNull MaterialDialog dialog, @NonNull DialogAction which) {
dialog.dismiss();
}
})
.build()
.show();
}
private int getAccountId() {
if (mAccount == null) {
return Constants.NOT_SET;
}
return mAccount.getId();
}
private ActionBar getActionBar() {
if (!(getActivity() instanceof AppCompatActivity)) return null;
AppCompatActivity activity = (AppCompatActivity) getActivity();
ActionBar actionBar = activity.getSupportActionBar();
return actionBar;
}
private Spinner getAccountsSpinner() {
// get from custom view, not the menu.
ActionBar actionBar = getActionBar();
if (actionBar == null) return null;
Spinner spinner = (Spinner) actionBar.getCustomView().findViewById(R.id.spinner);
return spinner;
}
private void initializeAccountsSelector() {
ActionBar actionBar = getActionBar();
if (actionBar == null) return;
actionBar.setDisplayShowTitleEnabled(false);
actionBar.setCustomView(R.layout.spinner);
actionBar.setDisplayShowCustomEnabled(true);
Spinner spinner = getAccountsSpinner();
if (spinner == null) return;
// Load accounts into the spinner.
AccountService accountService = new AccountService(getActivity());
accountService.loadInvestmentAccountsToSpinner(spinner, true);
// e account switching.
spinner.setOnItemSelectedListener(new AdapterView.OnItemSelectedListener() {
@Override
public void onItemSelected(AdapterView<?> adapterView, View view, int i, long l) {
// switch account.
Cursor cursor = (Cursor) adapterView.getItemAtPosition(i);
Account account = Account.from(cursor);
int accountId = account.getId();
switchAccount(accountId);
}
@Override
public void onNothingSelected(AdapterView<?> adapterView) {
}
});
}
private void initializeListHeader(LayoutInflater inflater) {
this.viewHolder.mListHeader = (ViewGroup) inflater.inflate(R.layout.fragment_watchlist_header, null, false);
// favorite icon
this.viewHolder.imgAccountFav = (ImageView) this.viewHolder.mListHeader.findViewById(R.id.imageViewAccountFav);
// set listener click on favorite icon for change image
this.viewHolder.imgAccountFav.setOnClickListener(new OnClickListener() {
public void onClick(View v) {
mAccount.setFavorite(!mAccount.getFavorite());
AccountRepository repo = new AccountRepository(getActivity());
boolean saved = repo.save(mAccount);
if (!saved) {
Toast.makeText(getActivity(),
getActivity().getResources().getString(R.string.db_update_failed),
Toast.LENGTH_LONG).show();
} else {
setImageViewFavorite();
}
}
});
// Edit account
this.viewHolder.imgGotoAccount = (ImageView) this.viewHolder.mListHeader.findViewById(R.id.imageViewGotoAccount);
this.viewHolder.imgGotoAccount.setOnClickListener(new OnClickListener() {
@Override
public void onClick(View v) {
Intent intent = new Intent(getActivity(), AccountEditActivity.class);
intent.putExtra(AccountEditActivity.KEY_ACCOUNT_ID, getAccountId());
intent.setAction(Intent.ACTION_EDIT);
startActivity(intent);
}
});
}
// private void initializeSwipeToRefresh(View view) {
// final SwipeRefreshLayout layout = (SwipeRefreshLayout) view.findViewById(R.id.swipeLayout);
//
// layout.setOnRefreshListener(new SwipeRefreshLayout.OnRefreshListener() {
// @Override
// public void onRefresh() {
// // update prices
// // todo: do not display the manual progress binaryDialog
// String[] symbols = getAllShownSymbols();
// ISecurityPriceUpdater updater = SecurityPriceUpdaterFactory
// .getUpdaterInstance(getContext());
// updater.downloadPrices(Arrays.asList(symbols));
//
// layout.setRefreshing(false);
// }
// });
// }
private void loadAccount() {
Bundle args = getArguments();
if (args == null) return;
if (!args.containsKey(KEY_ACCOUNT_ID)) return;
int accountId = args.getInt(KEY_ACCOUNT_ID);
this.mAccount = new AccountRepository(getActivity()).load(accountId);
}
private void purgePriceHistory() {
new MaterialDialog.Builder(getContext())
.title(R.string.purge_history)
.icon(FontIconDrawable.inflate(getContext(), R.xml.ic_question))
.content(R.string.purge_history_confirmation)
.positiveText(android.R.string.ok)
.onPositive(new MaterialDialog.SingleButtonCallback() {
@Override
public void onClick(@NonNull MaterialDialog dialog, @NonNull DialogAction which) {
StockHistoryRepository history = new StockHistoryRepository(getActivity());
int deleted = history.deleteAllPriceHistory();
if (deleted > 0) {
new SyncManager(getActivity()).dataChanged();
Toast.makeText(getActivity(),
getActivity().getString(R.string.purge_history_complete), Toast.LENGTH_SHORT)
.show();
} else {
Toast.makeText(getActivity(),
getActivity().getString(R.string.purge_history_failed), Toast.LENGTH_SHORT)
.show();
}
dialog.dismiss();
}
})
.negativeText(android.R.string.cancel)
.onNegative(new MaterialDialog.SingleButtonCallback() {
@Override
public void onClick(@NonNull MaterialDialog dialog, @NonNull DialogAction which) {
dialog.dismiss(); }
})
.build()
.show();
}
/**
* Select the current account in the accounts dropdown.
*/
private void selectCurrentAccount() {
Spinner spinner = getAccountsSpinner();
if (spinner == null) return;
// find account
SimpleCursorAdapter adapter = (SimpleCursorAdapter) spinner.getAdapter();
if (adapter == null) return;
Cursor cursor = adapter.getCursor();
int position = Constants.NOT_SET;
for (int i = 0; i < adapter.getCount(); i++) {
cursor.moveToPosition(i);
String accountIdString = cursor.getString(cursor.getColumnIndex(Account.ACCOUNTID));
int accountId = Integer.parseInt(accountIdString);
if (accountId == getAccountId()) {
position = i;
break;
}
}
spinner.setSelection(position);
}
private void switchAccount(int accountId) {
if (accountId == getAccountId()) return;
// switch account. Reload transactions.
mAccount = new AccountRepository(getActivity()).load(accountId);
mDataFragment.accountId = accountId;
mDataFragment.reloadData();
// hide account details bar if all accounts are selected
if (accountId == Constants.NOT_SET) {
/*
Can't remove header view once it has been added.
Ref: http://stackoverflow.com/questions/13603888/remove-header-from-listview
*/
// mDataFragment.getListView().removeHeaderView(mListHeader);
// mListHeader.setVisibility(View.GONE);
/*
Just hide the contents and the row will automatically shrink (but not disappear).
*/
this.viewHolder.mListHeader.findViewById(R.id.headerRow).setVisibility(View.GONE);
} else {
if (mDataFragment.getListView().getHeaderViewsCount() == 0) {
mDataFragment.getListView().addHeaderView(this.viewHolder.mListHeader);
}
// mListHeader.setVisibility(View.VISIBLE);
this.viewHolder.mListHeader.findViewById(R.id.headerRow).setVisibility(View.VISIBLE);
}
}
}