/*
* Copyright (c) 2012 - 2014 Ngewi Fet <ngewif@gmail.com>
* Copyright (c) 2014 Yongxin Wang <fefe.wyx@gmail.com>
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.gnucash.android.ui.account;
import android.Manifest;
import android.annotation.TargetApi;
import android.app.Activity;
import android.app.AlertDialog;
import android.content.ActivityNotFoundException;
import android.content.Context;
import android.content.DialogInterface;
import android.content.Intent;
import android.content.SharedPreferences;
import android.content.SharedPreferences.Editor;
import android.content.pm.PackageInfo;
import android.content.pm.PackageManager;
import android.content.pm.PackageManager.NameNotFoundException;
import android.content.res.Resources;
import android.net.Uri;
import android.os.Build;
import android.os.Bundle;
import android.support.design.widget.CoordinatorLayout;
import android.support.design.widget.FloatingActionButton;
import android.support.design.widget.Snackbar;
import android.support.design.widget.TabLayout;
import android.support.v4.app.Fragment;
import android.support.v4.app.FragmentManager;
import android.support.v4.app.FragmentPagerAdapter;
import android.support.v4.view.ViewPager;
import android.support.v7.app.AppCompatActivity;
import android.support.v7.preference.PreferenceManager;
import android.util.Log;
import android.util.SparseArray;
import android.view.Menu;
import android.view.MenuInflater;
import android.view.MenuItem;
import android.view.View;
import android.view.ViewGroup;
import android.widget.Toast;
import com.crashlytics.android.Crashlytics;
import com.kobakei.ratethisapp.RateThisApp;
import org.gnucash.android.BuildConfig;
import org.gnucash.android.R;
import org.gnucash.android.app.GnuCashApplication;
import org.gnucash.android.db.DatabaseSchema;
import org.gnucash.android.db.adapter.AccountsDbAdapter;
import org.gnucash.android.db.adapter.BooksDbAdapter;
import org.gnucash.android.export.xml.GncXmlExporter;
import org.gnucash.android.importer.ImportAsyncTask;
import org.gnucash.android.ui.common.BaseDrawerActivity;
import org.gnucash.android.ui.common.FormActivity;
import org.gnucash.android.ui.common.Refreshable;
import org.gnucash.android.ui.common.UxArgument;
import org.gnucash.android.ui.transaction.TransactionsActivity;
import org.gnucash.android.ui.util.TaskDelegate;
import org.gnucash.android.ui.wizard.FirstRunWizardActivity;
import butterknife.BindView;
/**
* Manages actions related to accounts, displaying, exporting and creating new accounts
* The various actions are implemented as Fragments which are then added to this activity
*
* @author Ngewi Fet <ngewif@gmail.com>
* @author Oleksandr Tyshkovets <olexandr.tyshkovets@gmail.com>
*/
public class AccountsActivity extends BaseDrawerActivity implements OnAccountClickedListener {
/**
* Request code for GnuCash account structure file to import
*/
public static final int REQUEST_PICK_ACCOUNTS_FILE = 0x1;
/**
* Request code for opening the account to edit
*/
public static final int REQUEST_EDIT_ACCOUNT = 0x10;
/**
* Logging tag
*/
protected static final String LOG_TAG = "AccountsActivity";
/**
* Number of pages to show
*/
private static final int DEFAULT_NUM_PAGES = 3;
/**
* Index for the recent accounts tab
*/
public static final int INDEX_RECENT_ACCOUNTS_FRAGMENT = 0;
/**
* Index of the top level (all) accounts tab
*/
public static final int INDEX_TOP_LEVEL_ACCOUNTS_FRAGMENT = 1;
/**
* Index of the favorite accounts tab
*/
public static final int INDEX_FAVORITE_ACCOUNTS_FRAGMENT = 2;
/**
* Used to save the index of the last open tab and restore the pager to that index
*/
public static final String LAST_OPEN_TAB_INDEX = "last_open_tab";
/**
* Key for putting argument for tab into bundle arguments
*/
public static final String EXTRA_TAB_INDEX = "org.gnucash.android.extra.TAB_INDEX";
/**
* Map containing fragments for the different tabs
*/
private SparseArray<Refreshable> mFragmentPageReferenceMap = new SparseArray<>();
/**
* ViewPager which manages the different tabs
*/
@BindView(R.id.pager) ViewPager mViewPager;
@BindView(R.id.fab_create_account) FloatingActionButton mFloatingActionButton;
@BindView(R.id.coordinatorLayout) CoordinatorLayout mCoordinatorLayout;
/**
* Configuration for rating the app
*/
public static RateThisApp.Config rateAppConfig = new RateThisApp.Config(14, 100);
private AccountViewPagerAdapter mPagerAdapter;
/**
* Adapter for managing the sub-account and transaction fragment pages in the accounts view
*/
private class AccountViewPagerAdapter extends FragmentPagerAdapter {
public AccountViewPagerAdapter(FragmentManager fm){
super(fm);
}
@Override
public Fragment getItem(int i) {
AccountsListFragment currentFragment = (AccountsListFragment) mFragmentPageReferenceMap.get(i);
if (currentFragment == null) {
switch (i) {
case INDEX_RECENT_ACCOUNTS_FRAGMENT:
currentFragment = AccountsListFragment.newInstance(AccountsListFragment.DisplayMode.RECENT);
break;
case INDEX_FAVORITE_ACCOUNTS_FRAGMENT:
currentFragment = AccountsListFragment.newInstance(AccountsListFragment.DisplayMode.FAVORITES);
break;
case INDEX_TOP_LEVEL_ACCOUNTS_FRAGMENT:
default:
currentFragment = AccountsListFragment.newInstance(AccountsListFragment.DisplayMode.TOP_LEVEL);
break;
}
mFragmentPageReferenceMap.put(i, currentFragment);
}
return currentFragment;
}
@Override
public void destroyItem(ViewGroup container, int position, Object object) {
super.destroyItem(container, position, object);
mFragmentPageReferenceMap.remove(position);
}
@Override
public CharSequence getPageTitle(int position) {
switch (position){
case INDEX_RECENT_ACCOUNTS_FRAGMENT:
return getString(R.string.title_recent_accounts);
case INDEX_FAVORITE_ACCOUNTS_FRAGMENT:
return getString(R.string.title_favorite_accounts);
case INDEX_TOP_LEVEL_ACCOUNTS_FRAGMENT:
default:
return getString(R.string.title_all_accounts);
}
}
@Override
public int getCount() {
return DEFAULT_NUM_PAGES;
}
}
public AccountsListFragment getCurrentAccountListFragment(){
int index = mViewPager.getCurrentItem();
Fragment fragment = (Fragment) mFragmentPageReferenceMap.get(index);
if (fragment == null)
fragment = mPagerAdapter.getItem(index);
return (AccountsListFragment) fragment;
}
@Override
public int getContentView() {
return R.layout.activity_accounts;
}
@Override
public int getTitleRes() {
return R.string.title_accounts;
}
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
final Intent intent = getIntent();
handleOpenFileIntent(intent);
init();
TabLayout tabLayout = (TabLayout) findViewById(R.id.tab_layout);
tabLayout.addTab(tabLayout.newTab().setText(R.string.title_recent_accounts));
tabLayout.addTab(tabLayout.newTab().setText(R.string.title_all_accounts));
tabLayout.addTab(tabLayout.newTab().setText(R.string.title_favorite_accounts));
tabLayout.setTabGravity(TabLayout.GRAVITY_FILL);
//show the simple accounts list
mPagerAdapter = new AccountViewPagerAdapter(getSupportFragmentManager());
mViewPager.setAdapter(mPagerAdapter);
mViewPager.addOnPageChangeListener(new TabLayout.TabLayoutOnPageChangeListener(tabLayout));
tabLayout.setOnTabSelectedListener(new TabLayout.OnTabSelectedListener() {
@Override
public void onTabSelected(TabLayout.Tab tab) {
mViewPager.setCurrentItem(tab.getPosition());
}
@Override
public void onTabUnselected(TabLayout.Tab tab) {
//nothing to see here, move along
}
@Override
public void onTabReselected(TabLayout.Tab tab) {
//nothing to see here, move along
}
});
setCurrentTab();
mFloatingActionButton.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
Intent addAccountIntent = new Intent(AccountsActivity.this, FormActivity.class);
addAccountIntent.setAction(Intent.ACTION_INSERT_OR_EDIT);
addAccountIntent.putExtra(UxArgument.FORM_TYPE, FormActivity.FormType.ACCOUNT.name());
startActivityForResult(addAccountIntent, AccountsActivity.REQUEST_EDIT_ACCOUNT);
}
});
}
@Override
protected void onStart() {
super.onStart();
if (BuildConfig.CAN_REQUEST_RATING) {
RateThisApp.init(rateAppConfig);
RateThisApp.onStart(this);
RateThisApp.showRateDialogIfNeeded(this);
}
}
/**
* Handles the case where another application has selected to open a (.gnucash or .gnca) file with this app
* @param intent Intent containing the data to be imported
*/
private void handleOpenFileIntent(Intent intent) {
//when someone launches the app to view a (.gnucash or .gnca) file
Uri data = intent.getData();
if (data != null){
GncXmlExporter.createBackup();
intent.setData(null);
new ImportAsyncTask(this).execute(data);
removeFirstRunFlag();
}
}
@Override
protected void onNewIntent(Intent intent) {
super.onNewIntent(intent);
setIntent(intent);
setCurrentTab();
int index = mViewPager.getCurrentItem();
Fragment fragment = (Fragment) mFragmentPageReferenceMap.get(index);
if (fragment != null)
((Refreshable)fragment).refresh();
handleOpenFileIntent(intent);
}
/**
* Sets the current tab in the ViewPager
*/
public void setCurrentTab(){
SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(this);
int lastTabIndex = preferences.getInt(LAST_OPEN_TAB_INDEX, INDEX_TOP_LEVEL_ACCOUNTS_FRAGMENT);
int index = getIntent().getIntExtra(EXTRA_TAB_INDEX, lastTabIndex);
mViewPager.setCurrentItem(index);
}
/**
* Loads default setting for currency and performs app first-run initialization.
* <p>Also handles displaying the What's New dialog</p>
*/
private void init() {
PreferenceManager.setDefaultValues(this, BooksDbAdapter.getInstance().getActiveBookUID(),
Context.MODE_PRIVATE, R.xml.fragment_transaction_preferences, true);
SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(this);
boolean firstRun = prefs.getBoolean(getString(R.string.key_first_run), true);
if (firstRun){
startActivity(new Intent(GnuCashApplication.getAppContext(), FirstRunWizardActivity.class));
//default to using double entry and save the preference explicitly
prefs.edit().putBoolean(getString(R.string.key_use_double_entry), true).apply();
finish();
return;
}
if (hasNewFeatures()){
showWhatsNewDialog(this);
}
GnuCashApplication.startScheduledActionExecutionService(this);
}
@Override
protected void onDestroy() {
super.onDestroy();
SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(this);
preferences.edit().putInt(LAST_OPEN_TAB_INDEX, mViewPager.getCurrentItem()).apply();
}
/**
* Checks if the minor version has been increased and displays the What's New dialog box.
* This is the minor version as per semantic versioning.
* @return <code>true</code> if the minor version has been increased, <code>false</code> otherwise.
*/
private boolean hasNewFeatures(){
String minorVersion = getResources().getString(R.string.app_minor_version);
int currentMinor = Integer.parseInt(minorVersion);
SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(this);
int previousMinor = prefs.getInt(getString(R.string.key_previous_minor_version), 0);
if (currentMinor > previousMinor){
Editor editor = prefs.edit();
editor.putInt(getString(R.string.key_previous_minor_version), currentMinor);
editor.apply();
return true;
}
return false;
}
/**
* Show dialog with new features for this version
*/
public static AlertDialog showWhatsNewDialog(Context context){
Resources resources = context.getResources();
StringBuilder releaseTitle = new StringBuilder(resources.getString(R.string.title_whats_new));
PackageInfo packageInfo;
try {
packageInfo = context.getPackageManager().getPackageInfo(context.getPackageName(), 0);
releaseTitle.append(" - v").append(packageInfo.versionName);
} catch (NameNotFoundException e) {
Crashlytics.logException(e);
Log.e(LOG_TAG, "Error displaying 'Whats new' dialog");
}
return new AlertDialog.Builder(context)
.setTitle(releaseTitle.toString())
.setMessage(R.string.whats_new)
.setPositiveButton(R.string.label_dismiss, new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
dialog.dismiss();
}
}).show();
}
/**
* Displays the dialog for exporting transactions
*/
public static void openExportFragment(AppCompatActivity activity) {
Intent intent = new Intent(activity, FormActivity.class);
intent.putExtra(UxArgument.FORM_TYPE, FormActivity.FormType.EXPORT.name());
activity.startActivity(intent);
}
@Override
public boolean onCreateOptionsMenu(Menu menu) {
MenuInflater inflater = getMenuInflater();
inflater.inflate(R.menu.global_actions, menu);
return true;
}
@Override
public boolean onOptionsItemSelected(MenuItem item) {
switch (item.getItemId()) {
case android.R.id.home:
return super.onOptionsItemSelected(item);
default:
return false;
}
}
/**
* Creates default accounts with the specified currency code.
* If the currency parameter is null, then locale currency will be used if available
*
* @param currencyCode Currency code to assign to the imported accounts
* @param activity Activity for providing context and displaying dialogs
*/
public static void createDefaultAccounts(final String currencyCode, final Activity activity) {
TaskDelegate delegate = null;
if (currencyCode != null) {
delegate = new TaskDelegate() {
@Override
public void onTaskComplete() {
AccountsDbAdapter.getInstance().updateAllAccounts(DatabaseSchema.AccountEntry.COLUMN_CURRENCY, currencyCode);
GnuCashApplication.setDefaultCurrencyCode(currencyCode);
}
};
}
Uri uri = Uri.parse("android.resource://" + BuildConfig.APPLICATION_ID + "/" + R.raw.default_accounts);
new ImportAsyncTask(activity, delegate).execute(uri);
}
/**
* Starts Intent chooser for selecting a GnuCash accounts file to import.
* <p>The {@code activity} is responsible for the actual import of the file and can do so by calling {@link #importXmlFileFromIntent(Activity, Intent, TaskDelegate)}<br>
* The calling class should respond to the request code {@link AccountsActivity#REQUEST_PICK_ACCOUNTS_FILE} in its {@link #onActivityResult(int, int, Intent)} method</p>
* @param activity Activity starting the request and will also handle the response
* @see #importXmlFileFromIntent(Activity, Intent, TaskDelegate)
*/
public static void startXmlFileChooser(Activity activity) {
Intent pickIntent = new Intent(Intent.ACTION_GET_CONTENT);
pickIntent.addCategory(Intent.CATEGORY_OPENABLE);
pickIntent.setType("*/*");
Intent chooser = Intent.createChooser(pickIntent, "Select GnuCash account file"); //todo internationalize string
try {
activity.startActivityForResult(chooser, REQUEST_PICK_ACCOUNTS_FILE);
} catch (ActivityNotFoundException ex){
Crashlytics.log("No file manager for selecting files available");
Crashlytics.logException(ex);
Toast.makeText(activity, R.string.toast_install_file_manager, Toast.LENGTH_LONG).show();
}
}
/**
* Overloaded method.
* Starts chooser for selecting a GnuCash account file to import
* @param fragment Fragment creating the chooser and which will also handle the result
* @see #startXmlFileChooser(Activity)
*/
public static void startXmlFileChooser(Fragment fragment) {
Intent pickIntent = new Intent(Intent.ACTION_GET_CONTENT);
pickIntent.addCategory(Intent.CATEGORY_OPENABLE);
pickIntent.setType("*/*");
Intent chooser = Intent.createChooser(pickIntent, "Select GnuCash account file"); //todo internationalize string
try {
fragment.startActivityForResult(chooser, REQUEST_PICK_ACCOUNTS_FILE);
} catch (ActivityNotFoundException ex){
Crashlytics.log("No file manager for selecting files available");
Crashlytics.logException(ex);
Toast.makeText(fragment.getActivity(), R.string.toast_install_file_manager, Toast.LENGTH_LONG).show();
}
}
/**
* Reads and XML file from an intent and imports it into the database
* <p>This method is usually called in response to {@link AccountsActivity#startXmlFileChooser(Activity)}</p>
* @param context Activity context
* @param data Intent data containing the XML uri
* @param onFinishTask Task to be executed when import is complete
*/
public static void importXmlFileFromIntent(Activity context, Intent data, TaskDelegate onFinishTask) {
GncXmlExporter.createBackup();
new ImportAsyncTask(context, onFinishTask).execute(data.getData());
}
/**
* Starts the AccountsActivity and clears the activity stack
* @param context Application context
*/
public static void start(Context context){
Intent accountsActivityIntent = new Intent(context, AccountsActivity.class);
accountsActivityIntent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP);
accountsActivityIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK|Intent.FLAG_ACTIVITY_SINGLE_TOP);
context.startActivity(accountsActivityIntent);
}
@Override
public void accountSelected(String accountUID) {
Intent intent = new Intent(this, TransactionsActivity.class);
intent.setAction(Intent.ACTION_VIEW);
intent.putExtra(UxArgument.SELECTED_ACCOUNT_UID, accountUID);
startActivity(intent);
}
/**
* Removes the flag indicating that the app is being run for the first time.
* This is called every time the app is started because the next time won't be the first time
*/
public static void removeFirstRunFlag(){
Context context = GnuCashApplication.getAppContext();
Editor editor = PreferenceManager.getDefaultSharedPreferences(context).edit();
editor.putBoolean(context.getString(R.string.key_first_run), false);
editor.commit();
}
}