Java tutorial
/* * Copyright (c) 2012 Ngewi Fet <ngewif@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.accounts; import java.util.Locale; import org.gnucash.android.R; import org.gnucash.android.data.Account; import org.gnucash.android.data.Money; import org.gnucash.android.db.AccountsDbAdapter; import org.gnucash.android.db.DatabaseAdapter; import org.gnucash.android.db.DatabaseCursorLoader; import org.gnucash.android.db.DatabaseHelper; import org.gnucash.android.db.TransactionsDbAdapter; import org.gnucash.android.ui.settings.SettingsActivity; import org.gnucash.android.ui.transactions.TransactionsActivity; import org.gnucash.android.ui.transactions.TransactionsListFragment; import org.gnucash.android.ui.widget.WidgetConfigurationActivity; import org.gnucash.android.util.OnAccountClickedListener; import android.app.Activity; import android.app.AlertDialog; import android.app.Dialog; import android.content.Context; import android.content.DialogInterface; import android.content.Intent; import android.database.Cursor; import android.os.Bundle; import android.support.v4.app.DialogFragment; import android.support.v4.app.Fragment; import android.support.v4.app.FragmentManager; import android.support.v4.app.FragmentTransaction; import android.support.v4.app.LoaderManager.LoaderCallbacks; import android.support.v4.content.Loader; import android.support.v4.widget.SimpleCursorAdapter; import android.util.Log; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; import android.widget.AdapterView; import android.widget.AdapterView.OnItemLongClickListener; import android.widget.ImageView; import android.widget.ListAdapter; import android.widget.ListView; import android.widget.TextView; import android.widget.Toast; import com.actionbarsherlock.app.ActionBar; import com.actionbarsherlock.app.SherlockDialogFragment; import com.actionbarsherlock.app.SherlockListFragment; import com.actionbarsherlock.view.ActionMode; import com.actionbarsherlock.view.ActionMode.Callback; import com.actionbarsherlock.view.Menu; import com.actionbarsherlock.view.MenuInflater; import com.actionbarsherlock.view.MenuItem; /** * Fragment for displaying the list of accounts in the database * @author Ngewi Fet <ngewif@gmail.com> * */ public class AccountsListFragment extends SherlockListFragment implements LoaderCallbacks<Cursor>, OnItemLongClickListener { /** * Request code passed when displaying the "Add Account" dialog. */ private static final int DIALOG_ADD_ACCOUNT = 0x10; /** * Logging tag */ protected static final String TAG = "AccountsListFragment"; /** * {@link ListAdapter} for the accounts which will be bound to the list */ AccountsCursorAdapter mAccountsCursorAdapter; /** * Dialog fragment for adding new accounts */ NewAccountDialogFragment mAddAccountFragment; /** * Database adapter for loading Account records from the database */ private AccountsDbAdapter mAccountsDbAdapter; /** * Listener to be notified when an account is clicked */ private OnAccountClickedListener mAccountSelectedListener; /** * Flag to indicate if the fragment is in edit mode * Edit mode means an account has been selected (through long press) and the * context action bar (CAB) is activated */ private boolean mInEditMode = false; /** * Android action mode * Is not null only when an accoun is selected and the Context ActionBar (CAB) is activated */ private ActionMode mActionMode = null; /** * Position which has been selected in the ListView */ private int mSelectedViewPosition = -1; /** * Stores the database ID of the currently selected account when in action mode. * This is necessary because getSelectedItemId() does not work properly (by design) * in touch mode (which is the majority of devices today) */ private long mSelectedItemId = -1; /** * Callbacks for the CAB menu */ private ActionMode.Callback mActionModeCallbacks = new Callback() { @Override public boolean onCreateActionMode(ActionMode mode, Menu menu) { MenuInflater inflater = mode.getMenuInflater(); inflater.inflate(R.menu.account_context_menu, menu); mode.setTitle(getString(R.string.title_selected, 1)); return true; } @Override public boolean onPrepareActionMode(ActionMode mode, Menu menu) { // nothing to see here, move along return false; } @Override public boolean onActionItemClicked(ActionMode mode, MenuItem item) { switch (item.getItemId()) { case R.id.context_menu_edit_accounts: showAddAccountDialog(mSelectedItemId); return true; case R.id.context_menu_delete: tryDeleteAccount(mSelectedItemId); mode.finish(); return true; default: return false; } } @Override public void onDestroyActionMode(ActionMode mode) { finishEditMode(); } }; /** * Delete confirmation dialog * Is displayed when deleting an account which has transactions. * If an account has no transactions, it is deleted immediately with no confirmation required * @author Ngewi Fet <ngewif@gmail.com> * */ public static class DeleteConfirmationDialogFragment extends SherlockDialogFragment { public static DeleteConfirmationDialogFragment newInstance(int title, long id) { DeleteConfirmationDialogFragment frag = new DeleteConfirmationDialogFragment(); Bundle args = new Bundle(); args.putInt("title", title); args.putLong(TransactionsListFragment.SELECTED_ACCOUNT_ID, id); frag.setArguments(args); return frag; } @Override public Dialog onCreateDialog(Bundle savedInstanceState) { int title = getArguments().getInt("title"); final long rowId = getArguments().getLong(TransactionsListFragment.SELECTED_ACCOUNT_ID); return new AlertDialog.Builder(getActivity()).setIcon(android.R.drawable.ic_delete).setTitle(title) .setMessage(R.string.delete_account_confirmation_message) .setPositiveButton(R.string.alert_dialog_ok_delete, new DialogInterface.OnClickListener() { public void onClick(DialogInterface dialog, int whichButton) { ((AccountsListFragment) getTargetFragment()).deleteAccount(rowId); } }).setNegativeButton(R.string.alert_dialog_cancel, new DialogInterface.OnClickListener() { public void onClick(DialogInterface dialog, int whichButton) { dismiss(); } }).create(); } } @Override public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { return inflater.inflate(R.layout.fragment_accounts_list, container, false); } @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); mAccountsDbAdapter = new AccountsDbAdapter(getActivity()); mAccountsCursorAdapter = new AccountsCursorAdapter(getActivity().getApplicationContext(), R.layout.list_item_account, null, new String[] { DatabaseHelper.KEY_NAME }, new int[] { R.id.account_name }); setListAdapter(mAccountsCursorAdapter); } @Override public void onActivityCreated(Bundle savedInstanceState) { super.onActivityCreated(savedInstanceState); ActionBar actionbar = getSherlockActivity().getSupportActionBar(); actionbar.setTitle(R.string.title_accounts); actionbar.setDisplayHomeAsUpEnabled(false); setHasOptionsMenu(true); ListView lv = getListView(); lv.setOnItemLongClickListener(this); getLoaderManager().initLoader(0, null, this); } @Override public void onResume() { super.onResume(); refreshList(); } @Override public void onAttach(Activity activity) { super.onAttach(activity); try { mAccountSelectedListener = (OnAccountClickedListener) activity; } catch (ClassCastException e) { throw new ClassCastException(activity.toString() + " must implement OnAccountSelectedListener"); } } @Override public void onListItemClick(ListView l, View v, int position, long id) { super.onListItemClick(l, v, position, id); if (mInEditMode) { mSelectedItemId = id; selectItem(position); return; } mAccountSelectedListener.accountSelected(id); } @Override public boolean onItemLongClick(AdapterView<?> parent, View view, int position, long id) { if (mActionMode != null) { return false; } mInEditMode = true; mSelectedItemId = id; // Start the CAB using the ActionMode.Callback defined above mActionMode = getSherlockActivity().startActionMode(mActionModeCallbacks); selectItem(position); return true; } /** * Delete the account with record ID <code>rowId</code> * It shows the delete confirmation dialog if the account has transactions, * else deletes the account immediately * @param rowId The record ID of the account */ public void tryDeleteAccount(long rowId) { Account acc = mAccountsDbAdapter.getAccount(rowId); if (acc.getTransactionCount() > 0) { showConfirmationDialog(rowId); } else { deleteAccount(rowId); } } /** * Deletes an account and show a {@link Toast} notification on success * @param rowId Record ID of the account to be deleted */ protected void deleteAccount(long rowId) { boolean deleted = mAccountsDbAdapter.destructiveDeleteAccount(rowId); if (deleted) { Toast.makeText(getActivity(), R.string.toast_account_deleted, Toast.LENGTH_SHORT).show(); WidgetConfigurationActivity.updateAllWidgets(getActivity().getApplicationContext()); } refreshList(); } /** * Shows the delete confirmation dialog * @param id Record ID of account to be deleted after confirmation */ public void showConfirmationDialog(long id) { DeleteConfirmationDialogFragment alertFragment = DeleteConfirmationDialogFragment .newInstance(R.string.title_confirm_delete, id); alertFragment.setTargetFragment(this, 0); alertFragment.show(getSherlockActivity().getSupportFragmentManager(), "dialog"); } /** * Finish the edit mode and dismisses the Contextual ActionBar * Any selected (highlighted) accounts are deselected */ public void finishEditMode() { mInEditMode = false; deselectPreviousSelectedItem(); mActionMode = null; mSelectedItemId = -1; } /** * Highlights the item at <code>position</code> in the ListView. * Android has facilities for managing list selection but the highlighting * is not reliable when using the ActionBar on pre-Honeycomb devices- * @param position Position of item to be highlighted */ private void selectItem(int position) { deselectPreviousSelectedItem(); ListView lv = getListView(); lv.setItemChecked(position, true); View v = lv.getChildAt(position - lv.getFirstVisiblePosition()); v.setSelected(true); v.setBackgroundColor(getResources().getColor(R.color.abs__holo_blue_light)); mSelectedViewPosition = position; } /** * De-selects the previously selected item in a ListView. * Only one account entry can be highlighted at a time, so the previously selected * one is deselected. */ private void deselectPreviousSelectedItem() { if (mSelectedViewPosition >= 0) { ListView lv = getListView(); lv.setItemChecked(mSelectedViewPosition, false); View v = getListView().getChildAt(mSelectedViewPosition - lv.getFirstVisiblePosition()); if (v == null) { //if we just deleted a row, then the previous position is invalid return; } v.setBackgroundColor(getResources().getColor(android.R.color.transparent)); v.setSelected(false); } } @Override public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) { inflater.inflate(R.menu.account_actions, menu); } @Override public boolean onOptionsItemSelected(MenuItem item) { switch (item.getItemId()) { case R.id.menu_add_account: showAddAccountDialog(0); return true; case R.id.menu_export: showExportDialog(); return true; case R.id.menu_settings: startActivity(new Intent(getActivity(), SettingsActivity.class)); return true; default: return false; } } /** * Refreshes the list by restarting the {@link DatabaseCursorLoader} associated * with the ListView */ public void refreshList() { getLoaderManager().restartLoader(0, null, this); } /** * Closes any open database adapters used by the list */ @Override public void onDestroy() { super.onDestroy(); mAccountsDbAdapter.close(); mAccountsCursorAdapter.close(); } /** * Show dialog for creating a new {@link Account} */ public void showAddAccountDialog(long accountId) { FragmentManager manager = getSherlockActivity().getSupportFragmentManager(); FragmentTransaction ft = manager.beginTransaction(); Fragment prev = manager.findFragmentByTag(AccountsActivity.FRAGMENT_NEW_ACCOUNT); if (prev != null) { ft.remove(prev); } ft.addToBackStack(null); mAddAccountFragment = NewAccountDialogFragment.newInstance(mAccountsDbAdapter); Bundle args = new Bundle(); args.putLong(TransactionsListFragment.SELECTED_ACCOUNT_ID, accountId); mAddAccountFragment.setArguments(args); mAddAccountFragment.setTargetFragment(this, DIALOG_ADD_ACCOUNT); if (mActionMode != null) { //if we were editing, stop before going somewhere else mActionMode.finish(); } mAddAccountFragment.show(ft, AccountsActivity.FRAGMENT_NEW_ACCOUNT); } /** * Displays the dialog for exporting transactions in OFX */ public void showExportDialog() { FragmentManager manager = getSherlockActivity().getSupportFragmentManager(); FragmentTransaction ft = manager.beginTransaction(); Fragment prev = manager.findFragmentByTag(AccountsActivity.FRAGMENT_EXPORT_OFX); if (prev != null) { ft.remove(prev); } ft.addToBackStack(null); // Create and show the dialog. DialogFragment exportFragment = new ExportDialogFragment(); exportFragment.show(ft, AccountsActivity.FRAGMENT_EXPORT_OFX); } /** * Overrides the {@link SimpleCursorAdapter} to provide custom binding of the * information from the database to the views * @author Ngewi Fet <ngewif@gmail.com> */ private class AccountsCursorAdapter extends SimpleCursorAdapter { TransactionsDbAdapter transactionsDBAdapter; public AccountsCursorAdapter(Context context, int layout, Cursor c, String[] from, int[] to) { super(context, layout, c, from, to, 0); transactionsDBAdapter = new TransactionsDbAdapter(context); } public void close() { transactionsDBAdapter.close(); } @Override public void bindView(View v, Context context, Cursor cursor) { // perform the default binding super.bindView(v, context, cursor); // add a summary of transactions to the account view TextView summary = (TextView) v.findViewById(R.id.transactions_summary); final long accountId = cursor.getLong(DatabaseAdapter.COLUMN_ROW_ID); Money balance = transactionsDBAdapter.getTransactionsSum(accountId); summary.setText(balance.formattedString(Locale.getDefault())); int fontColor = balance.isNegative() ? getResources().getColor(R.color.debit_red) : getResources().getColor(R.color.credit_green); summary.setTextColor(fontColor); ImageView newTrans = (ImageView) v.findViewById(R.id.btn_new_transaction); newTrans.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { Intent intent = new Intent(getActivity(), TransactionsActivity.class); intent.setAction(Intent.ACTION_INSERT_OR_EDIT); intent.putExtra(TransactionsListFragment.SELECTED_ACCOUNT_ID, accountId); getActivity().startActivity(intent); } }); } } /** * Extends {@link DatabaseCursorLoader} for loading of {@link Account} from the * database asynchronously * @author Ngewi Fet <ngewif@gmail.com> */ private static final class AccountsCursorLoader extends DatabaseCursorLoader { public AccountsCursorLoader(Context context) { super(context); } @Override public Cursor loadInBackground() { mDatabaseAdapter = new AccountsDbAdapter(getContext()); Cursor cursor = ((AccountsDbAdapter) mDatabaseAdapter).fetchAllAccounts(); if (cursor != null) registerContentObserver(cursor); return cursor; } } @Override public Loader<Cursor> onCreateLoader(int id, Bundle args) { Log.d(TAG, "Creating the accounts loader"); return new AccountsCursorLoader(this.getActivity().getApplicationContext()); } @Override public void onLoadFinished(Loader<Cursor> loaderCursor, Cursor cursor) { Log.d(TAG, "Accounts loader finished. Swapping in cursor"); mAccountsCursorAdapter.swapCursor(cursor); mAccountsCursorAdapter.notifyDataSetChanged(); } @Override public void onLoaderReset(Loader<Cursor> arg0) { Log.d(TAG, "Resetting the accounts loader"); mAccountsCursorAdapter.swapCursor(null); } }