package ca.josephroque.bowlingcompanion.fragment;
import android.content.DialogInterface;
import android.graphics.PorterDuff;
import android.os.AsyncTask;
import android.os.Bundle;
import android.support.v4.app.Fragment;
import android.support.v7.app.AlertDialog;
import android.support.v7.widget.CardView;
import android.text.Editable;
import android.text.TextUtils;
import android.text.TextWatcher;
import android.util.Log;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.Button;
import android.widget.EditText;
import android.widget.LinearLayout;
import android.widget.ProgressBar;
import android.widget.ScrollView;
import android.widget.TextView;
import java.io.BufferedReader;
import java.io.DataInputStream;
import java.io.DataOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.lang.ref.WeakReference;
import java.net.HttpURLConnection;
import java.net.MalformedURLException;
import java.net.SocketTimeoutException;
import java.net.URL;
import java.util.Locale;
import ca.josephroque.bowlingcompanion.MainActivity;
import ca.josephroque.bowlingcompanion.R;
import ca.josephroque.bowlingcompanion.database.DatabaseHelper;
import ca.josephroque.bowlingcompanion.theme.Theme;
import ca.josephroque.bowlingcompanion.utilities.DisplayUtils;
import ca.josephroque.bowlingcompanion.utilities.FileUtils;
import ca.josephroque.bowlingcompanion.utilities.NavigationController;
import ca.josephroque.bowlingcompanion.utilities.TransferUtils;
/**
* A {@link android.support.v4.app.Fragment} for users to upload or download data from a remote server to back up their
* data.
*/
public final class TransferFragment
extends Fragment
implements Theme.ChangeableTheme {
/** Identifies output from this class in Logcat. */
@SuppressWarnings("unused")
private static final String TAG = "TransferFragment";
/** Represents the number of failed imports. */
private static final String IMPORT_FAILURES = "im_fail";
/** Represents the number of failed exports. */
private static final String EXPORT_FAILURES = "ex_fail";
/** Represents the last transfer key the user received from the server. */
private static final String LAST_KEY_RECEIVED = "last_key";
/** Represents the import cardview. */
private static final byte VIEW_IMPORT = 0;
/** Represents the export cardview. */
private static final byte VIEW_EXPORT = 1;
/** Represents the restore/delete cardview. */
private static final byte VIEW_RESTORE_DELETE = 2;
/** The current {@link android.os.AsyncTask} being executed, so it can be cancelled if necessary. */
private AsyncTask<Boolean, Integer, String> mCurrentTransferTask = null;
/** LinearLayout that contains the views of the Fragment. */
private LinearLayout mLinearLayoutRoot = null;
/** Reference to the last view which was added to the layout. */
private CardView mLastViewAdded = null;
/** Indicates if the fragment is currently showing the import, export, or restore/delete options. */
private int mCurrentCardView = -1;
/** Number of times importing data failed. */
private int mImportFailures = 0;
/** Number of times exporting data failed. */
private int mExportFailures = 0;
/** Last key that the user received from the server after a successful upload. */
private String mLastKeyReceived = null;
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState) {
final ScrollView rootView = (ScrollView) inflater.inflate(R.layout.fragment_transfer, container, false);
View.OnClickListener cardClickListener = new View.OnClickListener() {
@Override
public void onClick(View v) {
if (mCurrentTransferTask == null) {
if (v.getId() == R.id.btn_import)
showCardView(VIEW_IMPORT);
else if (v.getId() == R.id.btn_export)
showCardView(VIEW_EXPORT);
else if (v.getId() == R.id.btn_restore_delete)
showCardView(VIEW_RESTORE_DELETE);
rootView.post(new Runnable() {
@Override
public void run() {
rootView.fullScroll(View.FOCUS_DOWN);
}
});
}
}
};
rootView.findViewById(R.id.btn_import).setOnClickListener(cardClickListener);
rootView.findViewById(R.id.btn_export).setOnClickListener(cardClickListener);
rootView.findViewById(R.id.btn_restore_delete).setOnClickListener(cardClickListener);
mLinearLayoutRoot = (LinearLayout) rootView.findViewById(R.id.ll_transfer);
if (savedInstanceState != null) {
mImportFailures = savedInstanceState.getInt(IMPORT_FAILURES, 0);
mExportFailures = savedInstanceState.getInt(EXPORT_FAILURES, 0);
mLastKeyReceived = savedInstanceState.getString(LAST_KEY_RECEIVED, null);
}
return rootView;
}
@Override
public void onResume() {
super.onResume();
if (getActivity() != null) {
MainActivity mainActivity = (MainActivity) getActivity();
mainActivity.setActionBarTitle(R.string.title_fragment_transfer, true);
mainActivity.setDrawerState(false);
mainActivity.setFloatingActionButtonState(0, 0);
}
}
@Override
public void onSaveInstanceState(Bundle outState) {
super.onSaveInstanceState(outState);
outState.putInt(IMPORT_FAILURES, mImportFailures);
outState.putInt(EXPORT_FAILURES, mExportFailures);
outState.putString(LAST_KEY_RECEIVED, mLastKeyReceived);
}
@Override
public void updateTheme() {
}
/**
* Removes {@code mLastViewAdded} from {@code mLinearLayoutRoot} if the view is not null.
*/
private void hideCurrentCard() {
if (mLastViewAdded != null) {
// The other button has been pressed so the current view needs to be removed.
mLinearLayoutRoot.removeView(mLastViewAdded);
mLastViewAdded = null;
}
}
/**
* Animates a container to allow a user to import or export their data.
*
* @param cardView id of the card view to show.
*/
private void showCardView(byte cardView) {
if (cardView != mCurrentCardView)
hideCurrentCard();
else
return;
mCurrentCardView = cardView;
if (mCurrentCardView == VIEW_IMPORT) {
mLastViewAdded = (CardView) LayoutInflater.from(getContext())
.inflate(R.layout.cardview_transfer_import, mLinearLayoutRoot, false);
setupImportInteractions(mLastViewAdded);
} else if (mCurrentCardView == VIEW_EXPORT) {
mLastViewAdded = (CardView) LayoutInflater.from(getContext())
.inflate(R.layout.cardview_transfer_export, mLinearLayoutRoot, false);
setupExportInteractions(mLastViewAdded);
} else {
mLastViewAdded = (CardView) LayoutInflater.from(getContext())
.inflate(R.layout.cardview_transfer_restore_delete, mLinearLayoutRoot, false);
setupRestoreDeleteInteractions(mLastViewAdded);
}
LinearLayout.LayoutParams params = new LinearLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT,
ViewGroup.LayoutParams.WRAP_CONTENT);
mLinearLayoutRoot.addView(mLastViewAdded, params);
}
/**
* Sets listeners and colors for views in the export CardView.
*
* @param rootView root CardView.
*/
private void setupExportInteractions(final View rootView) {
Button cancelButton = (Button) rootView.findViewById(R.id.btn_cancel);
cancelButton.getBackground()
.setColorFilter(DisplayUtils.getColorResource(getResources(), R.color.theme_red_tertiary),
PorterDuff.Mode.MULTIPLY);
cancelButton.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
if (mCurrentTransferTask != null) {
// Display text that task is being cancelled
if (mLastViewAdded != null) {
TextView textView = (TextView) mLastViewAdded.findViewById(R.id.tv_export_progress);
if (textView != null)
textView.setText(R.string.text_cancelling);
}
mCurrentTransferTask.cancel(true);
}
}
});
Button exportButton = (Button) rootView.findViewById(R.id.btn_begin_export);
exportButton.getBackground().setColorFilter(Theme.getPrimaryThemeColor(), PorterDuff.Mode.MULTIPLY);
exportButton.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
if (mLastKeyReceived == null) {
if (mCurrentTransferTask == null) {
mCurrentTransferTask = new DataExportTask(TransferFragment.this);
mCurrentTransferTask.execute(mExportFailures > 0);
}
} else {
TextView textView = (TextView) rootView.findViewById(R.id.tv_transfer_export_result);
textView.setText(String.format(Locale.CANADA,
getResources().getString(R.string.text_transfer_exported_already),
mLastKeyReceived));
textView.setTextColor(DisplayUtils.getColorResource(getResources(), R.color.transfer_error));
textView.setVisibility(View.VISIBLE);
}
}
});
((ProgressBar) rootView.findViewById(R.id.pb_export)).getProgressDrawable()
.setColorFilter(Theme.getPrimaryThemeColor(), PorterDuff.Mode.SRC_IN);
}
/**
* Sets listeners and colors for views in the import CardView.
*
* @param rootView root CardView.
*/
private void setupImportInteractions(final View rootView) {
Button cancelButton = (Button) rootView.findViewById(R.id.btn_cancel);
cancelButton.getBackground()
.setColorFilter(DisplayUtils.getColorResource(getResources(), R.color.theme_red_tertiary),
PorterDuff.Mode.MULTIPLY);
cancelButton.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
if (mCurrentTransferTask != null) {
// Display text that task is being cancelled
if (mLastViewAdded != null) {
TextView textView = (TextView) mLastViewAdded.findViewById(R.id.tv_import_progress);
if (textView != null)
textView.setText(R.string.text_cancelling);
}
mCurrentTransferTask.cancel(true);
}
}
});
final Button importButton = (Button) rootView.findViewById(R.id.btn_begin_import);
importButton.setEnabled(false);
importButton.getBackground().setColorFilter(Theme.getPrimaryThemeColor(), PorterDuff.Mode.MULTIPLY);
importButton.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
if (mCurrentTransferTask == null) {
String transferKey = ((EditText) rootView.findViewById(R.id.et_transfer_code)).getText().toString();
if (!TextUtils.isEmpty(transferKey) && transferKey.length() == TransferUtils.TRANSFER_KEY_LENGTH) {
mCurrentTransferTask = new DataImportTask(TransferFragment.this, transferKey);
mCurrentTransferTask.execute(mImportFailures > 0);
} else {
TextView textView = (TextView) rootView.findViewById(R.id.tv_transfer_import_result);
textView.setText(String.format(Locale.CANADA,
getResources().getString(R.string.text_transfer_invalid_key),
TransferUtils.TRANSFER_KEY_LENGTH));
textView.setTextColor(DisplayUtils.getColorResource(getResources(), R.color.transfer_error));
textView.setVisibility(View.VISIBLE);
}
}
}
});
((EditText) rootView.findViewById(R.id.et_transfer_code)).addTextChangedListener(new TextWatcher() {
@Override
public void beforeTextChanged(CharSequence s, int start, int count, int after) {
// Unused
}
@Override
public void onTextChanged(CharSequence s, int start, int before, int count) {
// Unused
}
@Override
public void afterTextChanged(Editable s) {
if (s.length() == TransferUtils.TRANSFER_KEY_LENGTH) {
importButton.setEnabled(true);
} else if (importButton.isEnabled()) {
importButton.setEnabled(false);
}
}
});
((ProgressBar) rootView.findViewById(R.id.pb_import)).getProgressDrawable()
.setColorFilter(Theme.getPrimaryThemeColor(), PorterDuff.Mode.SRC_IN);
}
/**
* Sets listeners and colors for views in the restore/delete CardView.
*
* @param rootView root CardView.
*/
private void setupRestoreDeleteInteractions(final View rootView) {
rootView.findViewById(R.id.btn_restore).setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
new AlertDialog.Builder(getContext())
.setMessage(R.string.text_restore_backup_prompt)
.setPositiveButton(R.string.text_restore, new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
new ReplaceBowlerDataTask(TransferFragment.this).execute(false);
}
})
.setNegativeButton(R.string.text_cancel, null)
.create()
.show();
}
});
rootView.findViewById(R.id.btn_delete).setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
new AlertDialog.Builder(getContext())
.setMessage(R.string.text_delete_backup_prompt)
.setPositiveButton(R.string.dialog_delete, new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
new DeleteBowlerDataTask(TransferFragment.this).execute(false);
}
})
.setNegativeButton(R.string.text_cancel, null)
.create()
.show();
}
});
}
/**
* Displays a prompt asking the user if they want to override their current data. If they answer yes, the process of
* replacing the current data with either the backup data or downloaded data begins.
*
* @param replaceWithDownloaded {@code true} to replace the default data with newly downloaded data, or {@code
* false} to replace downloaded data with the original data
* @param deleteDownloaded {@code true} to delete the downloaded data when the overwrite is finished
*/
private void promptUserToOverride(final boolean replaceWithDownloaded, final boolean deleteDownloaded) {
DialogInterface.OnClickListener onClickListener = new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
if (which == DialogInterface.BUTTON_POSITIVE) {
new ReplaceBowlerDataTask(TransferFragment.this).execute(replaceWithDownloaded, deleteDownloaded);
} else if (deleteDownloaded && replaceWithDownloaded) {
new DeleteBowlerDataTask(TransferFragment.this).execute(true);
}
dialog.dismiss();
}
};
new AlertDialog.Builder(getContext())
.setTitle(R.string.text_overwrite_data_title)
.setMessage(R.string.text_overwrite_data_message)
.setPositiveButton(R.string.text_overwrite, onClickListener)
.setNegativeButton(R.string.text_cancel, onClickListener)
.create()
.show();
}
/**
* Creates a new instance of this fragment.
*
* @return the new instance
*/
public static TransferFragment newInstance() {
return new TransferFragment();
}
/**
* Deletes either the user's backup or downloaded bowler data.
*/
private static final class DeleteBowlerDataTask
extends AsyncTask<Boolean, Void, Void> {
/** Weak reference to the parent fragment. */
private final WeakReference<TransferFragment> mFragment;
/**
* Assigns a weak reference to the parent fragment.
*
* @param fragment parent fragment
*/
private DeleteBowlerDataTask(TransferFragment fragment) {
mFragment = new WeakReference<>(fragment);
}
@SuppressWarnings("ResultOfMethodCallIgnored")
@Override
protected Void doInBackground(Boolean... deleteDownloaded) {
TransferFragment fragment = mFragment.get();
if (fragment == null || fragment.getContext() == null)
return null;
String modifier = (deleteDownloaded[0])
? TransferUtils.DATA_DOWNLOADED
: TransferUtils.DATA_BACKUP;
File originalDatabase = fragment.getContext().getDatabasePath(DatabaseHelper.DATABASE_NAME);
File databaseToDelete = new File(originalDatabase.getAbsolutePath() + modifier);
databaseToDelete.delete();
return null;
}
}
/**
* Replaces the user's current data with either downloaded data or a backup of old data.
*/
private static final class ReplaceBowlerDataTask
extends AsyncTask<Boolean, Void, Integer> {
/** Weak reference to the parent fragment. */
private final WeakReference<TransferFragment> mFragment;
/**
* Assigns a weak reference to the parent fragment.
*
* @param fragment parent fragment
*/
private ReplaceBowlerDataTask(TransferFragment fragment) {
mFragment = new WeakReference<>(fragment);
}
@Override
protected void onPreExecute() {
TransferFragment fragment = mFragment.get();
if (fragment == null)
return;
NavigationController navigationController = (NavigationController) fragment.getActivity();
if (navigationController == null)
return;
// Disable database and app navigation
navigationController.setNavigationEnabled(false);
DatabaseHelper.closeInstance();
}
@Override
protected Integer doInBackground(Boolean... options) {
final TransferFragment fragment = mFragment.get();
if (fragment == null || fragment.getContext() == null)
return 0;
boolean replaceWithDownloaded = options[0];
boolean deleteDownloaded = false;
if (options.length >= 2)
deleteDownloaded = options[1];
File originalDatabase = fragment.getContext().getDatabasePath(DatabaseHelper.DATABASE_NAME);
File replacementDatabase;
if (replaceWithDownloaded) {
replacementDatabase = new File(originalDatabase.getAbsolutePath() + TransferUtils.DATA_DOWNLOADED);
if (!FileUtils.copyFile(originalDatabase,
originalDatabase.getAbsolutePath() + TransferUtils.DATA_BACKUP)) {
return R.string.text_backup_failure;
}
} else {
replacementDatabase = new File(originalDatabase.getAbsolutePath() + TransferUtils.DATA_BACKUP);
}
if (!replacementDatabase.exists()) {
return R.string.text_transfer_nonexist;
}
if (!FileUtils.copyFile(replacementDatabase, originalDatabase.getAbsolutePath())) {
return R.string.text_transfer_overwrite_failed;
}
if (replaceWithDownloaded && deleteDownloaded) {
// Delete the downloaded data if it was successfully copied over
fragment.getActivity().runOnUiThread(new Runnable() {
@Override
public void run() {
new DeleteBowlerDataTask(fragment).execute(true);
}
});
}
return R.string.text_transfer_successful;
}
@Override
protected void onPostExecute(Integer result) {
TransferFragment fragment = mFragment.get();
if (fragment == null)
return;
NavigationController navigationController = (NavigationController) fragment.getActivity();
if (navigationController == null)
return;
navigationController.setNavigationEnabled(true);
new AlertDialog.Builder(fragment.getContext())
.setMessage(result)
.setPositiveButton(R.string.dialog_okay, null)
.create()
.show();
}
}
/**
* Imports the user's data from a remote server.
*/
private static final class DataImportTask
extends AsyncTask<Boolean, Integer, String> {
/** Weak reference to the parent fragment. */
private final WeakReference<TransferFragment> mFragment;
/** Weak reference to the progress bar for the task. */
private final WeakReference<ProgressBar> mProgressBar;
/** Weak reference to the text view to display results of the text. */
private final WeakReference<TextView> mTextViewProgress;
/** Key which represents data on the server. */
private final String mTransferKey;
/**
* Assigns a weak reference to the parent fragment.
*
* @param fragment parent fragment
* @param key unique key for requesting data from server
*/
private DataImportTask(TransferFragment fragment, String key) {
mFragment = new WeakReference<>(fragment);
mProgressBar = new WeakReference<>((ProgressBar) fragment.mLastViewAdded.findViewById(R.id.pb_import));
mTextViewProgress = new WeakReference<>(
(TextView) fragment.mLastViewAdded.findViewById(R.id.tv_import_progress));
mTransferKey = key;
}
@Override
protected void onPreExecute() {
TransferFragment fragment = mFragment.get();
if (fragment == null || fragment.getContext() == null)
return;
// Setting up cancel button
View view = fragment.mLastViewAdded.findViewById(R.id.btn_cancel);
view.setEnabled(true);
view.setVisibility(View.VISIBLE);
// Displaying progress bar
ProgressBar progressBar = mProgressBar.get();
if (progressBar != null)
progressBar.setProgress(-1);
// Displaying progress text
TextView textView = mTextViewProgress.get();
if (textView != null)
textView.setText(R.string.text_contacting_server);
// Display appropriate views
fragment.mLastViewAdded.findViewById(R.id.et_transfer_code).setVisibility(View.GONE);
fragment.mLastViewAdded.findViewById(R.id.tv_transfer_import_result).setVisibility(View.GONE);
fragment.mLastViewAdded.findViewById(R.id.ll_transfer_progress).setVisibility(View.VISIBLE);
}
@SuppressWarnings("CheckStyle") // Ignore length of method
@Override
protected String doInBackground(Boolean... retry) {
TransferFragment fragment = mFragment.get();
if (fragment == null || fragment.getContext() == null)
return TransferUtils.ERROR_EXCEPTION;
if (!TransferUtils.isConnectionAvailable(fragment.getContext())) {
return TransferUtils.ERROR_NO_INTERNET;
} else if (!TransferUtils.getServerStatus()) {
return TransferUtils.ERROR_UNAVAILABLE;
} else if (!TransferUtils.isKeyValid(mTransferKey)) {
return TransferUtils.ERROR_INVALID_KEY;
}
File dbFile = fragment.getContext().getDatabasePath(DatabaseHelper.DATABASE_NAME);
String dbFilePath = dbFile.getAbsolutePath() + TransferUtils.DATA_DOWNLOADED;
dbFile = new File(dbFilePath);
final int timeout = (retry[0])
? TransferUtils.CONNECTION_TIMEOUT
: TransferUtils.CONNECTION_EXTENDED_TIMEOUT;
HttpURLConnection connection;
try {
URL url = new URL(TransferUtils.getDownloadEndpoint(mTransferKey));
// Preparing connection for upload
connection = (HttpURLConnection) url.openConnection();
connection.setReadTimeout(timeout);
connection.setConnectTimeout(timeout);
// Get the expected size of the file
int contentLength = connection.getContentLength();
InputStream inputStream = url.openStream();
FileOutputStream outputStream = new FileOutputStream(dbFile);
byte[] data = new byte[TransferUtils.MAX_BUFFER_SIZE];
int totalDataRead = 0;
int lastProgressPercentage = 0;
try {
int dataRead = inputStream.read(data);
while (dataRead != -1 && !isCancelled()) {
totalDataRead += dataRead;
// Update the progress bar
int currentProgressPercentage = (int) ((totalDataRead) / (float) contentLength
* TransferUtils.TARGET_PERCENTAGE);
if (currentProgressPercentage > lastProgressPercentage) {
lastProgressPercentage = currentProgressPercentage;
publishProgress(currentProgressPercentage);
}
outputStream.write(data, 0, dataRead);
dataRead = inputStream.read(data);
}
} catch (Exception ex) {
Log.e(TAG, "Error receiving file.", ex);
return TransferUtils.ERROR_EXCEPTION;
} finally {
try {
outputStream.close();
if (inputStream != null) {
inputStream.close();
}
} catch (IOException ex) {
Log.e(TAG, "Error closing streams.", ex);
}
}
// Update progress bar
publishProgress(TransferUtils.TARGET_PERCENTAGE);
} catch (MalformedURLException ex) {
Log.e(TAG, "Malformed url. I have no idea how this happened.", ex);
return TransferUtils.ERROR_MALFORMED_URL;
} catch (SocketTimeoutException ex) {
Log.e(TAG, "Timed out reading response.", ex);
return TransferUtils.ERROR_TIMEOUT;
} catch (IOException ex) {
Log.e(TAG, "Couldn't open or maintain connection.", ex);
return TransferUtils.ERROR_IO_EXCEPTION;
} catch (Exception ex) {
Log.e(TAG, "Unknown exception.", ex);
return TransferUtils.ERROR_EXCEPTION;
}
return TransferUtils.SUCCESSFUL_IMPORT;
}
@Override
public void onProgressUpdate(Integer... progress) {
if (progress[0] < 0) {
TextView textView = mTextViewProgress.get();
if (textView != null)
textView.setText(R.string.text_downloading);
ProgressBar progressBar = mProgressBar.get();
if (progressBar != null)
progressBar.setProgress(0);
} else if (progress[0] == 0) {
TextView textView = mTextViewProgress.get();
if (textView != null)
textView.setText(R.string.text_cancelling);
} else if (progress[0] >= TransferUtils.TARGET_PERCENTAGE) {
ProgressBar progressBar = mProgressBar.get();
if (progressBar != null)
progressBar.setProgress(TransferUtils.TARGET_PERCENTAGE);
TextView textView = mTextViewProgress.get();
if (textView != null)
textView.setText(R.string.text_processing);
} else {
// Updating progress bar
ProgressBar progressBar = mProgressBar.get();
if (progressBar != null)
progressBar.setProgress(progress[0]);
}
}
@Override
protected void onCancelled() {
mProgressBar.clear();
TransferFragment fragment = mFragment.get();
if (fragment == null || fragment.getContext() == null)
return;
cleanupTask(fragment);
displayResult(fragment, TransferUtils.ERROR_CANCELLED);
fragment.mImportFailures++;
}
@Override
protected void onPostExecute(String result) {
mProgressBar.clear();
TransferFragment fragment = mFragment.get();
if (fragment == null || fragment.getContext() == null)
return;
cleanupTask(fragment);
displayResult(fragment, result);
if (result.equals(TransferUtils.SUCCESSFUL_IMPORT)) {
fragment.mImportFailures = 0;
fragment.promptUserToOverride(true, true);
} else {
fragment.mImportFailures++;
}
}
/**
* Returns the fragment to the state it was in before the task was started.
*
* @param fragment fragment to clean up.
*/
private void cleanupTask(TransferFragment fragment) {
// Disabling cancel button
View view = fragment.mLastViewAdded.findViewById(R.id.btn_cancel);
view.setEnabled(false);
view.setVisibility(View.GONE);
// Hiding progress bar
view = fragment.mLastViewAdded.findViewById(R.id.ll_transfer_progress);
view.setVisibility(View.GONE);
fragment.mLastViewAdded.findViewById(R.id.et_transfer_code).setVisibility(View.VISIBLE);
fragment.mCurrentTransferTask = null;
}
/**
* Shows a {@code TextView} with the result of the upload.
*
* @param fragment fragment with text view to display result
* @param result string indicating the result of the upload
*/
private void displayResult(TransferFragment fragment, String result) {
TextView textViewResult = (TextView) fragment.mLastViewAdded.findViewById(R.id.tv_transfer_import_result);
int textColor = DisplayUtils.getColorResource(fragment.getResources(), R.color.transfer_error);
boolean showTextView = true;
switch (result) {
case TransferUtils.ERROR_CANCELLED:
textViewResult.setText(R.string.text_transfer_cancelled);
break;
case TransferUtils.ERROR_IO_EXCEPTION:
case TransferUtils.ERROR_EXCEPTION:
case TransferUtils.ERROR_MALFORMED_URL:
textViewResult.setText(R.string.text_transfer_unknown_error);
break;
case TransferUtils.ERROR_FILE_NOT_FOUND:
textViewResult.setText(R.string.text_transfer_file_not_found);
break;
case TransferUtils.ERROR_OUT_OF_MEMORY:
textViewResult.setText(R.string.text_transfer_oom);
break;
case TransferUtils.ERROR_TIMEOUT:
textViewResult.setText(R.string.text_transfer_try_again_later);
break;
case TransferUtils.ERROR_UNAVAILABLE:
textViewResult.setText(R.string.text_transfer_unavailable);
break;
case TransferUtils.ERROR_INVALID_KEY:
textViewResult.setText(String.format(Locale.CANADA, fragment.getResources()
.getString(R.string.text_transfer_invalid_key), TransferUtils.TRANSFER_KEY_LENGTH));
break;
case TransferUtils.ERROR_NO_INTERNET:
textViewResult.setText(R.string.text_transfer_no_internet);
break;
default:
showTextView = false;
break;
}
if (showTextView) {
textViewResult.setTextColor(textColor);
textViewResult.setVisibility(View.VISIBLE);
}
}
}
/**
* Exports the user's data to a remote server.
*/
private static final class DataExportTask
extends AsyncTask<Boolean, Integer, String> {
/** Weak reference to the parent fragment. */
private final WeakReference<TransferFragment> mFragment;
/** Weak reference to the progress bar for the task. */
private final WeakReference<ProgressBar> mProgressBar;
/** Weak reference to the text view to display results of the text. */
private final WeakReference<TextView> mTextViewProgress;
/**
* Assigns a weak reference to the parent fragment.
*
* @param fragment parent fragment
*/
private DataExportTask(TransferFragment fragment) {
mFragment = new WeakReference<>(fragment);
mProgressBar = new WeakReference<>((ProgressBar) fragment.mLastViewAdded.findViewById(R.id.pb_export));
mTextViewProgress = new WeakReference<>(
(TextView) fragment.mLastViewAdded.findViewById(R.id.tv_export_progress));
}
@Override
protected void onPreExecute() {
TransferFragment fragment = mFragment.get();
if (fragment == null || fragment.getContext() == null)
return;
// Setting up cancel button
View view = fragment.mLastViewAdded.findViewById(R.id.btn_cancel);
view.setEnabled(true);
view.setVisibility(View.VISIBLE);
// Displaying progress bar
ProgressBar progressBar = mProgressBar.get();
if (progressBar != null)
progressBar.setProgress(-1);
// Displaying progress text
TextView textView = mTextViewProgress.get();
if (textView != null)
textView.setText(R.string.text_contacting_server);
// Display appropriate views
fragment.mLastViewAdded.findViewById(R.id.tv_transfer_export_result).setVisibility(View.GONE);
fragment.mLastViewAdded.findViewById(R.id.ll_transfer_progress).setVisibility(View.VISIBLE);
}
@SuppressWarnings("CheckStyle") // Ignore length of method
@Override
protected String doInBackground(Boolean... retry) {
TransferFragment fragment = mFragment.get();
if (fragment == null || fragment.getContext() == null)
return TransferUtils.ERROR_EXCEPTION;
if (!TransferUtils.isConnectionAvailable(fragment.getContext())) {
return TransferUtils.ERROR_NO_INTERNET;
} else if (!TransferUtils.getServerStatus()) {
return TransferUtils.ERROR_UNAVAILABLE;
}
// Most of this method retrieved from this StackOverflow question.
// http://stackoverflow.com/a/7645328/4896787
// Preparing the database
File dbFile = fragment.getContext().getDatabasePath(DatabaseHelper.DATABASE_NAME);
String dbFilePath = dbFile.getAbsolutePath();
String dbFileName = dbFile.getName();
HttpURLConnection connection;
FileInputStream fileInputStream = null;
DataOutputStream outputStream = null;
BufferedReader reader = null;
final int timeout = (retry[0])
? TransferUtils.CONNECTION_TIMEOUT
: TransferUtils.CONNECTION_EXTENDED_TIMEOUT;
final String lineEnd = "\r\n";
final String twoHyphens = "--";
final String boundary = "*****";
final String transferApiKey = fragment.getResources().getString(R.string.transfer_api_key);
int bytesRead;
int bytesAvailable;
int bufferSize;
byte[] buffer;
try {
fileInputStream = new FileInputStream(dbFile);
URL url = new URL(TransferUtils.getUploadEndpoint());
// Preparing connection for upload
connection = (HttpURLConnection) url.openConnection();
connection.setDoInput(true);
connection.setDoOutput(true);
connection.setUseCaches(false);
connection.setConnectTimeout(timeout);
connection.setReadTimeout(timeout);
connection.setRequestMethod("POST");
connection.setRequestProperty("Connection", "Keep-Alive");
connection.setRequestProperty("Content-Type", "multipart/form-data;boundary=" + boundary);
connection.setRequestProperty("Authorization", transferApiKey);
outputStream = new DataOutputStream(connection.getOutputStream());
outputStream.writeBytes(twoHyphens + boundary + lineEnd);
outputStream.writeBytes("Content-Disposition: form-data; name=\"uploadedfile\";filename=\""
+ dbFileName + "\"" + lineEnd);
outputStream.writeBytes(lineEnd);
final int totalBytes = fileInputStream.available();
int lastProgressPercentage = 0;
bytesAvailable = totalBytes;
bufferSize = Math.min(bytesAvailable, TransferUtils.MAX_BUFFER_SIZE);
buffer = new byte[bufferSize];
bytesRead = fileInputStream.read(buffer, 0, bufferSize);
try {
while (bytesRead > 0 && !isCancelled()) {
try {
outputStream.write(buffer, 0, bufferSize);
} catch (OutOfMemoryError ex) {
Log.e(TAG, "Out of memory sending file.", ex);
return TransferUtils.ERROR_OUT_OF_MEMORY;
}
// Update the progress bar
int currentProgressPercentage = (int) ((-bytesAvailable + totalBytes) / (float) totalBytes
* TransferUtils.TARGET_PERCENTAGE);
if (currentProgressPercentage > lastProgressPercentage) {
lastProgressPercentage = currentProgressPercentage;
publishProgress(currentProgressPercentage);
}
bytesAvailable = fileInputStream.available();
bufferSize = Math.min(bytesAvailable, TransferUtils.MAX_BUFFER_SIZE);
bytesRead = fileInputStream.read(buffer, 0, bufferSize);
}
} catch (Exception ex) {
Log.e(TAG, "Error sending file.", ex);
return TransferUtils.ERROR_EXCEPTION;
}
if (isCancelled()) {
publishProgress(0);
return TransferUtils.ERROR_CANCELLED;
}
outputStream.writeBytes(lineEnd);
outputStream.writeBytes(twoHyphens + boundary + twoHyphens + lineEnd);
publishProgress(TransferUtils.TARGET_PERCENTAGE);
// Get info about the status of the server
int serverResponseCode = connection.getResponseCode();
StringBuilder responseBuilder = new StringBuilder();
if (serverResponseCode == HttpURLConnection.HTTP_OK) {
try {
reader = new BufferedReader(
new InputStreamReader(new DataInputStream(connection.getInputStream())));
String line = reader.readLine();
while (line != null && !isCancelled()) {
responseBuilder.append(line);
line = reader.readLine();
}
} catch (IOException ex) {
Log.e(TAG, "Error reading server response.", ex);
return TransferUtils.ERROR_IO_EXCEPTION;
} finally {
try {
if (reader != null)
reader.close();
} catch (IOException ex) {
Log.e(TAG, "Error closing stream.", ex);
}
}
}
if (isCancelled())
return TransferUtils.ERROR_CANCELLED;
else
return responseBuilder.toString();
} catch (FileNotFoundException ex) {
Log.e(TAG, "Unable to find database file.", ex);
return TransferUtils.ERROR_FILE_NOT_FOUND;
} catch (MalformedURLException ex) {
Log.e(TAG, "Malformed url. I have no idea how this happened.", ex);
return TransferUtils.ERROR_MALFORMED_URL;
} catch (SocketTimeoutException ex) {
Log.e(TAG, "Timed out reading response.", ex);
return TransferUtils.ERROR_TIMEOUT;
} catch (IOException ex) {
Log.e(TAG, "Couldn't open or maintain connection.", ex);
return TransferUtils.ERROR_IO_EXCEPTION;
} catch (Exception ex) {
Log.e(TAG, "Unknown exception.", ex);
return TransferUtils.ERROR_EXCEPTION;
} finally {
try {
if (fileInputStream != null)
fileInputStream.close();
if (outputStream != null) {
outputStream.flush();
outputStream.close();
}
} catch (IOException ex) {
Log.e(TAG, "Error closing streams.", ex);
}
}
}
@Override
protected void onProgressUpdate(Integer... progress) {
if (progress[0] < 0) {
TextView textView = mTextViewProgress.get();
if (textView != null)
textView.setText(R.string.text_uploading);
ProgressBar progressBar = mProgressBar.get();
if (progressBar != null)
progressBar.setProgress(0);
} else if (progress[0] == 0) {
TextView textView = mTextViewProgress.get();
if (textView != null)
textView.setText(R.string.text_cancelling);
} else if (progress[0] >= TransferUtils.TARGET_PERCENTAGE) {
ProgressBar progressBar = mProgressBar.get();
if (progressBar != null)
progressBar.setProgress(TransferUtils.TARGET_PERCENTAGE);
TextView textView = mTextViewProgress.get();
if (textView != null)
textView.setText(R.string.text_processing);
} else {
// Updating progress bar
ProgressBar progressBar = mProgressBar.get();
if (progressBar != null)
progressBar.setProgress(progress[0]);
}
}
@Override
protected void onCancelled() {
mProgressBar.clear();
TransferFragment fragment = mFragment.get();
if (fragment == null || fragment.getContext() == null)
return;
cleanupTask(fragment);
displayResult(fragment, TransferUtils.ERROR_CANCELLED);
fragment.mExportFailures++;
}
@Override
protected void onPostExecute(String result) {
mProgressBar.clear();
TransferFragment fragment = mFragment.get();
if (fragment == null || fragment.getContext() == null)
return;
cleanupTask(fragment);
displayResult(fragment, result);
if (result.contains("requestId")) {
fragment.mExportFailures = 0;
} else {
fragment.mExportFailures++;
}
}
/**
* Returns the fragment to the state it was in before the task was started.
*
* @param fragment fragment to clean up.
*/
private void cleanupTask(TransferFragment fragment) {
// Disabling cancel button
View view = fragment.mLastViewAdded.findViewById(R.id.btn_cancel);
view.setEnabled(false);
view.setVisibility(View.GONE);
// Hiding progress bar
view = fragment.mLastViewAdded.findViewById(R.id.ll_transfer_progress);
view.setVisibility(View.GONE);
fragment.mCurrentTransferTask = null;
}
/**
* Shows a {@code TextView} with the result of the upload.
*
* @param fragment fragment with text view to display result
* @param result string indicating the result of the upload
*/
private void displayResult(TransferFragment fragment, String result) {
TextView textViewResult = (TextView) fragment.mLastViewAdded.findViewById(R.id.tv_transfer_export_result);
int textColor = DisplayUtils.getColorResource(fragment.getResources(), R.color.transfer_error);
switch (result) {
case TransferUtils.ERROR_CANCELLED:
textViewResult.setText(R.string.text_transfer_cancelled);
break;
case TransferUtils.ERROR_IO_EXCEPTION:
case TransferUtils.ERROR_EXCEPTION:
case TransferUtils.ERROR_MALFORMED_URL:
textViewResult.setText(R.string.text_transfer_unknown_error);
break;
case TransferUtils.ERROR_FILE_NOT_FOUND:
textViewResult.setText(R.string.text_transfer_file_not_found);
break;
case TransferUtils.ERROR_OUT_OF_MEMORY:
textViewResult.setText(R.string.text_transfer_oom);
break;
case TransferUtils.ERROR_TIMEOUT:
textViewResult.setText(R.string.text_transfer_try_again_later);
break;
case TransferUtils.ERROR_UNAVAILABLE:
textViewResult.setText(R.string.text_transfer_unavailable);
break;
case TransferUtils.ERROR_NO_INTERNET:
textViewResult.setText(R.string.text_transfer_no_internet);
break;
default:
int requestIdIndex = result.indexOf("requestId");
if (requestIdIndex >= 0) {
int start = requestIdIndex + TransferUtils.TRANSFER_KEY_START;
String key = result.substring(start, start + TransferUtils.TRANSFER_KEY_LENGTH);
fragment.mLastKeyReceived = key;
textColor = DisplayUtils.getColorResource(fragment.getResources(), android.R.color.black);
textViewResult.setText(String.format(Locale.CANADA, fragment.getResources()
.getString(R.string.text_transfer_upload_complete), key));
} else {
textViewResult.setText(R.string.text_transfer_unknown_error);
}
break;
}
textViewResult.setTextColor(textColor);
textViewResult.setVisibility(View.VISIBLE);
}
}
}