org.gnucash.android.export.ExportAsyncTask.java Source code

Java tutorial

Introduction

Here is the source code for org.gnucash.android.export.ExportAsyncTask.java

Source

/*
 * Copyright (c) 2013 - 2015 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.export;

import android.annotation.TargetApi;
import android.app.Activity;
import android.app.ProgressDialog;
import android.content.Context;
import android.content.Intent;
import android.content.SharedPreferences;
import android.content.pm.ResolveInfo;
import android.database.sqlite.SQLiteDatabase;
import android.net.Uri;
import android.os.AsyncTask;
import android.os.Build;
import android.preference.PreferenceManager;
import android.support.annotation.NonNull;
import android.support.v4.content.FileProvider;
import android.util.Log;
import android.widget.Toast;

import com.crashlytics.android.Crashlytics;
import com.dropbox.core.v2.DbxClientV2;
import com.dropbox.core.v2.files.FileMetadata;
import com.dropbox.core.v2.files.Metadata;
import com.google.android.gms.common.api.GoogleApiClient;
import com.google.android.gms.drive.Drive;
import com.google.android.gms.drive.DriveApi;
import com.google.android.gms.drive.DriveContents;
import com.google.android.gms.drive.DriveFolder;
import com.google.android.gms.drive.DriveId;
import com.google.android.gms.drive.MetadataChangeSet;
import com.owncloud.android.lib.common.OwnCloudClient;
import com.owncloud.android.lib.common.OwnCloudClientFactory;
import com.owncloud.android.lib.common.OwnCloudCredentialsFactory;
import com.owncloud.android.lib.common.operations.RemoteOperationResult;
import com.owncloud.android.lib.resources.files.CreateRemoteFolderOperation;
import com.owncloud.android.lib.resources.files.FileUtils;
import com.owncloud.android.lib.resources.files.UploadRemoteFileOperation;

import org.gnucash.android.R;
import org.gnucash.android.app.GnuCashApplication;
import org.gnucash.android.db.adapter.AccountsDbAdapter;
import org.gnucash.android.db.adapter.DatabaseAdapter;
import org.gnucash.android.db.adapter.SplitsDbAdapter;
import org.gnucash.android.db.adapter.TransactionsDbAdapter;
import org.gnucash.android.export.ofx.OfxExporter;
import org.gnucash.android.export.qif.QifExporter;
import org.gnucash.android.export.xml.GncXmlExporter;
import org.gnucash.android.model.Transaction;
import org.gnucash.android.ui.account.AccountsActivity;
import org.gnucash.android.ui.account.AccountsListFragment;
import org.gnucash.android.ui.settings.BackupPreferenceFragment;
import org.gnucash.android.ui.transaction.TransactionsActivity;

import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.nio.channels.FileChannel;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
import java.util.concurrent.TimeUnit;

/**
 * Asynchronous task for exporting transactions.
 *
 * @author Ngewi Fet <ngewif@gmail.com>
 */
public class ExportAsyncTask extends AsyncTask<ExportParams, Void, Boolean> {

    /**
     * App context
     */
    private final Context mContext;

    private ProgressDialog mProgressDialog;

    private SQLiteDatabase mDb;

    /**
     * Log tag
     */
    public static final String TAG = "ExportAsyncTask";

    /**
     * Export parameters
     */
    private ExportParams mExportParams;

    // File paths generated by the exporter
    private List<String> mExportedFiles;

    private Exporter mExporter;

    public ExportAsyncTask(Context context, SQLiteDatabase db) {
        this.mContext = context;
        this.mDb = db;
    }

    @Override
    @TargetApi(11)
    protected void onPreExecute() {
        super.onPreExecute();
        if (mContext instanceof Activity) {
            mProgressDialog = new ProgressDialog(mContext);
            mProgressDialog.setTitle(R.string.title_progress_exporting_transactions);
            mProgressDialog.setIndeterminate(true);
            mProgressDialog.setProgressStyle(ProgressDialog.STYLE_HORIZONTAL);
            if (Build.VERSION.SDK_INT > Build.VERSION_CODES.HONEYCOMB) {
                mProgressDialog.setProgressNumberFormat(null);
                mProgressDialog.setProgressPercentFormat(null);
            }
            mProgressDialog.show();
        }
    }

