package co.smartreceipts.android.persistence.database.controllers.alterations; import android.content.Context; import android.support.annotation.NonNull; import android.support.annotation.Nullable; import com.google.common.base.Preconditions; import java.io.File; import java.io.IOException; import java.sql.Date; import java.util.AbstractMap; import java.util.Arrays; import java.util.List; import java.util.Map; import co.smartreceipts.android.model.Receipt; import co.smartreceipts.android.model.Trip; import co.smartreceipts.android.model.factory.BuilderFactory1; import co.smartreceipts.android.model.factory.ReceiptBuilderFactory; import co.smartreceipts.android.model.factory.ReceiptBuilderFactoryFactory; import co.smartreceipts.android.persistence.database.operations.DatabaseOperationMetadata; import co.smartreceipts.android.persistence.database.tables.ReceiptsTable; import co.smartreceipts.android.utils.FileUtils; import co.smartreceipts.android.utils.UriUtils; import co.smartreceipts.android.utils.log.Logger; import io.reactivex.Observable; import io.reactivex.Single; import wb.android.storage.StorageManager; public class ReceiptTableActionAlterations extends StubTableActionAlterations<Receipt> { private final Context context; private final ReceiptsTable mReceiptsTable; private final StorageManager mStorageManager; private final BuilderFactory1<Receipt, ReceiptBuilderFactory> mReceiptBuilderFactoryFactory; public ReceiptTableActionAlterations(@NonNull Context context, @NonNull ReceiptsTable receiptsTable, @NonNull StorageManager storageManager) { this.context = Preconditions.checkNotNull(context); mReceiptsTable = Preconditions.checkNotNull(receiptsTable); mStorageManager = Preconditions.checkNotNull(storageManager); mReceiptBuilderFactoryFactory = new ReceiptBuilderFactoryFactory(); } ReceiptTableActionAlterations(@NonNull Context context, @NonNull ReceiptsTable receiptsTable, @NonNull StorageManager storageManager, @Nullable BuilderFactory1<Receipt, ReceiptBuilderFactory> receiptBuilderFactoryFactory) { this.context = Preconditions.checkNotNull(context); mReceiptsTable = Preconditions.checkNotNull(receiptsTable); mStorageManager = Preconditions.checkNotNull(storageManager); mReceiptBuilderFactoryFactory = Preconditions.checkNotNull(receiptBuilderFactoryFactory); } @NonNull @Override public Single<Receipt> preInsert(@NonNull final Receipt receipt) { return Single.fromCallable(() -> updateReceiptFileNameBlocking(mReceiptBuilderFactoryFactory.build(receipt).setIndex(getNextReceiptIndex(receipt)).build())); } @NonNull @Override public Single<Receipt> preUpdate(@NonNull final Receipt oldReceipt, @NonNull final Receipt newReceipt) { return Single.fromCallable(() -> { if (newReceipt.getFile() != null) { if (!newReceipt.getFile().equals(oldReceipt.getFile())) { // If we changed the receipt file, replace the old file name if (oldReceipt.getFile() != null) { final ReceiptBuilderFactory factory = mReceiptBuilderFactoryFactory.build(newReceipt); final String oldExtension = "." + UriUtils.getExtension(oldReceipt.getFile(), context); final String newExtension = "." + UriUtils.getExtension(newReceipt.getFile(), context); if (newExtension.equals(oldExtension)) { if (newReceipt.getFile().renameTo(oldReceipt.getFile())) { // Note: Keep 'oldReceipt' here, since File is immutable (and renamedTo doesn't change it) factory.setFile(oldReceipt.getFile()); } } else { final String renamedNewFileName = oldReceipt.getFile().getName().replace(oldExtension, newExtension); final String renamedNewFilePath = newReceipt.getFile().getAbsolutePath().replace(newReceipt.getFile().getName(), renamedNewFileName); final File renamedNewFile = new File(renamedNewFilePath); if (newReceipt.getFile().renameTo(renamedNewFile)) { factory.setFile(renamedNewFile); } } return factory.build(); } else { return updateReceiptFileNameBlocking(newReceipt); } } else if (newReceipt.getIndex() != oldReceipt.getIndex()) { return updateReceiptFileNameBlocking(newReceipt); } else { return newReceipt; } } else { return newReceipt; } }); } @NonNull @Override public Single<Receipt> postUpdate(@NonNull final Receipt oldReceipt, @Nullable final Receipt newReceipt) { return Single.fromCallable(() -> { if (newReceipt == null) { throw new Exception("Post update failed due to a null receipt"); } if (oldReceipt.getFile() != null && newReceipt.getFile() != null && !newReceipt.getFile().equals(oldReceipt.getFile())) { // Only delete the old file if we have a new one now... mStorageManager.delete(oldReceipt.getFile()); } return newReceipt; }); } @NonNull @Override public Single<Receipt> postDelete(@Nullable final Receipt receipt) { return Single.fromCallable(() -> { if (receipt == null) { throw new Exception("Post delete failed due to a null receipt"); } if (receipt.getFile() != null) { mStorageManager.delete(receipt.getFile()); } return receipt; }); } @NonNull public Single<Receipt> preCopy(@NonNull final Receipt receipt, @NonNull final Trip toTrip) { return Single.fromCallable(() -> copyReceiptFileBlocking(receipt, toTrip)); } public void postCopy(@NonNull Receipt oldReceipt, @Nullable Receipt newReceipt) throws Exception { // Intentional no-op } @NonNull public Single<Receipt> preMove(@NonNull final Receipt receipt, @NonNull final Trip toTrip) { // Move = Copy + Delete return preCopy(receipt, toTrip); } public void postMove(@NonNull Receipt oldReceipt, @Nullable Receipt newReceipt) throws Exception { if (newReceipt != null) { // i.e. - the move succeeded (delete the old data) Logger.info(this, "Completed the move procedure"); if (mReceiptsTable.delete(oldReceipt, new DatabaseOperationMetadata()).blockingGet() != null) { if (oldReceipt.hasFile()) { if (!mStorageManager.delete(oldReceipt.getFile())) { Logger.error(this, "Failed to delete the moved receipt's file"); } } } else { Logger.error(this, "Failed to delete the moved receipt from the database for it's original trip"); } } } @NonNull public Observable<List<? extends Map.Entry<Receipt, Receipt>>> getReceiptsToSwapUp(@NonNull Receipt receiptToSwapUp, @NonNull List<Receipt> receipts) { final int indexToSwapWith = receipts.indexOf(receiptToSwapUp) - 1; if (indexToSwapWith < 0) { return Observable.error(new RuntimeException("This receipt is at the start of the list already")); } else { final Receipt swappingWith = receipts.get(indexToSwapWith); return Observable.<List<? extends Map.Entry<Receipt, Receipt>>>just(swapDates(receiptToSwapUp, swappingWith, true)); } } @NonNull public Observable<List<? extends Map.Entry<Receipt, Receipt>>> getReceiptsToSwapDown(@NonNull Receipt receiptToSwapDown, @NonNull List<Receipt> receipts) { final int indexToSwapWith = receipts.indexOf(receiptToSwapDown) + 1; if (indexToSwapWith > (receipts.size() - 1)) { return Observable.error(new RuntimeException("This receipt is at the end of the list already")); } else { final Receipt swappingWith = receipts.get(indexToSwapWith); return Observable.<List<? extends Map.Entry<Receipt, Receipt>>>just(swapDates(receiptToSwapDown, swappingWith, false)); } } @NonNull private Receipt updateReceiptFileNameBlocking(@NonNull Receipt receipt) { final ReceiptBuilderFactory builder = mReceiptBuilderFactoryFactory.build(receipt); final StringBuilder stringBuilder = new StringBuilder(receipt.getIndex() + "_"); stringBuilder.append(FileUtils.omitIllegalCharactersFromFileName(receipt.getName().trim())); final File file = receipt.getFile(); if (file != null) { final String extension = UriUtils.getExtension(file, context); stringBuilder.append('.').append(extension); final String newName = stringBuilder.toString(); final File renamedFile = mStorageManager.getFile(receipt.getTrip().getDirectory(), newName); if (!renamedFile.exists()) { Logger.info(this, "Changing image name from: {} to: {}", file.getName(), newName); builder.setFile(mStorageManager.rename(file, newName)); // Returns oldFile on failure } } return builder.build(); } @NonNull private Receipt copyReceiptFileBlocking(@NonNull Receipt receipt, @NonNull Trip toTrip) throws IOException { final ReceiptBuilderFactory builder = mReceiptBuilderFactoryFactory.build(receipt); builder.setTrip(toTrip); if (receipt.hasFile()) { final File destination = mStorageManager.getFile(toTrip.getDirectory(), System.currentTimeMillis() + receipt.getFileName()); if (mStorageManager.copy(receipt.getFile(), destination, true)) { Logger.info(this, "Successfully copied the receipt file to the new trip: {}", toTrip.getName()); builder.setFile(destination); } else { throw new IOException("Failed to copy the receipt file to the new trip: " + toTrip.getName()); } } builder.setIndex(getNextReceiptIndex(receipt)); return updateReceiptFileNameBlocking(builder.build()); } @NonNull private List<? extends Map.Entry<Receipt, Receipt>> swapDates(@NonNull Receipt receipt1, @NonNull Receipt receipt2, boolean isSwappingUp) { final ReceiptBuilderFactory builder1 = mReceiptBuilderFactoryFactory.build(receipt1); final ReceiptBuilderFactory builder2 = mReceiptBuilderFactoryFactory.build(receipt2); long dateShift = 0; if (receipt1.getDate().equals(receipt2.getDate())) { // We shift this way to avoid possible issues wrt sorting order if these are identical dateShift = isSwappingUp ? 1 : -1; } builder1.setDate(new Date(receipt2.getDate().getTime() + dateShift)); builder1.setIndex(receipt2.getIndex()); builder2.setDate(receipt1.getDate()); builder2.setIndex(receipt1.getIndex()); return Arrays.asList(new AbstractMap.SimpleImmutableEntry<>(receipt2, builder2.build()), new AbstractMap.SimpleImmutableEntry<>(receipt1, builder1.build())); } private int getNextReceiptIndex(@NonNull Receipt receipt) { return mReceiptsTable.get(receipt.getTrip()).blockingGet().size() + 1; } }