package co.smartreceipts.android.receipts; import android.app.Activity; import android.app.AlertDialog; import android.content.Context; import android.content.DialogInterface; import android.content.Intent; import android.net.Uri; import android.os.Bundle; import android.support.annotation.NonNull; import android.support.annotation.Nullable; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; import android.view.animation.AnimationUtils; import android.widget.ListView; import android.widget.ProgressBar; import android.widget.TextView; import android.widget.Toast; import com.github.clans.fab.FloatingActionMenu; import com.google.common.base.Preconditions; import java.util.List; import javax.inject.Inject; import co.smartreceipts.android.R; import co.smartreceipts.android.activities.Attachable; import co.smartreceipts.android.activities.NavigationHandler; import co.smartreceipts.android.adapters.ReceiptCardAdapter; import co.smartreceipts.android.analytics.Analytics; import co.smartreceipts.android.analytics.events.Events; import co.smartreceipts.android.config.ConfigurationManager; import co.smartreceipts.android.fragments.ImportPhotoPdfDialogFragment; import co.smartreceipts.android.fragments.ReceiptMoveCopyDialogFragment; import co.smartreceipts.android.fragments.ReportInfoFragment; import co.smartreceipts.android.imports.ActivityFileResultImporter; import co.smartreceipts.android.imports.AttachmentSendFileImporter; import co.smartreceipts.android.imports.CameraInteractionController; import co.smartreceipts.android.imports.RequestCodes; import co.smartreceipts.android.model.Attachment; import co.smartreceipts.android.model.Receipt; import co.smartreceipts.android.model.Trip; import co.smartreceipts.android.model.factory.ReceiptBuilderFactory; import co.smartreceipts.android.ocr.OcrManager; import co.smartreceipts.android.ocr.widget.alert.OcrStatusAlerterPresenter; import co.smartreceipts.android.persistence.PersistenceManager; import co.smartreceipts.android.persistence.database.controllers.ReceiptTableEventsListener; import co.smartreceipts.android.persistence.database.controllers.impl.ReceiptTableController; import co.smartreceipts.android.persistence.database.controllers.impl.StubTableEventsListener; import co.smartreceipts.android.persistence.database.controllers.impl.TripTableController; import co.smartreceipts.android.persistence.database.operations.DatabaseOperationMetadata; import co.smartreceipts.android.sync.BackupProvidersManager; import co.smartreceipts.android.utils.cache.FragmentStateCache; import co.smartreceipts.android.utils.log.Logger; import dagger.android.support.AndroidSupportInjection; import io.reactivex.android.schedulers.AndroidSchedulers; import io.reactivex.disposables.CompositeDisposable; import io.reactivex.schedulers.Schedulers; import wb.android.dialog.BetterDialogBuilder; import wb.android.flex.Flex; public class ReceiptsListFragment extends ReceiptsFragment implements ReceiptTableEventsListener { // Outstate private static final String OUT_HIGHLIGHTED_RECEIPT = "out_highlighted_receipt"; private static final String OUT_IMAGE_URI = "out_image_uri"; @Inject Flex flex; @Inject PersistenceManager persistenceManager; @Inject ConfigurationManager configurationManager; @Inject Analytics analytics; @Inject TripTableController tripTableController; @Inject ReceiptTableController receiptTableController; @Inject BackupProvidersManager backupProvidersManager; @Inject OcrManager ocrManager; @Inject NavigationHandler navigationHandler; @Inject FragmentStateCache fragmentStateCache; private ReceiptCardAdapter adapter; private ActivityFileResultImporter activityFileResultImporter; private Receipt highlightedReceipt; private Uri imageUri; private ProgressBar loadingProgress; private ProgressBar updatingDataProgress; private TextView noDataAlert; private Attachable attachable; private FloatingActionMenu floatingActionMenu; private View floatingActionMenuActiveMaskView; private CompositeDisposable compositeDisposable; private OcrStatusAlerterPresenter ocrStatusAlerterPresenter; private ActionBarSubtitleUpdatesListener actionBarSubtitleUpdatesListener = new ActionBarSubtitleUpdatesListener(); @Override public void onAttach(Context context) { AndroidSupportInjection.inject(this); super.onAttach(context); if (context instanceof Attachable) { attachable = (Attachable) context; } else { throw new ClassCastException("The ReceiptFragment's Activity must extend the Attachable interfaces"); } } @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); Logger.debug(this, "onCreate"); adapter = new ReceiptCardAdapter(getActivity(), navigationHandler, persistenceManager.getPreferenceManager(), backupProvidersManager); if (savedInstanceState != null) { imageUri = savedInstanceState.getParcelable(OUT_IMAGE_URI); highlightedReceipt = savedInstanceState.getParcelable(OUT_HIGHLIGHTED_RECEIPT); } } @Override public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { Logger.debug(this, "onCreateView"); final View rootView = inflater.inflate(R.layout.receipt_fragment_layout, container, false); loadingProgress = (ProgressBar) rootView.findViewById(R.id.progress); updatingDataProgress = (ProgressBar) rootView.findViewById(R.id.progress_adding_new); noDataAlert = (TextView) rootView.findViewById(R.id.no_data); View.OnClickListener listener = new View.OnClickListener() { @Override public void onClick(View v) { final int id = v.getId(); if (id == R.id.receipt_action_camera) { analytics.record(Events.Receipts.AddPictureReceipt); addPictureReceipt(); } else if (id == R.id.receipt_action_text) { analytics.record(Events.Receipts.AddTextReceipt); addTextReceipt(); } else if (id == R.id.receipt_action_import) { analytics.record(Events.Receipts.ImportPictureReceipt); importReceipt(); } } }; rootView.findViewById(R.id.receipt_action_camera).setOnClickListener(listener); rootView.findViewById(R.id.receipt_action_text).setOnClickListener(listener); rootView.findViewById(R.id.receipt_action_import).setOnClickListener(listener); rootView.findViewById(R.id.receipt_action_text).setVisibility(configurationManager.isTextReceiptsOptionAvailable() ? View.VISIBLE : View.GONE); floatingActionMenu = (FloatingActionMenu) rootView.findViewById(R.id.fab_menu); floatingActionMenuActiveMaskView = rootView.findViewById(R.id.fab_active_mask); floatingActionMenuActiveMaskView.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { // Intentional stub to block click events when this view is active } }); floatingActionMenu.setOnMenuToggleListener(new FloatingActionMenu.OnMenuToggleListener() { @Override public void onMenuToggle(boolean isOpen) { // TODO: Animate this change with the buttons appearing for cleaner effect final Context context = floatingActionMenuActiveMaskView.getContext(); if (isOpen) { floatingActionMenuActiveMaskView.startAnimation(AnimationUtils.loadAnimation(context, R.anim.out_from_bottom_right)); floatingActionMenuActiveMaskView.setVisibility(View.VISIBLE); } else { floatingActionMenuActiveMaskView.startAnimation(AnimationUtils.loadAnimation(context, R.anim.in_to_bottom_right)); floatingActionMenuActiveMaskView.setVisibility(View.GONE); } } }); ocrStatusAlerterPresenter = new OcrStatusAlerterPresenter(getActivity(), ocrManager); return rootView; } @Override public void onActivityCreated(Bundle savedInstanceState) { super.onActivityCreated(savedInstanceState); Logger.debug(this, "onActivityCreated"); trip = ((ReportInfoFragment) getParentFragment()).getTrip(); Preconditions.checkNotNull(trip, "A valid trip is required"); activityFileResultImporter = new ActivityFileResultImporter(getActivity(), getFragmentManager(), trip, persistenceManager, analytics, ocrManager); setListAdapter(adapter); // Set this here to ensure this has been laid out already } @Override public void onResume() { super.onResume(); Logger.debug(this, "onResume"); compositeDisposable = new CompositeDisposable(); tripTableController.subscribe(actionBarSubtitleUpdatesListener); receiptTableController.subscribe(this); receiptTableController.get(trip); compositeDisposable.add(activityFileResultImporter.getResultStream() .subscribe(response -> { Logger.info(ReceiptsListFragment.this, "Successfully handled the import of {}", response); switch (response.getRequestCode()) { case RequestCodes.IMPORT_GALLERY_IMAGE: case RequestCodes.IMPORT_GALLERY_PDF: case RequestCodes.NATIVE_NEW_RECEIPT_CAMERA_REQUEST: navigationHandler.navigateToCreateNewReceiptFragment(trip, response.getFile(), response.getOcrResponse()); break; case RequestCodes.NATIVE_ADD_PHOTO_CAMERA_REQUEST: final Receipt updatedReceipt = new ReceiptBuilderFactory(highlightedReceipt).setImage(response.getFile()).build(); receiptTableController.update(highlightedReceipt, updatedReceipt, new DatabaseOperationMetadata()); break; } }, throwable -> { Toast.makeText(getActivity(), getFlexString(R.string.IMG_SAVE_ERROR), Toast.LENGTH_SHORT).show(); highlightedReceipt = null; updatingDataProgress.setVisibility(View.GONE); activityFileResultImporter.dispose(); }, () -> { highlightedReceipt = null; updatingDataProgress.setVisibility(View.GONE); activityFileResultImporter.dispose(); })); ocrStatusAlerterPresenter.onResume(); } @Override public void setUserVisibleHint(boolean isVisibleToUser) { super.setUserVisibleHint(isVisibleToUser); if (getView() != null && isVisibleToUser) { // Refresh as soon as we're visible receiptTableController.get(trip); } } @Override public void onPause() { Logger.debug(this, "onPause"); ocrStatusAlerterPresenter.onPause(); floatingActionMenu.close(false); receiptTableController.unsubscribe(this); tripTableController.unsubscribe(actionBarSubtitleUpdatesListener); compositeDisposable.dispose(); compositeDisposable = null; super.onPause(); } @Override public void onSaveInstanceState(Bundle outState) { super.onSaveInstanceState(outState); Logger.debug(this, "onSaveInstanceState"); outState.putParcelable(OUT_IMAGE_URI, imageUri); outState.putParcelable(OUT_HIGHLIGHTED_RECEIPT, highlightedReceipt); } @Override public void onActivityResult(final int requestCode, final int resultCode, final Intent data) { Logger.debug(this, "onActivityResult"); Logger.debug(this, "Result Code: {}", resultCode); Logger.debug(this, "Request Code: {}", requestCode); // Need to make this call here, since users with "Don't keep activities" will hit this call // before any of onCreate/onStart/onResume is called. This should restore our current trip (what // onResume() would normally do to prevent a variety of crashes that we might encounter if (trip == null) { trip = ((ReportInfoFragment) getParentFragment()).getTrip(); } // Null out the last request final Uri cachedImageSaveLocation = imageUri; imageUri = null; updatingDataProgress.setVisibility(View.VISIBLE); activityFileResultImporter.onActivityResult(requestCode, resultCode, data, cachedImageSaveLocation); if (resultCode != Activity.RESULT_OK) { updatingDataProgress.setVisibility(View.GONE); } } @Override public void onDestroyView() { ocrStatusAlerterPresenter.onDestroyView(); super.onDestroyView(); } @Override public void onDestroy() { fragmentStateCache.onDestroy(this); super.onDestroy(); } @Override protected PersistenceManager getPersistenceManager() { return persistenceManager; } public final void addPictureReceipt() { imageUri = new CameraInteractionController(this).takePhoto(); } public final void addTextReceipt() { navigationHandler.navigateToCreateNewReceiptFragment(trip, null, null); } private void importReceipt() { final ImportPhotoPdfDialogFragment fragment = new ImportPhotoPdfDialogFragment(); fragment.show(getChildFragmentManager(), ImportPhotoPdfDialogFragment.TAG); } public final boolean showReceiptMenu(final Receipt receipt) { highlightedReceipt = receipt; final BetterDialogBuilder builder = new BetterDialogBuilder(getActivity()); builder.setTitle(receipt.getName()).setCancelable(true).setNegativeButton(android.R.string.cancel, new DialogInterface.OnClickListener() { @Override public void onClick(DialogInterface dialog, int id) { dialog.cancel(); } }); final Attachment attachment = attachable.getAttachment(); if (attachment != null && attachment.isDirectlyAttachable()) { final String[] receiptActions; final int attachmentStringId = attachment.isPDF() ? R.string.pdf : R.string.image; final int receiptStringId = receipt.hasPDF() ? R.string.pdf : R.string.image; final String attachFile = getString(R.string.action_send_attach, getString(attachmentStringId)); final String viewFile = getString(R.string.action_send_view, getString(receiptStringId)); final String replaceFile = getString(R.string.action_send_replace, getString(receipt.hasPDF() ? R.string.pdf : R.string.image)); if (receipt.hasFile()) { receiptActions = new String[]{viewFile, replaceFile}; } else { receiptActions = new String[]{attachFile}; } builder.setItems(receiptActions, new DialogInterface.OnClickListener() { @Override public void onClick(DialogInterface dialog, int item) { final String selection = receiptActions[item]; if (selection != null) { if (selection.equals(viewFile)) { // Show File if (attachment.isPDF()) { ReceiptsListFragment.this.showPDF(receipt); } else { ReceiptsListFragment.this.showImage(receipt); } } else if (selection.equals(attachFile) || selection.equals(replaceFile)) { // Attach File to Receipt // TODO: Make this more graceful final AttachmentSendFileImporter importer = new AttachmentSendFileImporter(getActivity(), trip, persistenceManager, receiptTableController, analytics); compositeDisposable.add(importer.importAttachment(attachment, receipt) .subscribeOn(Schedulers.io()) .observeOn(AndroidSchedulers.mainThread()) .subscribe(file -> { // Intentional no-op }, throwable -> Toast.makeText(getActivity(), R.string.database_error, Toast.LENGTH_SHORT).show())); } } } }); } else { final String receiptActionEdit = getString(R.string.receipt_dialog_action_edit); final String receiptActionView = getString(R.string.receipt_dialog_action_view, getString(receipt.hasPDF() ? R.string.pdf : R.string.image)); final String receiptActionCamera = getString(R.string.receipt_dialog_action_camera); final String receiptActionDelete = getString(R.string.receipt_dialog_action_delete); final String receiptActionMoveCopy = getString(R.string.receipt_dialog_action_move_copy); final String receiptActionSwapUp = getString(R.string.receipt_dialog_action_swap_up); final String receiptActionSwapDown = getString(R.string.receipt_dialog_action_swap_down); final String[] receiptActions; if (!receipt.hasFile()) { receiptActions = new String[]{receiptActionEdit, receiptActionCamera, receiptActionDelete, receiptActionMoveCopy, receiptActionSwapUp, receiptActionSwapDown}; } else { receiptActions = new String[]{receiptActionEdit, receiptActionView, receiptActionDelete, receiptActionMoveCopy, receiptActionSwapUp, receiptActionSwapDown}; } builder.setItems(receiptActions, new DialogInterface.OnClickListener() { @Override public void onClick(DialogInterface dialog, int item) { final String selection = receiptActions[item]; if (selection != null) { if (selection.equals(receiptActionEdit)) { // Edit Receipt analytics.record(Events.Receipts.ReceiptMenuEdit); // ReceiptsListFragment.this.receiptMenu(trip, receipt, null); navigationHandler.navigateToEditReceiptFragment(trip, receipt); } else if (selection.equals(receiptActionCamera)) { // Take Photo analytics.record(Events.Receipts.ReceiptMenuRetakePhoto); imageUri = new CameraInteractionController(ReceiptsListFragment.this).addPhoto(); } else if (selection.equals(receiptActionView)) { // View Photo/PDF if (receipt.hasPDF()) { analytics.record(Events.Receipts.ReceiptMenuViewImage); ReceiptsListFragment.this.showPDF(receipt); } else { analytics.record(Events.Receipts.ReceiptMenuViewPdf); ReceiptsListFragment.this.showImage(receipt); } } else if (selection.equals(receiptActionDelete)) { // Delete Receipt analytics.record(Events.Receipts.ReceiptMenuDelete); ReceiptsListFragment.this.deleteReceipt(receipt); } else if (selection.equals(receiptActionMoveCopy)) {// Move-Copy analytics.record(Events.Receipts.ReceiptMenuMoveCopy); ReceiptMoveCopyDialogFragment.newInstance(receipt).show(getFragmentManager(), ReceiptMoveCopyDialogFragment.TAG); } else if (selection.equals(receiptActionSwapUp)) { // Swap Up analytics.record(Events.Receipts.ReceiptMenuSwapUp); receiptTableController.swapUp(receipt); } else if (selection.equals(receiptActionSwapDown)) { // Swap Down analytics.record(Events.Receipts.ReceiptMenuSwapDown); receiptTableController.swapDown(receipt); } } dialog.cancel(); } }); } builder.show(); return true; } private void showImage(@NonNull Receipt receipt) { navigationHandler.navigateToViewReceiptImage(receipt); } private void showPDF(@NonNull Receipt receipt) { navigationHandler.navigateToViewReceiptPdf(receipt); } public final void deleteReceipt(final Receipt receipt) { AlertDialog.Builder builder = new AlertDialog.Builder(getActivity()); builder.setTitle(getString(R.string.delete_item, receipt.getName())).setMessage(R.string.delete_sync_information).setCancelable(true).setPositiveButton(R.string.delete, new DialogInterface.OnClickListener() { @Override public void onClick(DialogInterface dialog, int id) { receiptTableController.delete(receipt, new DatabaseOperationMetadata()); } }).setNegativeButton(android.R.string.cancel, new DialogInterface.OnClickListener() { @Override public void onClick(DialogInterface dialog, int id) { dialog.cancel(); } }).show(); } @Override public void onListItemClick(ListView l, View v, int position, long id) { showReceiptMenu(adapter.getItem(position)); } @Override public void onGetSuccess(@NonNull List<Receipt> receipts, @NonNull Trip trip) { if (isAdded()) { loadingProgress.setVisibility(View.GONE); getListView().setVisibility(View.VISIBLE); if (receipts.isEmpty()) { noDataAlert.setVisibility(View.VISIBLE); } else { noDataAlert.setVisibility(View.INVISIBLE); } adapter.notifyDataSetChanged(receipts); updateActionBarTitle(getUserVisibleHint()); } } @Override public void onGetFailure(@Nullable Throwable e, @NonNull Trip trip) { Toast.makeText(getActivity(), R.string.database_get_error, Toast.LENGTH_LONG).show(); } @Override public void onGetSuccess(@NonNull List<Receipt> list) { // TODO: Respond? } @Override public void onGetFailure(@Nullable Throwable e) { Toast.makeText(getActivity(), R.string.database_get_error, Toast.LENGTH_LONG).show(); } @Override public void onInsertSuccess(@NonNull Receipt receipt, @NonNull DatabaseOperationMetadata databaseOperationMetadata) { if (isResumed()) { receiptTableController.get(trip); } } @Override public void onInsertFailure(@NonNull Receipt receipt, @Nullable Throwable e, @NonNull DatabaseOperationMetadata databaseOperationMetadata) { if (isAdded()) { Toast.makeText(getActivity(), getFlexString(R.string.database_error), Toast.LENGTH_SHORT).show(); } } @Override public void onUpdateSuccess(@NonNull Receipt oldReceipt, @NonNull Receipt newReceipt, @NonNull DatabaseOperationMetadata databaseOperationMetadata) { if (isAdded()) { if (newReceipt.getFile() != null && newReceipt.getFileLastModifiedTime() != oldReceipt.getFileLastModifiedTime()) { final int stringId; if (oldReceipt.getFile() != null) { if (newReceipt.hasImage()) { stringId = R.string.toast_receipt_image_replaced; } else { stringId = R.string.toast_receipt_pdf_replaced; } } else { if (newReceipt.hasImage()) { stringId = R.string.toast_receipt_image_added; } else { stringId = R.string.toast_receipt_pdf_added; } } Toast.makeText(getActivity(), getString(stringId, newReceipt.getName()), Toast.LENGTH_SHORT).show(); final Attachment attachment = attachable.getAttachment(); if (attachment != null && attachment.isDirectlyAttachable()) { attachable.setAttachment(null); getActivity().finish(); } } receiptTableController.get(trip); } } @Override public void onUpdateFailure(@NonNull Receipt oldReceipt, @Nullable Throwable e, @NonNull DatabaseOperationMetadata databaseOperationMetadata) { if (isAdded()) { Toast.makeText(getActivity(), getFlexString(R.string.database_error), Toast.LENGTH_SHORT).show(); } } @Override public void onDeleteSuccess(@NonNull Receipt receipt, @NonNull DatabaseOperationMetadata databaseOperationMetadata) { if (isAdded()) { receiptTableController.get(trip); } } @Override public void onDeleteFailure(@NonNull Receipt receipt, @Nullable Throwable e, @NonNull DatabaseOperationMetadata databaseOperationMetadata) { if (isAdded()) { Toast.makeText(getActivity(), getFlexString(R.string.database_error), Toast.LENGTH_SHORT).show(); } } @Override public void onCopySuccess(@NonNull Receipt oldReceipt, @NonNull Receipt newReceipt) { if (isAdded()) { receiptTableController.get(trip); Toast.makeText(getActivity(), getFlexString(R.string.toast_receipt_copy), Toast.LENGTH_SHORT).show(); } } @Override public void onCopyFailure(@NonNull Receipt oldReceipt, @Nullable Throwable e) { if (isAdded()) { Toast.makeText(getActivity(), getFlexString(R.string.COPY_ERROR), Toast.LENGTH_SHORT).show(); } } @Override public void onSwapSuccess() { receiptTableController.get(trip); } @Override public void onSwapFailure(@Nullable Throwable e) { // TODO: Respond? } @Override public void onMoveSuccess(@NonNull Receipt oldReceipt, @NonNull Receipt newReceipt) { if (isAdded()) { receiptTableController.get(trip); Toast.makeText(getActivity(), getFlexString(R.string.toast_receipt_move), Toast.LENGTH_SHORT).show(); } } @Override public void onMoveFailure(@NonNull Receipt oldReceipt, @Nullable Throwable e) { if (isAdded()) { Toast.makeText(getActivity(), getFlexString(R.string.MOVE_ERROR), Toast.LENGTH_SHORT).show(); } } private class ActionBarSubtitleUpdatesListener extends StubTableEventsListener<Trip> { @Override public void onGetSuccess(@NonNull List<Trip> list) { if (isAdded()) { updateActionBarTitle(getUserVisibleHint()); } } } private String getFlexString(int id) { return getFlexString(flex, id); } }