Support for password-protected zip files

This commit is contained in:
Sylvia van Os
2021-06-29 22:40:02 +02:00
parent 0cc409d087
commit 3dceec8ec0
14 changed files with 339 additions and 115 deletions

View File

@@ -27,6 +27,8 @@ import androidx.appcompat.app.AppCompatActivity;
import androidx.appcompat.widget.Toolbar;
import androidx.core.app.ActivityCompat;
import androidx.core.content.ContextCompat;
import protect.card_locker.importexport.DataFormat;
import protect.card_locker.importexport.ImportExportResult;
public class ImportExportActivity extends AppCompatActivity
{
@@ -162,19 +164,19 @@ public class ImportExportActivity extends AppCompatActivity
builder.show();
}
private void startImport(final InputStream target, final Uri targetUri, final DataFormat dataFormat)
private void startImport(final InputStream target, final Uri targetUri, final DataFormat dataFormat, final char[] password)
{
ImportExportTask.TaskCompleteListener listener = new ImportExportTask.TaskCompleteListener()
{
@Override
public void onTaskComplete(boolean success)
public void onTaskComplete(ImportExportResult result)
{
onImportComplete(success, targetUri);
onImportComplete(result, targetUri);
}
};
importExporter = new ImportExportTask(ImportExportActivity.this,
dataFormat, target, listener);
dataFormat, target, password, listener);
importExporter.execute();
}
@@ -183,9 +185,9 @@ public class ImportExportActivity extends AppCompatActivity
ImportExportTask.TaskCompleteListener listener = new ImportExportTask.TaskCompleteListener()
{
@Override
public void onTaskComplete(boolean success)
public void onTaskComplete(ImportExportResult result)
{
onExportComplete(success, targetUri);
onExportComplete(result, targetUri);
}
};
@@ -245,20 +247,23 @@ public class ImportExportActivity extends AppCompatActivity
return super.onOptionsItemSelected(item);
}
private void onImportComplete(boolean success, Uri path)
private void onImportComplete(ImportExportResult result, Uri path)
{
AlertDialog.Builder builder = new AlertDialog.Builder(this);
if(success)
int messageId;
if(result == ImportExportResult.Success)
{
builder.setTitle(R.string.importSuccessfulTitle);
messageId = R.string.importSuccessful;
}
else
{
builder.setTitle(R.string.importFailedTitle);
messageId = R.string.importFailed;
}
int messageId = success ? R.string.importSuccessful : R.string.importFailed;
final String message = getResources().getString(messageId);
builder.setMessage(message);
@@ -274,20 +279,23 @@ public class ImportExportActivity extends AppCompatActivity
builder.create().show();
}
private void onExportComplete(boolean success, final Uri path)
private void onExportComplete(ImportExportResult result, final Uri path)
{
AlertDialog.Builder builder = new AlertDialog.Builder(this);
if(success)
int messageId;
if(result == ImportExportResult.Success)
{
builder.setTitle(R.string.exportSuccessfulTitle);
messageId = R.string.exportSuccessful;
}
else
{
builder.setTitle(R.string.exportFailedTitle);
messageId = R.string.exportFailed;
}
int messageId = success ? R.string.exportSuccessful : R.string.exportFailed;
final String message = getResources().getString(messageId);
builder.setMessage(message);
@@ -300,7 +308,7 @@ public class ImportExportActivity extends AppCompatActivity
}
});
if(success)
if(result == ImportExportResult.Success)
{
final CharSequence sendLabel = ImportExportActivity.this.getResources().getText(R.string.sendLabel);
@@ -389,7 +397,7 @@ public class ImportExportActivity extends AppCompatActivity
Log.e(TAG, "Starting file import with: " + uri.toString());
startImport(reader, uri, importDataFormat);
startImport(reader, uri, importDataFormat, null);
}
}
catch(FileNotFoundException e)
@@ -397,11 +405,11 @@ public class ImportExportActivity extends AppCompatActivity
Log.e(TAG, "Failed to import/export file: " + uri.toString(), e);
if (requestCode == CHOOSE_EXPORT_LOCATION)
{
onExportComplete(false, uri);
onExportComplete(ImportExportResult.GenericFailure, uri);
}
else
{
onImportComplete(false, uri);
onImportComplete(ImportExportResult.GenericFailure, uri);
}
}
}

View File

