package co.smartreceipts.android.persistence.database.tables.adapters; import android.content.ContentValues; import android.database.Cursor; import android.support.annotation.NonNull; import android.text.TextUtils; import com.google.common.base.Preconditions; import com.hadisatrio.optional.Optional; import java.io.File; import java.math.BigDecimal; import co.smartreceipts.android.model.Category; import co.smartreceipts.android.model.PaymentMethod; import co.smartreceipts.android.model.Receipt; import co.smartreceipts.android.model.Trip; import co.smartreceipts.android.model.factory.ExchangeRateBuilderFactory; import co.smartreceipts.android.model.factory.ReceiptBuilderFactory; import co.smartreceipts.android.model.impl.ImmutableCategoryImpl; import co.smartreceipts.android.persistence.DatabaseHelper; import co.smartreceipts.android.persistence.database.operations.DatabaseOperationMetadata; import co.smartreceipts.android.persistence.database.operations.OperationFamilyType; import co.smartreceipts.android.persistence.database.tables.ReceiptsTable; import co.smartreceipts.android.persistence.database.tables.Table; import co.smartreceipts.android.persistence.database.tables.keys.PrimaryKey; import co.smartreceipts.android.sync.model.SyncState; import io.reactivex.functions.Function; import wb.android.storage.StorageManager; /** * Implements the {@link DatabaseAdapter} contract for the {@link ReceiptsTable} */ public final class ReceiptDatabaseAdapter implements SelectionBackedDatabaseAdapter<Receipt, PrimaryKey<Receipt, Integer>, Trip> { private final Table<Trip, String> mTripsTable; private final Table<PaymentMethod, Integer> mPaymentMethodTable; private final Table<Category, String> mCategoriesTable; private final StorageManager mStorageManager; private final SyncStateAdapter mSyncStateAdapter; public ReceiptDatabaseAdapter(@NonNull Table<Trip, String> tripsTable, @NonNull Table<PaymentMethod, Integer> paymentMethodTable, @NonNull Table<Category, String> categoriesTable, @NonNull StorageManager storageManager) { this(tripsTable, paymentMethodTable, categoriesTable, storageManager, new SyncStateAdapter()); } public ReceiptDatabaseAdapter(@NonNull Table<Trip, String> tripsTable, @NonNull Table<PaymentMethod, Integer> paymentMethodTable, @NonNull Table<Category, String> categoriesTable, @NonNull StorageManager storageManager, @NonNull SyncStateAdapter syncStateAdapter) { mTripsTable = Preconditions.checkNotNull(tripsTable); mPaymentMethodTable = Preconditions.checkNotNull(paymentMethodTable); mCategoriesTable = Preconditions.checkNotNull(categoriesTable); mStorageManager = Preconditions.checkNotNull(storageManager); mSyncStateAdapter = Preconditions.checkNotNull(syncStateAdapter); } @NonNull @Override public Receipt read(@NonNull Cursor cursor) { final int parentIndex = cursor.getColumnIndex(ReceiptsTable.COLUMN_PARENT); final Trip trip = mTripsTable.findByPrimaryKey(cursor.getString(parentIndex)).blockingGet(); return readForSelection(cursor, trip, true); } @NonNull @Override public Receipt readForSelection(@NonNull Cursor cursor, @NonNull Trip trip, boolean isDescending) { final int idIndex = cursor.getColumnIndex(ReceiptsTable.COLUMN_ID); final int pathIndex = cursor.getColumnIndex(ReceiptsTable.COLUMN_PATH); final int nameIndex = cursor.getColumnIndex(ReceiptsTable.COLUMN_NAME); final int categoryIndex = cursor.getColumnIndex(ReceiptsTable.COLUMN_CATEGORY); final int priceIndex = cursor.getColumnIndex(ReceiptsTable.COLUMN_PRICE); final int taxIndex = cursor.getColumnIndex(ReceiptsTable.COLUMN_TAX); final int exchangeRateIndex = cursor.getColumnIndex(ReceiptsTable.COLUMN_EXCHANGE_RATE); final int dateIndex = cursor.getColumnIndex(ReceiptsTable.COLUMN_DATE); final int timeZoneIndex = cursor.getColumnIndex(ReceiptsTable.COLUMN_TIMEZONE); final int commentIndex = cursor.getColumnIndex(ReceiptsTable.COLUMN_COMMENT); final int reimbursableIndex = cursor.getColumnIndex(ReceiptsTable.COLUMN_REIMBURSABLE); final int currencyIndex = cursor.getColumnIndex(ReceiptsTable.COLUMN_ISO4217); final int fullpageIndex = cursor.getColumnIndex(ReceiptsTable.COLUMN_NOTFULLPAGEIMAGE); final int paymentMethodIdIndex = cursor.getColumnIndex(ReceiptsTable.COLUMN_PAYMENT_METHOD_ID); final int extra_edittext_1_Index = cursor.getColumnIndex(ReceiptsTable.COLUMN_EXTRA_EDITTEXT_1); final int extra_edittext_2_Index = cursor.getColumnIndex(ReceiptsTable.COLUMN_EXTRA_EDITTEXT_2); final int extra_edittext_3_Index = cursor.getColumnIndex(ReceiptsTable.COLUMN_EXTRA_EDITTEXT_3); final int id = cursor.getInt(idIndex); final String path = cursor.getString(pathIndex); final String name = cursor.getString(nameIndex); final String category = cursor.getString(categoryIndex); final double priceDouble = cursor.getDouble(priceIndex); final double taxDouble = cursor.getDouble(taxIndex); final double exchangeRateDouble = cursor.getDouble(exchangeRateIndex); final String priceString = cursor.getString(priceIndex); final String taxString = cursor.getString(taxIndex); final String exchangeRateString = cursor.getString(exchangeRateIndex); final long date = cursor.getLong(dateIndex); final String timezone = (timeZoneIndex > 0) ? cursor.getString(timeZoneIndex) : null; final String comment = cursor.getString(commentIndex); final boolean reimbursable = cursor.getInt(reimbursableIndex) > 0; final String currency = cursor.getString(currencyIndex); final boolean fullpage = !(cursor.getInt(fullpageIndex) > 0); final int paymentMethodId = cursor.getInt(paymentMethodIdIndex); final String extra_edittext_1 = cursor.getString(extra_edittext_1_Index); final String extra_edittext_2 = cursor.getString(extra_edittext_2_Index); final String extra_edittext_3 = cursor.getString(extra_edittext_3_Index); File file = null; if (!TextUtils.isEmpty(path) && !DatabaseHelper.NO_DATA.equals(path)) { file = mStorageManager.getFile(trip.getDirectory(), path); } final SyncState syncState = mSyncStateAdapter.read(cursor); // TODO: How to use JOINs w/o blocking final Category categoryImpl = mCategoriesTable.findByPrimaryKey(category).onErrorReturn(ignored -> new ImmutableCategoryImpl(category, category)).blockingGet(); final Optional<PaymentMethod> paymentMethodOptional = mPaymentMethodTable.findByPrimaryKey(paymentMethodId) .map(Optional::of) .onErrorReturn(ignored -> Optional.absent()) .blockingGet(); final int index = isDescending ? cursor.getCount() - cursor.getPosition() : cursor.getPosition() + 1; final ReceiptBuilderFactory builder = new ReceiptBuilderFactory(id); builder.setTrip(trip) .setName(name) .setCategory(categoryImpl) .setFile(file) .setDate(date) .setTimeZone(timezone) .setComment(comment) .setIsReimbursable(reimbursable) .setCurrency(currency) .setIsFullPage(fullpage) .setIndex(index) .setPaymentMethod(paymentMethodOptional.orNull()) .setExtraEditText1(extra_edittext_1) .setExtraEditText2(extra_edittext_2) .setExtraEditText3(extra_edittext_3) .setSyncState(syncState); /* * Please note that a very frustrating bug exists here. Android cursors only return the first 6 * characters of a price string if that string contains a '.' character. It returns all of them * if not. This means we'll break for prices over 5 digits unless we are using a comma separator, * which we'd do in the EU. Stupid check below to un-break this. Stupid Android. * * TODO: Longer term, everything should be saved with a decimal point * https://code.google.com/p/android/issues/detail?id=22219 */ if (!TextUtils.isEmpty(priceString) && priceString.contains(",")) { builder.setPrice(priceString); } else { builder.setPrice(priceDouble); } if (!TextUtils.isEmpty(taxString) && taxString.contains(",")) { builder.setTax(taxString); } else { builder.setTax(taxDouble); } final ExchangeRateBuilderFactory exchangeRateBuilder = new ExchangeRateBuilderFactory().setBaseCurrency(currency); if (!TextUtils.isEmpty(exchangeRateString) && exchangeRateString.contains(",")) { exchangeRateBuilder.setRate(trip.getTripCurrency(), exchangeRateString); } else { exchangeRateBuilder.setRate(trip.getTripCurrency(), exchangeRateDouble); } builder.setExchangeRate(exchangeRateBuilder.build()); return builder.build(); } @NonNull @Override public ContentValues write(@NonNull Receipt receipt, @NonNull DatabaseOperationMetadata databaseOperationMetadata) { final ContentValues values = new ContentValues(); // Add core data values.put(ReceiptsTable.COLUMN_PARENT, receipt.getTrip().getName()); values.put(ReceiptsTable.COLUMN_NAME, receipt.getName().trim()); values.put(ReceiptsTable.COLUMN_CATEGORY, receipt.getCategory().getName()); values.put(ReceiptsTable.COLUMN_DATE, receipt.getDate().getTime()); values.put(ReceiptsTable.COLUMN_TIMEZONE, receipt.getTimeZone().getID()); values.put(ReceiptsTable.COLUMN_COMMENT, receipt.getComment()); values.put(ReceiptsTable.COLUMN_ISO4217, receipt.getPrice().getCurrencyCode()); values.put(ReceiptsTable.COLUMN_REIMBURSABLE, receipt.isReimbursable()); values.put(ReceiptsTable.COLUMN_NOTFULLPAGEIMAGE, !receipt.isFullPage()); // Add file final File file = receipt.getFile(); if (file != null) { values.put(ReceiptsTable.COLUMN_PATH, file.getName()); } else { values.put(ReceiptsTable.COLUMN_PATH, (String) null); } // Add payment method if one exists if (receipt.getPaymentMethod() != null) { values.put(ReceiptsTable.COLUMN_PAYMENT_METHOD_ID, receipt.getPaymentMethod().getId()); } // Note: We replace the commas here with decimals to avoid database bugs around parsing decimal values // TODO: Ensure this logic works for prices like "1,234.56" values.put(ReceiptsTable.COLUMN_PRICE, receipt.getPrice().getPrice().doubleValue()); values.put(ReceiptsTable.COLUMN_TAX, receipt.getTax().getPrice().doubleValue()); final BigDecimal exchangeRate = receipt.getPrice().getExchangeRate().getExchangeRate(receipt.getTrip().getDefaultCurrencyCode()); if (exchangeRate != null) { values.put(ReceiptsTable.COLUMN_EXCHANGE_RATE, exchangeRate.doubleValue()); } // Add extras values.put(ReceiptsTable.COLUMN_EXTRA_EDITTEXT_1, receipt.getExtraEditText1()); values.put(ReceiptsTable.COLUMN_EXTRA_EDITTEXT_2, receipt.getExtraEditText2()); values.put(ReceiptsTable.COLUMN_EXTRA_EDITTEXT_3, receipt.getExtraEditText3()); if (databaseOperationMetadata.getOperationFamilyType() == OperationFamilyType.Sync) { values.putAll(mSyncStateAdapter.write(receipt.getSyncState())); } else { values.putAll(mSyncStateAdapter.writeUnsynced(receipt.getSyncState())); } return values; } @NonNull @Override public Receipt build(@NonNull Receipt receipt, @NonNull PrimaryKey<Receipt, Integer> primaryKey, @NonNull DatabaseOperationMetadata databaseOperationMetadata) { return new ReceiptBuilderFactory(primaryKey.getPrimaryKeyValue(receipt), receipt).setSyncState(mSyncStateAdapter.get(receipt.getSyncState(), databaseOperationMetadata)).build(); } }