    /**
     * Generates the appropriate exported transactions file for the given parameters
     * @param params Export parameters
     * @return <code>true</code> if export was successful, <code>false</code> otherwise
     */
    @Override
    protected Boolean doInBackground(ExportParams... params) {
        mExportParams = params[0];
        mExporter = getExporter();

        try {
            mExportedFiles = mExporter.generateExport();
        } catch (final Exception e) {
            Log.e(TAG, "Error exporting: " + e.getMessage());
            Crashlytics.logException(e);
            e.printStackTrace();
            if (mContext instanceof Activity) {
                ((Activity) mContext).runOnUiThread(new Runnable() {
                    @Override
                    public void run() {
                        Toast.makeText(mContext,
                                mContext.getString(R.string.toast_export_error,
                                        mExportParams.getExportFormat().name()) + "\n" + e.getMessage(),
                                Toast.LENGTH_SHORT).show();
                    }
                });
            }
            return false;
        }

        if (mExportedFiles.isEmpty())
            return false;

        try {
            moveToTarget();
        } catch (Exporter.ExporterException e) {
            Crashlytics.log(Log.ERROR, TAG, "Error sending exported files to target: " + e.getMessage());
            return false;
        }

        return true;
    }

    /**
     * Transmits the exported transactions to the designated location, either SD card or third-party application
     * Finishes the activity if the export was starting  in the context of an activity
     * @param exportSuccessful Result of background export execution
     */
    @Override
    protected void onPostExecute(Boolean exportSuccessful) {
        if (exportSuccessful) {
            if (mContext instanceof Activity)
                reportSuccess();

            if (mExportParams.shouldDeleteTransactionsAfterExport()) {
                backupAndDeleteTransactions();
                refreshViews();
            }
        } else {
            if (mContext instanceof Activity) {
                dismissProgressDialog();
                if (mExportedFiles.isEmpty()) {
                    Toast.makeText(mContext, R.string.toast_no_transactions_to_export, Toast.LENGTH_LONG).show();
                } else {
                    Toast.makeText(mContext,
                            mContext.getString(R.string.toast_export_error, mExportParams.getExportFormat().name()),
                            Toast.LENGTH_LONG).show();
                }
            }
        }

        dismissProgressDialog();
    }

    private void dismissProgressDialog() {
        if (mContext instanceof Activity) {
            if (mProgressDialog != null && mProgressDialog.isShowing())
                mProgressDialog.dismiss();
            ((Activity) mContext).finish();
        }
    }

    /**
     * Returns an exporter corresponding to the user settings.
     * @return Object of one of {@link QifExporter}, {@link OfxExporter} or {@link GncXmlExporter}
     */
    private Exporter getExporter() {
        switch (mExportParams.getExportFormat()) {
        case QIF:
            return new QifExporter(mExportParams, mDb);

        case OFX:
            return new OfxExporter(mExportParams, mDb);

        case XML:
        default:
            return new GncXmlExporter(mExportParams, mDb);
        }
    }

    /**
     * Moves the generated export files to the target specified by the user
     * @throws Exporter.ExporterException if the move fails
     */
    private void moveToTarget() throws Exporter.ExporterException {
        switch (mExportParams.getExportTarget()) {
        case SHARING:
            shareFiles(mExportedFiles);
            break;

        case DROPBOX:
            moveExportToDropbox();
            break;

        case GOOGLE_DRIVE:
            moveExportToGoogleDrive();
            break;

        case OWNCLOUD:
            moveExportToOwnCloud();
            break;

        case SD_CARD:
            moveExportToSDCard();
            break;

        default:
            throw new Exporter.ExporterException(mExportParams, "Invalid target");
        }
    }