@@ -13,10 +13,12 @@ import java.io.OutputStream;
import java.io.OutputStreamWriter;
import java.nio.charset.Charset;
import protect.card_locker.importexport.DataFormat;
import protect.card_locker.importexport.ImportExportResult;
import protect.card_locker.importexport.MultiFormatExporter;
import protect.card_locker.importexport.MultiFormatImporter;
class ImportExportTask extends AsyncTask<Void, Void, Boolean>
class ImportExportTask extends AsyncTask<Void, Void, ImportExportResult>
{
private static final String TAG = "Catima";
@@ -25,6 +27,7 @@ class ImportExportTask extends AsyncTask<Void, Void, Boolean>
private DataFormat format;
private OutputStream outputStream;
private InputStream inputStream;
private char[] password;
private TaskCompleteListener listener;
private ProgressDialog progress;
@@ -46,7 +49,7 @@ class ImportExportTask extends AsyncTask<Void, Void, Boolean>
/**
* Constructor which will setup a task for importing from the given InputStream.
*/
ImportExportTask(Activity activity, DataFormat format, InputStream input,
ImportExportTask(Activity activity, DataFormat format, InputStream input, char[] password,
TaskCompleteListener listener)
{
super();
@@ -54,24 +57,22 @@ class ImportExportTask extends AsyncTask<Void, Void, Boolean>
this.doImport = true;
this.format = format;
this.inputStream = input;
this.password = password;
this.listener = listener;
}
private boolean performImport(Context context, InputStream stream, DBHelper db)
private ImportExportResult performImport(Context context, InputStream stream, DBHelper db, char[] password)
{
boolean result = false;
ImportExportResult importResult = MultiFormatImporter.importData(context, db, stream, format, password);
Log.i(TAG, "Import result: " + importResult.name());
result = MultiFormatImporter.importData(context, db, stream, format);
Log.i(TAG, "Import result: " + result);
return result;
return importResult;
}
private boolean performExport(Context context, OutputStream stream, DBHelper db)
private ImportExportResult performExport(Context context, OutputStream stream, DBHelper db)
{
boolean result = false;
ImportExportResult result = ImportExportResult.GenericFailure;
try
{
@@ -106,14 +107,14 @@ class ImportExportTask extends AsyncTask<Void, Void, Boolean>
progress.show();
}
protected Boolean doInBackground(Void... nothing)
protected ImportExportResult doInBackground(Void... nothing)
{
final DBHelper db = new DBHelper(activity);
boolean result;
ImportExportResult result;
if(doImport)
{
result = performImport(activity.getApplicationContext(), inputStream, db);
result = performImport(activity.getApplicationContext(), inputStream, db, password);
}
else
{
@@ -123,7 +124,7 @@ class ImportExportTask extends AsyncTask<Void, Void, Boolean>
return result;
}
protected void onPostExecute(Boolean result)
protected void onPostExecute(ImportExportResult result)
{
listener.onTaskComplete(result);
@@ -138,7 +139,7 @@ class ImportExportTask extends AsyncTask<Void, Void, Boolean>
}
interface TaskCompleteListener
{
void onTaskComplete(boolean success);
void onTaskComplete(ImportExportResult result);
}
}

View File

@@ -37,7 +37,7 @@ import protect.card_locker.Utils;
*/
public class CsvImporter implements Importer
{
public void importData(Context context, DBHelper db, InputStream input) throws IOException, FormatException, InterruptedException
public void importData(Context context, DBHelper db, InputStream input, char[] password) throws IOException, FormatException, InterruptedException
{
BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(input, StandardCharsets.UTF_8));

View File

@@ -1,9 +1,10 @@
package protect.card_locker;
package protect.card_locker.importexport;
public enum DataFormat
{
Catima,
Fidme,
Stocard,
VoucherVault
;
}

View File

@@ -5,6 +5,9 @@ import android.database.sqlite.SQLiteDatabase;
import com.google.zxing.BarcodeFormat;
import net.lingala.zip4j.io.inputstream.ZipInputStream;
import net.lingala.zip4j.model.LocalFileHeader;
import org.apache.commons.csv.CSVFormat;
import org.apache.commons.csv.CSVParser;
import org.apache.commons.csv.CSVRecord;
@@ -16,8 +19,6 @@ import java.io.StringReader;
import java.math.BigDecimal;
import java.nio.charset.StandardCharsets;
import java.text.ParseException;
import java.util.zip.ZipEntry;
import java.util.zip.ZipInputStream;
import protect.card_locker.DBHelper;
import protect.card_locker.FormatException;
@@ -31,18 +32,18 @@ import protect.card_locker.FormatException;
*/
public class FidmeImporter implements Importer
{
public void importData(Context context, DBHelper db, InputStream input) throws IOException, FormatException, JSONException, ParseException {
public void importData(Context context, DBHelper db, InputStream input, char[] password) throws IOException, FormatException, JSONException, ParseException {
// We actually retrieve a .zip file
ZipInputStream zipInputStream = new ZipInputStream(input);
ZipInputStream zipInputStream = new ZipInputStream(input, password);
StringBuilder loyaltyCards = new StringBuilder();
byte[] buffer = new byte[1024];
int read = 0;
ZipEntry zipEntry;
LocalFileHeader localFileHeader;
while ((zipEntry = zipInputStream.getNextEntry()) != null) {
if (zipEntry.getName().equals("loyalty_programs.csv")) {
while ((localFileHeader = zipInputStream.getNextEntry()) != null) {
if (localFileHeader.getFileName().equals("loyalty_programs.csv")) {
while ((read = zipInputStream.read(buffer, 0, 1024)) >= 0) {
loyaltyCards.append(new String(buffer, 0, read, StandardCharsets.UTF_8));
}

View File

@@ -0,0 +1,9 @@
package protect.card_locker.importexport;
public enum ImportExportResult
{
Success,
GenericFailure,
BadPassword
;
}

View File

@@ -23,5 +23,5 @@ public interface Importer
* @throws IOException
* @throws FormatException
*/
void importData(Context context, DBHelper db, InputStream input) throws IOException, FormatException, InterruptedException, JSONException, ParseException;
void importData(Context context, DBHelper db, InputStream input, char[] password) throws IOException, FormatException, InterruptedException, JSONException, ParseException;
}

View File

@@ -7,7 +7,6 @@ import java.io.IOException;
import java.io.OutputStreamWriter;
import protect.card_locker.DBHelper;
import protect.card_locker.DataFormat;
public class MultiFormatExporter
{
@@ -19,11 +18,11 @@ public class MultiFormatExporter
*
* The output stream is closed on success.
*
* @return true if the database was successfully exported,
* false otherwise. If false, partial data may have been
* @return ImportExportResult.Success if the database was successfully exported,
* another ImportExportResult otherwise. If not Success, partial data may have been
* written to the output stream, and it should be discarded.
*/
public static boolean exportData(Context context, DBHelper db, OutputStreamWriter output, DataFormat format)
public static ImportExportResult exportData(Context context, DBHelper db, OutputStreamWriter output, DataFormat format)
{
Exporter exporter = null;
@@ -42,7 +41,7 @@ public class MultiFormatExporter
try
{
exporter.exportData(context, db, output);
return true;
return ImportExportResult.Success;
}
catch(IOException e)
{
@@ -53,12 +52,12 @@ public class MultiFormatExporter
Log.e(TAG, "Failed to export data", e);
}
return false;
return ImportExportResult.GenericFailure;
}
else
{
Log.e(TAG, "Unsupported data format exported: " + format.name());
return false;
return ImportExportResult.GenericFailure;
}
}
}

View File

@@ -3,6 +3,8 @@ package protect.card_locker.importexport;
import android.content.Context;
import android.util.Log;
import net.lingala.zip4j.exception.ZipException;
import org.json.JSONException;
import java.io.IOException;
@@ -10,7 +12,6 @@ import java.io.InputStream;
import java.text.ParseException;
import protect.card_locker.DBHelper;
import protect.card_locker.DataFormat;
import protect.card_locker.FormatException;
public class MultiFormatImporter
@@ -24,11 +25,11 @@ public class MultiFormatImporter
* The input stream is not closed, and doing so is the
* responsibility of the caller.
*
* @return true if the database was successfully imported,
* false otherwise. If false, no data was written to
* @return ImportExportResult.Success if the database was successfully imported,
* or another result otherwise. If no Success, no data was written to
* the database.
*/
public static boolean importData(Context context, DBHelper db, InputStream input, DataFormat format)
public static ImportExportResult importData(Context context, DBHelper db, InputStream input, DataFormat format, char[] password)
{
Importer importer = null;
@@ -40,6 +41,9 @@ public class MultiFormatImporter
case Fidme:
importer = new FidmeImporter();
break;
case Stocard:
importer = new StocardImporter();
break;
case VoucherVault:
importer = new VoucherVaultImporter();
break;
@@ -49,8 +53,12 @@ public class MultiFormatImporter
{
try
{
importer.importData(context, db, input);
return true;
importer.importData(context, db, input, password);
return ImportExportResult.Success;
}
catch(ZipException e)
{
return ImportExportResult.BadPassword;
}
catch(IOException | FormatException | InterruptedException | JSONException | ParseException e)
{
@@ -62,6 +70,7 @@ public class MultiFormatImporter
{
Log.e(TAG, "Unsupported data format imported: " + format.name());
}
return false;
return ImportExportResult.GenericFailure;
}
}

View File

@@ -0,0 +1,149 @@
package protect.card_locker.importexport;
import android.content.Context;
import android.database.sqlite.SQLiteDatabase;
import android.util.Log;
import com.google.zxing.BarcodeFormat;
import net.lingala.zip4j.io.inputstream.ZipInputStream;
import net.lingala.zip4j.model.LocalFileHeader;
import org.apache.commons.csv.CSVFormat;
import org.apache.commons.csv.CSVParser;
import org.apache.commons.csv.CSVRecord;
import org.json.JSONException;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.StringReader;
import java.math.BigDecimal;
import java.text.ParseException;
import protect.card_locker.DBHelper;
import protect.card_locker.FormatException;
/**
* Class for importing a database from CSV (Comma Separate Values)
* formatted data.
*
* The database's loyalty cards are expected to appear in the CSV data.
* A header is expected for the each table showing the names of the columns.
*/
public class StocardImporter implements Importer
{
public void importData(Context context, DBHelper db, InputStream input, char[] password) throws IOException, FormatException, JSONException, ParseException {
LocalFileHeader localFileHeader;
// We actually retrieve a .zip file
ZipInputStream zipInputStream = new ZipInputStream(input, password);
StringBuilder loyaltyCards = new StringBuilder();
byte[] buffer = new byte[1024];
int read = 0;
while ((localFileHeader = zipInputStream.getNextEntry()) != null) {
Log.w("STO", localFileHeader.getFileName());
//File extractedFile = new File(localFileHeader.getFileName());
//if (localFileHeader.isDirectory()) {
// localFileHeader = zipInputStream.getNextEntry(localFileHeader);
//}
//if (!localFileHeader.isDirectory()) {
// File extractedFile = new File(localFileHeader.getFileName());
// OutputStream outputStream = new FileOutputStream(extractedFile);
// while ((read = zipInputStream.read(buffer)) != -1) {
// outputStream.write(buffer, 0, read);
// }
//}
}
if (loyaltyCards.length() == 0) {
throw new FormatException("Couldn't find loyalty_programs.csv in zip file or it is empty");
}
SQLiteDatabase database = db.getWritableDatabase();
database.beginTransaction();
final CSVParser fidmeParser = new CSVParser(new StringReader(loyaltyCards.toString()), CSVFormat.RFC4180.withDelimiter(';').withHeader());
try {
for (CSVRecord record : fidmeParser) {
importLoyaltyCard(database, db, record);
if (Thread.currentThread().isInterrupted()) {
throw new InterruptedException();
}
}
} catch (IllegalArgumentException | IllegalStateException | InterruptedException e) {
throw new FormatException("Issue parsing CSV data", e);
} finally {
fidmeParser.close();
}
database.setTransactionSuccessful();
database.endTransaction();
database.close();
zipInputStream.close();
}
/**
* Import a single loyalty card into the database using the given
* session.
*/
private void importLoyaltyCard(SQLiteDatabase database, DBHelper helper, CSVRecord record)
throws IOException, FormatException
{
// A loyalty card export from Fidme contains the following fields:
// Retailer (store name)
// Program (program name)
// Added at (YYYY-MM-DD HH:MM:SS UTC)
// Reference (card ID)
// Firstname (card holder first name)
// Lastname (card holder last name)
// The store is called Retailer
String store = CSVHelpers.extractString("Retailer", record, "");
if (store.isEmpty())
{
throw new FormatException("No store listed, but is required");
}
// There seems to be no note field in the CSV? So let's combine other fields instead...
String program = CSVHelpers.extractString("Program", record, "").trim();
String addedAt = CSVHelpers.extractString("Added At", record, "").trim();
String firstName = CSVHelpers.extractString("Firstname", record, "").trim();
String lastName = CSVHelpers.extractString("Lastname", record, "").trim();
String combinedName = String.format("%s %s", firstName, lastName).trim();
StringBuilder noteBuilder = new StringBuilder();
if (!program.isEmpty()) noteBuilder.append(program).append('\n');
if (!addedAt.isEmpty()) noteBuilder.append(addedAt).append('\n');
if (!combinedName.isEmpty()) noteBuilder.append(combinedName).append('\n');
String note = noteBuilder.toString().trim();
// The ID is called reference
String cardId = CSVHelpers.extractString("Reference", record, "");
if(cardId.isEmpty())
{
throw new FormatException("No card ID listed, but is required");
}
// Sadly, Fidme exports don't contain the card type
// I guess they have an online DB of all the different companies and what type they use
// TODO: Hook this into our own loyalty card DB if we ever get one
BarcodeFormat barcodeType = null;
// No favourite data in the export either
int starStatus = 0;
// TODO: Front and back image
helper.insertLoyaltyCard(database, store, note, null, BigDecimal.valueOf(0), null, cardId, null, barcodeType, null, starStatus);
}
}

View File

@@ -35,7 +35,7 @@ import protect.card_locker.FormatException;
*/
public class VoucherVaultImporter implements Importer
{
public void importData(Context context, DBHelper db, InputStream input) throws IOException, FormatException, JSONException, ParseException {
public void importData(Context context, DBHelper db, InputStream input, char[] password) throws IOException, FormatException, JSONException, ParseException {
BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(input, StandardCharsets.UTF_8));
StringBuilder sb = new StringBuilder();