/**
Copyright 2015 Tim Engler, Rareventure LLC
This file is part of Tiny Travel Tracker.
Tiny Travel Tracker is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
Tiny Travel Tracker is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with Tiny Travel Tracker. If not, see <http://www.gnu.org/licenses/>.
*/
package com.rareventure.gps2.gpx;
import java.io.File;
import java.io.IOException;
import java.io.OutputStream;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.Iterator;
import android.app.AlertDialog;
import android.content.DialogInterface;
import android.content.DialogInterface.OnClickListener;
import android.content.pm.PackageManager.NameNotFoundException;
import android.database.Cursor;
import android.os.Bundle;
import android.os.Environment;
import android.util.Log;
import android.view.View;
import android.widget.CheckBox;
import android.widget.EditText;
import com.rareventure.android.DbUtil;
import com.rareventure.android.ProgressDialogActivity;
import com.rareventure.android.Util;
import com.rareventure.gps2.GTG;
import com.rareventure.gps2.GpsTrailerCrypt;
import com.rareventure.gps2.R;
import com.rareventure.gps2.database.GpsLocationRow;
import com.rareventure.gps2.database.TimeZoneTimeRow;
import com.rareventure.gpx.GpxWriter;
import com.rareventure.util.OutputStreamToInputStreamPipe;
import de.idyl.winzipaes.AesZipFileEncrypter;
import de.idyl.winzipaes.impl.AESEncrypterBC;
public class CreateGpxBackup extends ProgressDialogActivity {
private EditText enterPasswordView;
private CheckBox passwordCheckBox;
private EditText reenterPasswordView;
private EditText filenameView;
private boolean isShowBackupFinishedOnResume;
private boolean isPause = true;
private String filePath;;
@Override
public void doOnCreate(Bundle b) {
super.doOnCreate(b);
setContentView(R.layout.create_gpx_backup);
enterPasswordView = (EditText)findViewById(R.id.enter_new_password);
reenterPasswordView = (EditText)findViewById(R.id.reenter_new_password);
passwordCheckBox = (CheckBox)findViewById(R.id.passwordCheckBox);
filenameView = (EditText)findViewById(R.id.filename);
}
private static final SimpleDateFormat DATE_FORMAT = new SimpleDateFormat("yyyy-MM-dd");
private Task backupTask;
protected static final int MAX_BUFFER_SIZE = 65536;
/**
* Number of processed rows before we update the progress bar
*/
protected static final int UPDATE_INCREMENT = 250;
@Override
public void doOnResume() {
super.doOnResume();
isPause = false;
showCreateBackupFinished();
updatePasswordViews();
filenameView.setText("TTT-"+DATE_FORMAT.format(new Date())+".gpx.zip");
}
@Override
public void doOnPause(boolean doOnResumeCalled)
{
super.doOnPause(doOnResumeCalled);
isPause=true;
}
@Override
public void finish()
{
//if they try and enter a password and hit cancel, GTGActivityHelper has no choice but to back up through all the activities on the stack
//so we have to cancel the restore, because our activity is about to finished.
//Note that using singleTask doesn't help here because we are finishing the task regardless.
if(backupTask != null)
backupTask.cancel();
backupTask = null;
super.finish();
}
public void onCreateButton(final View v)
{
final boolean passwordProtected = passwordCheckBox.isChecked();
final String password = enterPasswordView.getText().toString();
if(passwordProtected)
{
if(!password.equals(reenterPasswordView.getText().toString()))
{
new AlertDialog.Builder(this)
.setCancelable(false)
.setTitle(R.string.error)
.setMessage(R.string.passwords_dont_match)
.setNeutralButton(R.string.details_ok, null)
.show();
return;
}
}
String localFilename = filenameView.getText().toString();
if(!localFilename.toLowerCase().matches(".*\\.zip"))
{
localFilename += ".zip";
}
final String filename = localFilename;
filePath = Environment.getExternalStorageDirectory()+"/"+filename;
final File filePathFile = new File(filePath);
//if need to overwrite
if(new File(filePath).exists())
{
DialogInterface.OnClickListener dialogClickListener = new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
switch (which){
case DialogInterface.BUTTON_POSITIVE:
//delete the file and try again
filePathFile.delete();
onCreateButton(v);
break;
case DialogInterface.BUTTON_NEGATIVE:
break;
}
}
};
AlertDialog.Builder builder = new AlertDialog.Builder(this);
builder.setMessage(R.string.backup_file_already_exists_overwrite).setPositiveButton("Yes", dialogClickListener)
.setNegativeButton("No", dialogClickListener).show();
return;
}
runLongTask(backupTask = new Task() {
private IOException failedException;
@Override
public void doIt() {
final OutputStreamToInputStreamPipe bis = new OutputStreamToInputStreamPipe(1024, 65536);
final GpxWriter gis = new GpxWriter(bis);
if(!passwordProtected)
{
if(!createRegularZipOutputThread(filePath, filename.replaceFirst("\\.zip$", ""), bis))
return;
}
else
{
if(!createEncryptedZipOutputThread(filePath, filename.replaceFirst("\\.zip$", ""), bis, password))
return;
}
int total = GTG.gpsLocCache.getNextRowId();
super.setMaxProgress(total);
//read the data
Cursor c = null;
Iterator<TimeZoneTimeRow> tztri = GTG.tztSet.getIterator();
// this try is to close the transaction with a finally
//and for removing the rwtm locks
GTG.cacheCreatorLock.registerReadingThread();
try {
/* ttt_installer:remove_line */Log.d(GTG.TAG,"Started gpx writing...");
c = GTG.gpsLocDbAccessor.query( null, "_id", (String [])null);
GpsLocationRow currGpsLocRow = GpsTrailerCrypt.allocateGpsLocationRow();
TimeZoneTimeRow tztRow, lastTztRow = null;
if(tztri.hasNext())
{
tztRow = tztri.next();
}
else tztRow = null;
try {
gis.startDoc(GTG.APP_NAME,
getPackageManager().getPackageInfo(getPackageName(), 0).versionName);
} catch (NameNotFoundException e1) {
throw new IllegalStateException(e1);
}
gis.startTrack(filenameView.getText().toString());
gis.startSegment();
// while processing this query
while (c.moveToNext() && !isCanceled) {
if(failedException != null)
{
onWriteFail();
return;
}
int curId = c.getInt(0);
try {
GTG.gpsLocDbAccessor.readRow(currGpsLocRow, c);
} catch (Exception e) {
// sometimes the encryption fails to work (not often)
// TODO 3: figure out how to fail gracefully in these
// situations
Log.e("GTG", "Error reading row " + c.getInt(0)
+ ", skipping", e);
continue;
}
//add timezone info if this the first gps row that has
//a new timezone
TimeZoneTimeRow tzToAdd;
//if we are now affected by the next timezone row
if(tztRow != null && currGpsLocRow.getTime() >= tztRow.getTime())
{
if(tztRow != null &&
(lastTztRow == null || lastTztRow.getTimeZone() !=
tztRow.getTimeZone()
|| !lastTztRow.getTimeZone().getID().
equals(tztRow.getTimeZone().getID())))
tzToAdd = tztRow;
else
tzToAdd = null;
if(tztri.hasNext())
{
lastTztRow = tztRow;
tztRow = tztri.next();
}
else tztRow = null;
}
else
tzToAdd = null;
gis.addPoint(currGpsLocRow.getLatm()/1000000d, currGpsLocRow.getLonm()/1000000d,
currGpsLocRow.getAltitude(), currGpsLocRow.getTime(), tzToAdd);
if(curId % UPDATE_INCREMENT == 0)
super.updateProgress(0, total, curId);
} //while processing points
gis.endSegment();
gis.endTrack();
gis.endDoc();
//wait for the thread to finish copying the data
bis.blockUntilClosed();
}
finally {
DbUtil.closeCursors(c);
GTG.cacheCreatorLock.unregisterReadingThread();
isShutdown = true;
}
}
private boolean createEncryptedZipOutputThread(String filePath,
final String zipEntryName, final OutputStreamToInputStreamPipe bis,
final String password) {
final AesZipFileEncrypter zfe;
try {
//note, we use AESEncrypterBC because AESEncrypterJCA seems to work
//but the resulting file is not decryptable by 7z (and possibly winzip)
//this means we need to include the huge BouncyCastle lib, but I don't
//see another way, really. Also bouncy castle could standardize encryption
// and decryption so we don't have to worry about funky phones with weird
// implementations of java crypto
zfe = new AesZipFileEncrypter(filePath, new AESEncrypterBC());
} catch (IOException e) {
failedException = e;
onWriteFail();
return false;
}
//we copy the data in another thread for speed
new Thread(new Runnable() {
@Override
public void run() {
try {
//this will write the entire stream to os
zfe.add(zipEntryName, bis, password);
} catch (IOException writingException) {
Log.e(GTG.TAG,"Failed writing to os", writingException);
failedException = writingException;
}
bis.close();
try {
zfe.close();
} catch (IOException e) {
Log.e(GTG.TAG,"Failed closing os", e);
failedException = e;
}
}
}).start();
return true;
}
private boolean createRegularZipOutputThread(String filePath, String zipEntryName, final OutputStreamToInputStreamPipe bis) {
final OutputStream os;
try {
os = Util.createZipOutputStream(filePath,zipEntryName);
}
catch(IOException e)
{
failedException = e;
onWriteFail();
return false;
}
//we copy the data in another thread for speed
new Thread(new Runnable() {
@Override
public void run() {
try {
//this will write the entire stream to os
bis.writeTo(os, 4096);
} catch (IOException writingException) {
Log.e(GTG.TAG,"Failed writing to os", writingException);
failedException = writingException;
}
bis.close();
try {
os.close();
} catch (IOException e) {
Log.e(GTG.TAG,"Failed closing os", e);
failedException = e;
}
}
}).start();
return true;
}
@Override
public void doAfterFinish() {
if(isCanceled)
{
new File(filePath).delete();
if(isPause)
return;
new AlertDialog.Builder(CreateGpxBackup.this)
.setCancelable(false)
.setTitle(R.string.backup_has_been_canceled)
.setNeutralButton(R.string.details_ok, null)
.show();
return;
}
else
{
isShowBackupFinishedOnResume=true;
if(!isPause)
showCreateBackupFinished();
}
}
private void onWriteFail() {
notifyFinish();
runOnUiThread(new Runnable() {
@Override
public void run() {
new AlertDialog.Builder(CreateGpxBackup.this)
.setCancelable(false)
.setIcon(R.string.error)
.setMessage(R.string.create_gpx_backup_error_cant_write_backup)
.setNeutralButton(R.string.details_ok, null)
.show();
}
});
}
}, true, false, R.string.create_gpx_backup_progress_title, R.string.create_gpx_backup_progress_msg);
}
private void showCreateBackupFinished() {
if(!isShowBackupFinishedOnResume)
return;
isShowBackupFinishedOnResume = false;
new AlertDialog.Builder(CreateGpxBackup.this)
.setCancelable(false)
.setTitle(R.string.success)
.setMessage(String.format(getText(R.string.create_backup_finished_fmt).toString(), filePath))
.setNeutralButton(R.string.details_ok, new OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
CreateGpxBackup.this.finish();
}
})
.show();
}
public void onCancelButton(View v)
{
finish();
}
public void onPasswordCheckBox(View v)
{
updatePasswordViews();
}
private void updatePasswordViews()
{
findViewById(R.id.password_file_desc).setVisibility(passwordCheckBox.isChecked() ? View.VISIBLE : View.GONE);
enterPasswordView.setText("");
reenterPasswordView.setText("");
enterPasswordView.setVisibility(passwordCheckBox.isChecked() ? View.VISIBLE : View.GONE);
reenterPasswordView.setVisibility(passwordCheckBox.isChecked() ? View.VISIBLE : View.GONE);
}
@Override
public int getRequirements() {
//we use trial expired here because we allow the user to create a backup if trial is expired
return GTG.REQUIREMENTS_TRIAL_EXPIRED_ACTIVITY;
}
}