    private void moveExportToGoogleDrive() throws Exporter.ExporterException {
        Log.i(TAG, "Moving exported file to Google Drive");
        final GoogleApiClient googleApiClient = BackupPreferenceFragment
                .getGoogleApiClient(GnuCashApplication.getAppContext());
        googleApiClient.blockingConnect();

        DriveApi.DriveContentsResult driveContentsResult = Drive.DriveApi.newDriveContents(googleApiClient).await(1,
                TimeUnit.MINUTES);
        if (!driveContentsResult.getStatus().isSuccess()) {
            throw new Exporter.ExporterException(mExportParams, "Error while trying to create new file contents");
        }
        final DriveContents driveContents = driveContentsResult.getDriveContents();
        DriveFolder.DriveFileResult driveFileResult = null;
        try {
            // write content to DriveContents
            OutputStream outputStream = driveContents.getOutputStream();
            for (String exportedFilePath : mExportedFiles) {
                File exportedFile = new File(exportedFilePath);
                FileInputStream fileInputStream = new FileInputStream(exportedFile);
                byte[] buffer = new byte[1024];
                int count;

                while ((count = fileInputStream.read(buffer)) >= 0) {
                    outputStream.write(buffer, 0, count);
                }
                fileInputStream.close();
                outputStream.flush();
                exportedFile.delete();

                MetadataChangeSet changeSet = new MetadataChangeSet.Builder().setTitle(exportedFile.getName())
                        .setMimeType(mExporter.getExportMimeType()).build();

                SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(mContext);
                String folderId = sharedPreferences
                        .getString(mContext.getString(R.string.key_google_drive_app_folder_id), "");
                DriveFolder folder = Drive.DriveApi.getFolder(googleApiClient, DriveId.decodeFromString(folderId));
                // create a file on root folder
                driveFileResult = folder.createFile(googleApiClient, changeSet, driveContents).await(1,
                        TimeUnit.MINUTES);
            }
        } catch (IOException e) {
            throw new Exporter.ExporterException(mExportParams, e);
        }

        if (driveFileResult == null)
            throw new Exporter.ExporterException(mExportParams, "No result received");

        if (!driveFileResult.getStatus().isSuccess())
            throw new Exporter.ExporterException(mExportParams, "Error creating file in Google Drive");

        Log.i(TAG, "Created file with id: " + driveFileResult.getDriveFile().getDriveId());
    }

    /**
     * Move the exported files (in the cache directory) to Dropbox
     */
    private void moveExportToDropbox() {
        Log.i(TAG, "Uploading exported files to DropBox");

        DbxClientV2 dbxClient = DropboxHelper.getClient();

        for (String exportedFilePath : mExportedFiles) {
            File exportedFile = new File(exportedFilePath);
            FileInputStream inputStream = null;
            try {
                inputStream = new FileInputStream(exportedFile);
                List<Metadata> entries = dbxClient.files().listFolder("").getEntries();

                FileMetadata metadata = dbxClient.files().uploadBuilder("/" + exportedFile.getName())
                        .uploadAndFinish(inputStream);
                Log.i(TAG, "Successfully uploaded file " + metadata.getName() + " to DropBox");
                inputStream.close();
                exportedFile.delete(); //delete file to prevent cache accumulation
            } catch (IOException e) {
                Crashlytics.logException(e);
                Log.e(TAG, e.getMessage());
            } catch (com.dropbox.core.DbxException e) {
                e.printStackTrace();
            }
        }
    }

