From fb34fb7451604bb618bc2070253778c01056233c Mon Sep 17 00:00:00 2001 From: Branden Archer Date: Thu, 12 May 2016 22:51:51 -0400 Subject: [PATCH 1/9] Add call for writing card entry to specific id This will be necessary when importing card entries later --- .../main/java/protect/card_locker/DBHelper.java | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/app/src/main/java/protect/card_locker/DBHelper.java b/app/src/main/java/protect/card_locker/DBHelper.java index 616dbba96..1538a88fb 100644 --- a/app/src/main/java/protect/card_locker/DBHelper.java +++ b/app/src/main/java/protect/card_locker/DBHelper.java @@ -64,6 +64,20 @@ public class DBHelper extends SQLiteOpenHelper return (newId != -1); } + public boolean insertLoyaltyCard(final SQLiteDatabase db, final int id, + final String store, final String note, final String cardId, + final String barcodeType) + { + ContentValues contentValues = new ContentValues(); + contentValues.put(LoyaltyCardDbIds.ID, id); + contentValues.put(LoyaltyCardDbIds.STORE, store); + contentValues.put(LoyaltyCardDbIds.NOTE, note); + contentValues.put(LoyaltyCardDbIds.CARD_ID, cardId); + contentValues.put(LoyaltyCardDbIds.BARCODE_TYPE, barcodeType); + final long newId = db.insert(LoyaltyCardDbIds.TABLE, null, contentValues); + return (newId != -1); + } + public boolean updateLoyaltyCard(final int id, final String store, final String note, final String cardId, final String barcodeType) From ce272fe7f13c998001f30227f6e04c4bba8ad63e Mon Sep 17 00:00:00 2001 From: Branden Archer Date: Thu, 12 May 2016 22:53:52 -0400 Subject: [PATCH 2/9] Add ability to import/export cards to/from CSV --- app/build.gradle | 1 + app/src/main/AndroidManifest.xml | 3 +- .../card_locker/CsvDatabaseExporter.java | 50 +++++ .../card_locker/CsvDatabaseImporter.java | 135 ++++++++++++ .../java/protect/card_locker/DataFormat.java | 8 + .../protect/card_locker/DatabaseExporter.java | 17 ++ .../protect/card_locker/DatabaseImporter.java | 19 ++ .../protect/card_locker/FormatException.java | 19 ++ .../card_locker/MultiFormatExporter.java | 57 +++++ .../card_locker/MultiFormatImporter.java | 62 ++++++ .../protect/card_locker/ImportExportTest.java | 207 ++++++++++++++++++ 11 files changed, 577 insertions(+), 1 deletion(-) create mode 100644 app/src/main/java/protect/card_locker/CsvDatabaseExporter.java create mode 100644 app/src/main/java/protect/card_locker/CsvDatabaseImporter.java create mode 100644 app/src/main/java/protect/card_locker/DataFormat.java create mode 100644 app/src/main/java/protect/card_locker/DatabaseExporter.java create mode 100644 app/src/main/java/protect/card_locker/DatabaseImporter.java create mode 100644 app/src/main/java/protect/card_locker/FormatException.java create mode 100644 app/src/main/java/protect/card_locker/MultiFormatExporter.java create mode 100644 app/src/main/java/protect/card_locker/MultiFormatImporter.java create mode 100644 app/src/test/java/protect/card_locker/ImportExportTest.java diff --git a/app/build.gradle b/app/build.gradle index a05fd310d..53b8b1014 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -39,6 +39,7 @@ dependencies { compile 'com.android.support:design:23.1.1' compile 'com.journeyapps:zxing-android-embedded:3.2.0@aar' compile 'com.google.zxing:core:3.2.1' + compile 'org.apache.commons:commons-csv:1.2' testCompile 'junit:junit:4.12' testCompile "org.robolectric:robolectric:3.0" } diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index fdc322e6b..611e5bc82 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -5,7 +5,8 @@ + 0; index--) + { + String storeName = String.format("store, \"%4d", index); + String note = String.format("note, \"%4d", index); + boolean result = db.insertLoyaltyCard(storeName, note, BARCODE_DATA, BARCODE_TYPE); + assertTrue(result); + } + + assertEquals(cardsToAdd, db.getLoyaltyCardCount()); + } + + /** + * Check that all of the cards follow the pattern + * specified in addLoyaltyCards(), and are in sequential order + * where the smallest card's index is 1 + */ + private void checkLoyaltyCards() + { + Cursor cursor = db.getLoyaltyCardCursor(); + int index = 1; + + while(cursor.moveToNext()) + { + LoyaltyCard card = LoyaltyCard.toLoyaltyCard(cursor); + + String expectedStore = String.format("store, \"%4d", index); + String expectedNote = String.format("note, \"%4d", index); + + assertEquals(expectedStore, card.store); + assertEquals(expectedNote, card.note); + assertEquals(BARCODE_DATA, card.cardId); + assertEquals(BARCODE_TYPE, card.barcodeType); + + index++; + } + cursor.close(); + } + + /** + * Delete the contents of the database + */ + private void clearDatabase() + { + SQLiteDatabase database = db.getWritableDatabase(); + database.execSQL("delete from " + DBHelper.LoyaltyCardDbIds.TABLE); + database.close(); + + assertEquals(0, db.getLoyaltyCardCount()); + } + + @Test + public void multipleCardsExportImport() throws IOException + { + final int NUM_CARDS = 1000; + + for(DataFormat format : DataFormat.values()) + { + addLoyaltyCards(NUM_CARDS); + + ByteArrayOutputStream outData = new ByteArrayOutputStream(); + OutputStreamWriter outStream = new OutputStreamWriter(outData); + + // Export data to CSV format + boolean result = MultiFormatExporter.exportData(db, outStream, format); + assertTrue(result); + outStream.close(); + + clearDatabase(); + + ByteArrayInputStream inData = new ByteArrayInputStream(outData.toByteArray()); + InputStreamReader inStream = new InputStreamReader(inData); + + // Import the CSV data + result = MultiFormatImporter.importData(db, inStream, DataFormat.CSV); + assertTrue(result); + + assertEquals(NUM_CARDS, db.getLoyaltyCardCount()); + + checkLoyaltyCards(); + + // Clear the database for the next format under test + clearDatabase(); + } + } + + @Test + public void importExistingCardsNotReplace() throws IOException + { + final int NUM_CARDS = 1000; + + for(DataFormat format : DataFormat.values()) + { + addLoyaltyCards(NUM_CARDS); + + ByteArrayOutputStream outData = new ByteArrayOutputStream(); + OutputStreamWriter outStream = new OutputStreamWriter(outData); + + // Export into CSV data + boolean result = MultiFormatExporter.exportData(db, outStream, format); + assertTrue(result); + outStream.close(); + + ByteArrayInputStream inData = new ByteArrayInputStream(outData.toByteArray()); + InputStreamReader inStream = new InputStreamReader(inData); + + // Import the CSV data on top of the existing database + result = MultiFormatImporter.importData(db, inStream, DataFormat.CSV); + assertTrue(result); + + assertEquals(NUM_CARDS, db.getLoyaltyCardCount()); + + checkLoyaltyCards(); + + // Clear the database for the next format under test + clearDatabase(); + } + } + + @Test + public void corruptedImportNothingSaved() throws IOException + { + final int NUM_CARDS = 1000; + + for(DataFormat format : DataFormat.values()) + { + addLoyaltyCards(NUM_CARDS); + + ByteArrayOutputStream outData = new ByteArrayOutputStream(); + OutputStreamWriter outStream = new OutputStreamWriter(outData); + + // Export data to CSV format + boolean result = MultiFormatExporter.exportData(db, outStream, format); + assertTrue(result); + + clearDatabase(); + + String corruptEntry = "ThisStringIsLikelyNotPartOfAnyFormat"; + + ByteArrayInputStream inData = new ByteArrayInputStream((outData.toString() + corruptEntry).getBytes()); + InputStreamReader inStream = new InputStreamReader(inData); + + // Attempt to import the CSV data + result = MultiFormatImporter.importData(db, inStream, DataFormat.CSV); + assertEquals(false, result); + + assertEquals(0, db.getLoyaltyCardCount()); + } + } +} From 20338eb09bf456abefaceb8c13021afca8c17cc3 Mon Sep 17 00:00:00 2001 From: Branden Archer Date: Thu, 12 May 2016 22:54:18 -0400 Subject: [PATCH 3/9] Add task for importing/exporting --- .../protect/card_locker/ImportExportTask.java | 146 ++++++++++++++++++ app/src/main/res/values/strings.xml | 12 ++ .../protect/card_locker/ImportExportTest.java | 35 +++++ 3 files changed, 193 insertions(+) create mode 100644 app/src/main/java/protect/card_locker/ImportExportTask.java diff --git a/app/src/main/java/protect/card_locker/ImportExportTask.java b/app/src/main/java/protect/card_locker/ImportExportTask.java new file mode 100644 index 000000000..a3fd3c58f --- /dev/null +++ b/app/src/main/java/protect/card_locker/ImportExportTask.java @@ -0,0 +1,146 @@ +package protect.card_locker; + +import android.app.Activity; +import android.app.ProgressDialog; +import android.content.DialogInterface; +import android.os.AsyncTask; +import android.os.Environment; +import android.util.Log; +import android.widget.Toast; + +import java.io.File; +import java.io.FileInputStream; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStreamReader; +import java.io.OutputStreamWriter; +import java.nio.charset.Charset; + +class ImportExportTask extends AsyncTask +{ + private static final String TAG = "BudgetWatch"; + + private static final String TARGET_FILE = "LoyaltyCardLocker.csv"; + + private Activity activity; + private boolean doImport; + private DataFormat format; + + private ProgressDialog progress; + + public ImportExportTask(Activity activity, boolean doImport, DataFormat format) + { + super(); + this.activity = activity; + this.doImport = doImport; + this.format = format; + } + + private void toastWithArg(int stringId, String argument) + { + final String template = activity.getResources().getString(stringId); + final String message = String.format(template, argument); + + activity.runOnUiThread(new Runnable() + { + @Override + public void run() + { + Toast.makeText(activity, message, Toast.LENGTH_LONG).show(); + } + }); + } + + private void performImport(File importFile, DBHelper db) + { + if(importFile.exists() == false) + { + toastWithArg(R.string.fileMissing, importFile.getAbsolutePath()); + return; + } + + boolean result = false; + + try + { + FileInputStream fileReader = new FileInputStream(importFile); + InputStreamReader reader = new InputStreamReader(fileReader, Charset.forName("UTF-8")); + result = MultiFormatImporter.importData(db, reader, format); + reader.close(); + } + catch(IOException e) + { + Log.e(TAG, "Unable to import file", e); + } + + int messageId = result ? R.string.importedFrom : R.string.importFailed; + toastWithArg(messageId, importFile.getAbsolutePath()); + } + + private void performExport(File exportFile, DBHelper db) + { + boolean result = false; + + try + { + FileOutputStream fileWriter = new FileOutputStream(exportFile); + OutputStreamWriter writer = new OutputStreamWriter(fileWriter, Charset.forName("UTF-8")); + result = MultiFormatExporter.exportData(db, writer, format); + writer.close(); + } + catch (IOException e) + { + Log.e(TAG, "Unable to export file", e); + } + + int messageId = result ? R.string.exportedTo : R.string.exportFailed; + toastWithArg(messageId, exportFile.getAbsolutePath()); + } + + protected void onPreExecute() + { + progress = new ProgressDialog(activity); + progress.setTitle(doImport ? R.string.importing : R.string.exporting); + + progress.setOnDismissListener(new DialogInterface.OnDismissListener() + { + @Override + public void onDismiss(DialogInterface dialog) + { + ImportExportTask.this.cancel(true); + } + }); + + progress.show(); + } + + protected Void doInBackground(Void... nothing) + { + final File sdcardDir = Environment.getExternalStorageDirectory(); + final File importExportFile = new File(sdcardDir, TARGET_FILE); + final DBHelper db = new DBHelper(activity); + + if(doImport) + { + performImport(importExportFile, db); + } + else + { + performExport(importExportFile, db); + } + + return null; + } + + protected void onPostExecute(Void result) + { + progress.dismiss(); + Log.i(TAG, (doImport ? "Import" : "Export") + " Complete"); + } + + protected void onCancelled() + { + progress.dismiss(); + Log.i(TAG, (doImport ? "Import" : "Export") + " Cancelled"); + } +} diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index c1de29597..6242a5c68 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -27,4 +27,16 @@ %1$s: %2$s %1$s - %2$s + + Import/Export + Import + Export + Data is imported to/exported from LoyaltyCardLocker.csv on external storage + Imported from: %1$s + Exported to: %1$s + File missing: %1$s + Failed to import: %1$s + Failed to export: %1$s + Importing… + Exporting… \ No newline at end of file diff --git a/app/src/test/java/protect/card_locker/ImportExportTest.java b/app/src/test/java/protect/card_locker/ImportExportTest.java index 5e1b69900..0950d4b59 100644 --- a/app/src/test/java/protect/card_locker/ImportExportTest.java +++ b/app/src/test/java/protect/card_locker/ImportExportTest.java @@ -204,4 +204,39 @@ public class ImportExportTest assertEquals(0, db.getLoyaltyCardCount()); } } + + @Test + public void useImportExportTask() + { + final int NUM_CARDS = 10; + + for(DataFormat format : DataFormat.values()) + { + addLoyaltyCards(NUM_CARDS); + + // Export to whatever the default location is + ImportExportTask task = new ImportExportTask(activity, false, format); + task.execute(); + + // Actually run the task to completion + Robolectric.flushBackgroundThreadScheduler(); + + clearDatabase(); + + // Import everything back from the default location + + task = new ImportExportTask(activity, true, format); + task.execute(); + + // Actually run the task to completion + Robolectric.flushBackgroundThreadScheduler(); + + assertEquals(NUM_CARDS, db.getLoyaltyCardCount()); + + checkLoyaltyCards(); + + // Clear the database for the next format under test + clearDatabase(); + } + } } From b8f3d891ea4d5b14e54e5152e313a17d66b2c3a3 Mon Sep 17 00:00:00 2001 From: Branden Archer Date: Thu, 12 May 2016 22:56:15 -0400 Subject: [PATCH 4/9] Allow user to import/export cards to/from CSV on external storage Italian translations provided by Airon90. Dutch translations provided by PanderMusubi --- app/src/main/AndroidManifest.xml | 6 ++ .../card_locker/ImportExportActivity.java | 63 ++++++++++++++++++ .../protect/card_locker/MainActivity.java | 7 ++ .../ic_import_export_white_24dp.png | Bin 0 -> 172 bytes .../ic_import_export_white_24dp.png | Bin 0 -> 132 bytes .../ic_import_export_white_24dp.png | Bin 0 -> 202 bytes .../ic_import_export_white_24dp.png | Bin 0 -> 252 bytes .../ic_import_export_white_24dp.png | Bin 0 -> 328 bytes .../res/layout/import_export_activity.xml | 54 +++++++++++++++ app/src/main/res/menu/main_menu.xml | 5 ++ app/src/main/res/values-it/strings.xml | 17 ++++- app/src/main/res/values-nl/strings.xml | 17 ++++- .../protect/card_locker/MainActivityTest.java | 3 +- 13 files changed, 169 insertions(+), 3 deletions(-) create mode 100644 app/src/main/java/protect/card_locker/ImportExportActivity.java create mode 100644 app/src/main/res/drawable-hdpi/ic_import_export_white_24dp.png create mode 100644 app/src/main/res/drawable-mdpi/ic_import_export_white_24dp.png create mode 100644 app/src/main/res/drawable-xhdpi/ic_import_export_white_24dp.png create mode 100644 app/src/main/res/drawable-xxhdpi/ic_import_export_white_24dp.png create mode 100644 app/src/main/res/drawable-xxxhdpi/ic_import_export_white_24dp.png create mode 100644 app/src/main/res/layout/import_export_activity.xml diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 611e5bc82..f55a3bfed 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -38,6 +38,12 @@ android:configChanges="orientation|screenSize" android:windowSoftInputMode="stateHidden" android:parentActivityName="protect.card_locker.MainActivity"/> + diff --git a/app/src/main/java/protect/card_locker/ImportExportActivity.java b/app/src/main/java/protect/card_locker/ImportExportActivity.java new file mode 100644 index 000000000..09e618049 --- /dev/null +++ b/app/src/main/java/protect/card_locker/ImportExportActivity.java @@ -0,0 +1,63 @@ +package protect.card_locker; + +import android.os.AsyncTask; +import android.os.Bundle; +import android.support.v7.app.ActionBar; +import android.support.v7.app.AppCompatActivity; +import android.support.v7.widget.Toolbar; +import android.view.View; +import android.widget.Button; + +public class ImportExportActivity extends AppCompatActivity +{ + + ImportExportTask importExporter; + + @Override + protected void onCreate(Bundle savedInstanceState) + { + super.onCreate(savedInstanceState); + setContentView(R.layout.import_export_activity); + Toolbar toolbar = (Toolbar) findViewById(R.id.toolbar); + setSupportActionBar(toolbar); + ActionBar actionBar = getSupportActionBar(); + if(actionBar != null) + { + actionBar.setDisplayHomeAsUpEnabled(true); + } + + Button importButton = (Button)findViewById(R.id.importButton); + importButton.setOnClickListener(new View.OnClickListener() + { + @Override + public void onClick(View v) + { + importExporter = new ImportExportTask(ImportExportActivity.this, + true, DataFormat.CSV); + importExporter.execute(); + } + }); + + Button exportButton = (Button)findViewById(R.id.exportButton); + exportButton.setOnClickListener(new View.OnClickListener() + { + @Override + public void onClick(View v) + { + importExporter = new ImportExportTask(ImportExportActivity.this, + false, DataFormat.CSV); + importExporter.execute(); + } + }); + } + + @Override + protected void onDestroy() + { + if(importExporter != null && importExporter.getStatus() != AsyncTask.Status.RUNNING) + { + importExporter.cancel(true); + } + super.onDestroy(); + } +} \ No newline at end of file diff --git a/app/src/main/java/protect/card_locker/MainActivity.java b/app/src/main/java/protect/card_locker/MainActivity.java index b7819033e..3661b7303 100644 --- a/app/src/main/java/protect/card_locker/MainActivity.java +++ b/app/src/main/java/protect/card_locker/MainActivity.java @@ -92,6 +92,13 @@ public class MainActivity extends AppCompatActivity return true; } + if(id == R.id.action_import_export) + { + Intent i = new Intent(getApplicationContext(), ImportExportActivity.class); + startActivity(i); + return true; + } + return super.onOptionsItemSelected(item); } } \ No newline at end of file diff --git a/app/src/main/res/drawable-hdpi/ic_import_export_white_24dp.png b/app/src/main/res/drawable-hdpi/ic_import_export_white_24dp.png new file mode 100644 index 0000000000000000000000000000000000000000..5b6c020108fd824e2acf1bd3fb9654344468aeb3 GIT binary patch literal 172 zcmeAS@N?(olHy`uVBq!ia0vp^Dj>|k0wldT1B8K8v8Rh;h)3t!bJju)3OufXWlY|Q ziDC`h_2)hWZ@D)F2G-W_5)*1r=uZlf_zQbY2xrsZVK^@n_JGlrj2 Ve(jXz+X%Ft!PC{xWt~$(697ERKFX+q~(rTgsw&_Laz&Y5z<#jFVdQ&MBb@02D=JU97u{ibH(BFHEj!mAa|dqi1qaxfezj^G;8jS-2Fe*6JCN!1ZNsuu$-}m1 z3Tq}OZx8JeNT@iK_=r{&XS3j3^P6sGas0NH%>vC=N?lyORBLI?v}le?Rm{AyTv1$|*>iGEvPyc` zYE5zVt|hM3XYb7kb=Tc#F;C@g$+oM_mtW`k&E5N2+3x)+8Gm*KhX#gNh9_NYEF1zr zc0~gtBNNLDe&yNimZjg$C8u1L5U)R--BUB^iRD!@<9}iIivKeQ9g#|E7uhO$Gc0_Y z&`*A;phBr78Ruq)1QlupiJfyhbwp8VSDybKP4B(@ACBJydWXT&)z4*}Q$iB}uv=m# literal 0 HcmV?d00001 diff --git a/app/src/main/res/drawable-xxxhdpi/ic_import_export_white_24dp.png b/app/src/main/res/drawable-xxxhdpi/ic_import_export_white_24dp.png new file mode 100644 index 0000000000000000000000000000000000000000..a5e55a4707c286e7068434f14468725902469e46 GIT binary patch literal 328 zcmeAS@N?(olHy`uVBq!ia0vp^2_VeD0wg^q?%&M7z{v0E;uunK>+S5bTulKIthL3a zpSNGnz4*>0QK4w+YS$X}v^%?I-0XO?)0GjZi6MTG*VN5*YsJ@b?t0<;cE$aSlSiZP z>pF%n&4~FDom*IX;r$l(*K6f%9rqT!SoDhj)YASH_OCdax7Tf9@wGZE;rf5sW*@;X z-ghTopY@W!_+)9|v31(NuDy0~+OqQ2xosCy-&ZIozKSVMR(NePUV8g{Lgyfrfc(s=X?cuujvCzP}U63Uj2zrL7w$= L^>bP0l+XkK5r~T2 literal 0 HcmV?d00001 diff --git a/app/src/main/res/layout/import_export_activity.xml b/app/src/main/res/layout/import_export_activity.xml new file mode 100644 index 000000000..3291f87f3 --- /dev/null +++ b/app/src/main/res/layout/import_export_activity.xml @@ -0,0 +1,54 @@ + + + + + + + + + + + + + +