    private void moveExportToOwnCloud() throws Exporter.ExporterException {
        Log.i(TAG, "Copying exported file to ownCloud");

        SharedPreferences mPrefs = mContext.getSharedPreferences(mContext.getString(R.string.owncloud_pref),
                Context.MODE_PRIVATE);

        Boolean mOC_sync = mPrefs.getBoolean(mContext.getString(R.string.owncloud_sync), false);

        if (!mOC_sync) {
            throw new Exporter.ExporterException(mExportParams, "ownCloud not enabled.");
        }

        String mOC_server = mPrefs.getString(mContext.getString(R.string.key_owncloud_server), null);
        String mOC_username = mPrefs.getString(mContext.getString(R.string.key_owncloud_username), null);
        String mOC_password = mPrefs.getString(mContext.getString(R.string.key_owncloud_password), null);
        String mOC_dir = mPrefs.getString(mContext.getString(R.string.key_owncloud_dir), null);

        Uri serverUri = Uri.parse(mOC_server);
        OwnCloudClient mClient = OwnCloudClientFactory.createOwnCloudClient(serverUri, this.mContext, true);
        mClient.setCredentials(OwnCloudCredentialsFactory.newBasicCredentials(mOC_username, mOC_password));

        if (mOC_dir.length() != 0) {
            RemoteOperationResult dirResult = new CreateRemoteFolderOperation(mOC_dir, true).execute(mClient);
            if (!dirResult.isSuccess()) {
                Log.w(TAG,
                        "Error creating folder (it may happen if it already exists): " + dirResult.getLogMessage());
            }
        }
        for (String exportedFilePath : mExportedFiles) {
            String remotePath = mOC_dir + FileUtils.PATH_SEPARATOR + stripPathPart(exportedFilePath);
            String mimeType = mExporter.getExportMimeType();

            RemoteOperationResult result = new UploadRemoteFileOperation(exportedFilePath, remotePath, mimeType)
                    .execute(mClient);

            if (!result.isSuccess())
                throw new Exporter.ExporterException(mExportParams, result.getLogMessage());

            new File(exportedFilePath).delete();
        }
    }

    /**
     * Moves the exported files from the internal storage where they are generated to
     * external storage, which is accessible to the user.
     * @return The list of files moved to the SD card.
     */
    private List<String> moveExportToSDCard() throws Exporter.ExporterException {
        Log.i(TAG, "Moving exported file to external storage");
        new File(Exporter.getExportFolderPath(mExporter.mBookUID));
        List<String> dstFiles = new ArrayList<>();

        for (String src : mExportedFiles) {
            String dst = Exporter.getExportFolderPath(mExporter.mBookUID) + stripPathPart(src);
            try {
                moveFile(src, dst);
                dstFiles.add(dst);
            } catch (IOException e) {
                throw new Exporter.ExporterException(mExportParams, e);
            }
        }

        return dstFiles;
    }

    // "/some/path/filename.ext" -> "filename.ext"
    private String stripPathPart(String fullPathName) {
        return (new File(fullPathName)).getName();
    }

    /**
     * Backups of the database, saves opening balances (if necessary)
     * and deletes all non-template transactions in the database.
     */
    private void backupAndDeleteTransactions() {
        Log.i(TAG, "Backup and deleting transactions after export");
        GncXmlExporter.createBackup(); //create backup before deleting everything
        List<Transaction> openingBalances = new ArrayList<>();
        boolean preserveOpeningBalances = GnuCashApplication.shouldSaveOpeningBalances(false);

        TransactionsDbAdapter transactionsDbAdapter = new TransactionsDbAdapter(mDb, new SplitsDbAdapter(mDb));
        if (preserveOpeningBalances) {
            openingBalances = new AccountsDbAdapter(mDb, transactionsDbAdapter).getAllOpeningBalanceTransactions();
        }
        transactionsDbAdapter.deleteAllNonTemplateTransactions();

        if (preserveOpeningBalances) {
            transactionsDbAdapter.bulkAddRecords(openingBalances, DatabaseAdapter.UpdateMethod.insert);
        }
    }

    /**
     * Starts an intent chooser to allow the user to select an activity to receive
     * the exported files.
     * @param paths list of full paths of the files to send to the activity.
     */
    private void shareFiles(List<String> paths) {
        Intent shareIntent = new Intent(Intent.ACTION_SEND_MULTIPLE);
        shareIntent.setType("text/xml");

        ArrayList<Uri> exportFiles = convertFilePathsToUris(paths);
        shareIntent.putParcelableArrayListExtra(Intent.EXTRA_STREAM, exportFiles);
        shareIntent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);

        shareIntent.putExtra(Intent.EXTRA_SUBJECT,
                mContext.getString(R.string.title_export_email, mExportParams.getExportFormat().name()));

        String defaultEmail = PreferenceManager.getDefaultSharedPreferences(mContext)
                .getString(mContext.getString(R.string.key_default_export_email), null);
        if (defaultEmail != null && defaultEmail.trim().length() > 0)
            shareIntent.putExtra(Intent.EXTRA_EMAIL, new String[] { defaultEmail });

        SimpleDateFormat formatter = (SimpleDateFormat) SimpleDateFormat.getDateTimeInstance();
        ArrayList<CharSequence> extraText = new ArrayList<>();
        extraText.add(mContext.getString(R.string.description_export_email) + " "
                + formatter.format(new Date(System.currentTimeMillis())));
        shareIntent.putExtra(Intent.EXTRA_TEXT, extraText);

        if (mContext instanceof Activity) {
            List<ResolveInfo> activities = mContext.getPackageManager().queryIntentActivities(shareIntent, 0);
            if (activities != null && !activities.isEmpty()) {
                mContext.startActivity(Intent.createChooser(shareIntent,
                        mContext.getString(R.string.title_select_export_destination)));
            } else {
                Toast.makeText(mContext, R.string.toast_no_compatible_apps_to_receive_export, Toast.LENGTH_LONG)
                        .show();
            }
        }
    }

    /**
     * Convert file paths to URIs by adding the file// prefix
     * <p>e.g. /some/path/file.ext --> file:///some/path/file.ext</p>
     * @param paths List of file paths to convert
     * @return List of file URIs
     */
    @NonNull
    private ArrayList<Uri> convertFilePathsToUris(List<String> paths) {
        ArrayList<Uri> exportFiles = new ArrayList<>();

        for (String path : paths) {
            File file = new File(path);
            Uri contentUri = FileProvider.getUriForFile(GnuCashApplication.getAppContext(),
                    GnuCashApplication.FILE_PROVIDER_AUTHORITY, file);
            exportFiles.add(contentUri);
        }
        return exportFiles;
    }

    /**
     * Moves a file from <code>src</code> to <code>dst</code>
     * @param src Absolute path to the source file
     * @param dst Absolute path to the destination file
     * @throws IOException if the file could not be moved.
     */
    public void moveFile(String src, String dst) throws IOException {
        File srcFile = new File(src);
        File dstFile = new File(dst);
        FileChannel inChannel = new FileInputStream(srcFile).getChannel();
        FileChannel outChannel = new FileOutputStream(dstFile).getChannel();
        try {
            inChannel.transferTo(0, inChannel.size(), outChannel);
        } finally {
            if (inChannel != null)
                inChannel.close();
            outChannel.close();
        }
        srcFile.delete();
    }

    private void reportSuccess() {
        String targetLocation;
        switch (mExportParams.getExportTarget()) {
        case SD_CARD:
            targetLocation = "SD card";
            break;
        case DROPBOX:
            targetLocation = "DropBox -> Apps -> GnuCash";
            break;
        case GOOGLE_DRIVE:
            targetLocation = "Google Drive -> " + mContext.getString(R.string.app_name);
            break;
        case OWNCLOUD:
            targetLocation = mContext
                    .getSharedPreferences(mContext.getString(R.string.owncloud_pref), Context.MODE_PRIVATE)
                    .getBoolean(mContext.getString(R.string.owncloud_sync), false) ?

                            "ownCloud -> " + mContext
                                    .getSharedPreferences(mContext.getString(R.string.owncloud_pref),
                                            Context.MODE_PRIVATE)
                                    .getString(mContext.getString(R.string.key_owncloud_dir), null)
                            : "ownCloud sync not enabled";
            break;
        default:
            targetLocation = mContext.getString(R.string.label_export_target_external_service);
        }
        Toast.makeText(mContext, String.format(mContext.getString(R.string.toast_exported_to), targetLocation),
                Toast.LENGTH_LONG).show();
    }

    private void refreshViews() {
        if (mContext instanceof AccountsActivity) {
            AccountsListFragment fragment = ((AccountsActivity) mContext).getCurrentAccountListFragment();
            if (fragment != null)
                fragment.refresh();
        }
        if (mContext instanceof TransactionsActivity) {
            ((TransactionsActivity) mContext).refresh();
        }
    }
}