diff --git a/.travis.yml b/.travis.yml index d3aa2b9d6..f669eaeef 100644 --- a/.travis.yml +++ b/.travis.yml @@ -19,7 +19,7 @@ android: # if you need to run emulator(s) during your tests - sys-img-x86-android-17 -script: gradle build lint findbugs test +script: ./gradlew build lint findbugs test after_failure: - cat app/build/outputs/lint-results.xml 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..f55a3bfed 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -5,7 +5,8 @@ + + diff --git a/app/src/main/java/protect/card_locker/CsvDatabaseExporter.java b/app/src/main/java/protect/card_locker/CsvDatabaseExporter.java new file mode 100644 index 000000000..17d8046b4 --- /dev/null +++ b/app/src/main/java/protect/card_locker/CsvDatabaseExporter.java @@ -0,0 +1,50 @@ +package protect.card_locker; + +import android.database.Cursor; + +import org.apache.commons.csv.CSVFormat; +import org.apache.commons.csv.CSVPrinter; + +import java.io.IOException; +import java.io.OutputStreamWriter; + +/** + * Class for exporting the database into CSV (Comma Separate Values) + * format. + */ +public class CsvDatabaseExporter implements DatabaseExporter +{ + public void exportData(DBHelper db, OutputStreamWriter output) throws IOException, InterruptedException + { + CSVPrinter printer = new CSVPrinter(output, CSVFormat.RFC4180); + + // Print the header + printer.printRecord(DBHelper.LoyaltyCardDbIds.ID, + DBHelper.LoyaltyCardDbIds.STORE, + DBHelper.LoyaltyCardDbIds.NOTE, + DBHelper.LoyaltyCardDbIds.CARD_ID, + DBHelper.LoyaltyCardDbIds.BARCODE_TYPE); + + Cursor cursor = db.getLoyaltyCardCursor(); + + while(cursor.moveToNext()) + { + LoyaltyCard card = LoyaltyCard.toLoyaltyCard(cursor); + + printer.printRecord(card.id, + card.store, + card.note, + card.cardId, + card.barcodeType); + + if(Thread.currentThread().isInterrupted()) + { + throw new InterruptedException(); + } + } + + cursor.close(); + + printer.close(); + } +} diff --git a/app/src/main/java/protect/card_locker/CsvDatabaseImporter.java b/app/src/main/java/protect/card_locker/CsvDatabaseImporter.java new file mode 100644 index 000000000..61a5e9f0a --- /dev/null +++ b/app/src/main/java/protect/card_locker/CsvDatabaseImporter.java @@ -0,0 +1,135 @@ +package protect.card_locker; + +import android.database.sqlite.SQLiteDatabase; + +import org.apache.commons.csv.CSVFormat; +import org.apache.commons.csv.CSVParser; +import org.apache.commons.csv.CSVRecord; + +import java.io.IOException; +import java.io.InputStreamReader; + +/** + * 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 CsvDatabaseImporter implements DatabaseImporter +{ + public void importData(DBHelper db, InputStreamReader input) throws IOException, FormatException, InterruptedException + { + final CSVParser parser = new CSVParser(input, CSVFormat.RFC4180.withHeader()); + + SQLiteDatabase database = db.getWritableDatabase(); + database.beginTransaction(); + + try + { + for (CSVRecord record : parser) + { + importLoyaltyCard(database, db, record); + + if(Thread.currentThread().isInterrupted()) + { + throw new InterruptedException(); + } + } + + parser.close(); + database.setTransactionSuccessful(); + } + catch(IllegalArgumentException e) + { + throw new FormatException("Issue parsing CSV data", e); + } + finally + { + database.endTransaction(); + database.close(); + } + } + + /** + * Extract a string from the items array. The index into the array + * is determined by looking up the index in the fields map using the + * "key" as the key. If no such key exists, defaultValue is returned + * if it is not null. Otherwise, a FormatException is thrown. + */ + private String extractString(String key, CSVRecord record, String defaultValue) + throws FormatException + { + String toReturn = defaultValue; + + if(record.isMapped(key)) + { + toReturn = record.get(key); + } + else + { + if(defaultValue == null) + { + throw new FormatException("Field not used but expected: " + key); + } + } + + return toReturn; + } + + /** + * Extract an integer from the items array. The index into the array + * is determined by looking up the index in the fields map using the + * "key" as the key. If no such key exists, or the data is not a valid + * int, a FormatException is thrown. + */ + private int extractInt(String key, CSVRecord record) + throws FormatException + { + if(record.isMapped(key) == false) + { + throw new FormatException("Field not used but expected: " + key); + } + + try + { + return Integer.parseInt(record.get(key)); + } + catch(NumberFormatException e) + { + throw new FormatException("Failed to parse field: " + key, e); + } + } + + /** + * Import a single loyalty card into the database using the given + * session. + */ + private void importLoyaltyCard(SQLiteDatabase database, DBHelper helper, CSVRecord record) + throws IOException, FormatException + { + int id = extractInt(DBHelper.LoyaltyCardDbIds.ID, record); + + String store = extractString(DBHelper.LoyaltyCardDbIds.STORE, record, ""); + if(store.isEmpty()) + { + throw new FormatException("No store listed, but is required"); + } + + String note = extractString(DBHelper.LoyaltyCardDbIds.NOTE, record, ""); + + String cardId = extractString(DBHelper.LoyaltyCardDbIds.CARD_ID, record, ""); + if(cardId.isEmpty()) + { + throw new FormatException("No card ID listed, but is required"); + } + + String barcodeType = extractString(DBHelper.LoyaltyCardDbIds.BARCODE_TYPE, record, ""); + if(barcodeType.isEmpty()) + { + throw new FormatException("No barcode type listed, but is required"); + } + + helper.insertLoyaltyCard(database, id, store, note, cardId, barcodeType); + } +} 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) diff --git a/app/src/main/java/protect/card_locker/DataFormat.java b/app/src/main/java/protect/card_locker/DataFormat.java new file mode 100644 index 000000000..722700f00 --- /dev/null +++ b/app/src/main/java/protect/card_locker/DataFormat.java @@ -0,0 +1,8 @@ +package protect.card_locker; + +public enum DataFormat +{ + CSV, + + ; +} diff --git a/app/src/main/java/protect/card_locker/DatabaseExporter.java b/app/src/main/java/protect/card_locker/DatabaseExporter.java new file mode 100644 index 000000000..94e4949d6 --- /dev/null +++ b/app/src/main/java/protect/card_locker/DatabaseExporter.java @@ -0,0 +1,17 @@ +package protect.card_locker; + +import java.io.IOException; +import java.io.OutputStreamWriter; + +/** + * Interface for a class which can export the contents of the database + * in a given format. + */ +public interface DatabaseExporter +{ + /** + * Export the database to the output stream in a given format. + * @throws IOException + */ + void exportData(DBHelper db, OutputStreamWriter output) throws IOException, InterruptedException; +} diff --git a/app/src/main/java/protect/card_locker/DatabaseImporter.java b/app/src/main/java/protect/card_locker/DatabaseImporter.java new file mode 100644 index 000000000..33d2797bf --- /dev/null +++ b/app/src/main/java/protect/card_locker/DatabaseImporter.java @@ -0,0 +1,19 @@ +package protect.card_locker; + +import java.io.IOException; +import java.io.InputStreamReader; + +/** + * Interface for a class which can import the contents of a stream + * into the database. + */ +public interface DatabaseImporter +{ + /** + * Import data from the input stream in a given format into + * the database. + * @throws IOException + * @throws FormatException + */ + void importData(DBHelper db, InputStreamReader input) throws IOException, FormatException, InterruptedException; +} diff --git a/app/src/main/java/protect/card_locker/FormatException.java b/app/src/main/java/protect/card_locker/FormatException.java new file mode 100644 index 000000000..db23ebfd4 --- /dev/null +++ b/app/src/main/java/protect/card_locker/FormatException.java @@ -0,0 +1,19 @@ +package protect.card_locker; + +/** + * Exception thrown when something unexpected is + * encountered with the format of data being + * imported or exported. + */ +public class FormatException extends Exception +{ + public FormatException(String message) + { + super(message); + } + + public FormatException(String message, Exception rootCause) + { + super(message, rootCause); + } +} 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..92c03a429 --- /dev/null +++ b/app/src/main/java/protect/card_locker/ImportExportActivity.java @@ -0,0 +1,131 @@ +package protect.card_locker; + +import android.Manifest; +import android.content.pm.PackageManager; +import android.os.AsyncTask; +import android.os.Bundle; +import android.support.v4.app.ActivityCompat; +import android.support.v4.content.ContextCompat; +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; +import android.widget.Toast; + +public class ImportExportActivity extends AppCompatActivity +{ + private static final int PERMISSIONS_EXTERNAL_STORAGE_IMPORT = 1; + private static final int PERMISSIONS_EXTERNAL_STORAGE_EXPORT = 2; + + 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) + { + if (ContextCompat.checkSelfPermission(ImportExportActivity.this, + Manifest.permission.READ_EXTERNAL_STORAGE) == PackageManager.PERMISSION_GRANTED) + { + startImport(); + } + else + { + ActivityCompat.requestPermissions(ImportExportActivity.this, + new String[]{Manifest.permission.READ_EXTERNAL_STORAGE}, + PERMISSIONS_EXTERNAL_STORAGE_IMPORT); + } + } + }); + + Button exportButton = (Button)findViewById(R.id.exportButton); + exportButton.setOnClickListener(new View.OnClickListener() + { + @Override + public void onClick(View v) + { + if (ContextCompat.checkSelfPermission(ImportExportActivity.this, + Manifest.permission.WRITE_EXTERNAL_STORAGE) == PackageManager.PERMISSION_GRANTED) + { + startExport(); + } + else + { + ActivityCompat.requestPermissions(ImportExportActivity.this, + new String[]{Manifest.permission.WRITE_EXTERNAL_STORAGE}, + PERMISSIONS_EXTERNAL_STORAGE_EXPORT); + } + } + }); + } + + private void startImport() + { + importExporter = new ImportExportTask(ImportExportActivity.this, + true, DataFormat.CSV); + importExporter.execute(); + } + + private void startExport() + { + importExporter = new ImportExportTask(ImportExportActivity.this, + false, DataFormat.CSV); + importExporter.execute(); + } + + @Override + public void onRequestPermissionsResult(int requestCode, String permissions[], int[] grantResults) + { + if(requestCode == PERMISSIONS_EXTERNAL_STORAGE_IMPORT || + requestCode == PERMISSIONS_EXTERNAL_STORAGE_EXPORT) + { + // If request is cancelled, the result arrays are empty. + if (grantResults.length > 0 && + grantResults[0] == PackageManager.PERMISSION_GRANTED) + { + // permission was granted. + if(requestCode == PERMISSIONS_EXTERNAL_STORAGE_IMPORT) + { + startImport(); + } + else + { + startExport(); + } + } + else + { + // External storage permission rejected, inform user that + // import/export is prevented + Toast.makeText(getApplicationContext(), R.string.noExternalStoragePermissionError, + Toast.LENGTH_LONG).show(); + } + + } + } + + @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/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/java/protect/card_locker/MainActivity.java b/app/src/main/java/protect/card_locker/MainActivity.java index b7819033e..72ad9e9c4 100644 --- a/app/src/main/java/protect/card_locker/MainActivity.java +++ b/app/src/main/java/protect/card_locker/MainActivity.java @@ -1,19 +1,29 @@ package protect.card_locker; +import android.content.DialogInterface; import android.content.Intent; +import android.content.pm.PackageInfo; +import android.content.pm.PackageManager; import android.database.Cursor; import android.os.Bundle; +import android.support.v7.app.AlertDialog; import android.support.v7.app.AppCompatActivity; import android.support.v7.widget.Toolbar; +import android.util.Log; import android.view.Menu; import android.view.MenuItem; import android.view.View; +import android.webkit.WebView; import android.widget.AdapterView; import android.widget.ListView; import android.widget.TextView; +import java.util.Calendar; + public class MainActivity extends AppCompatActivity { + private static final String TAG = "LoyaltyCardLocker"; + @Override protected void onCreate(Bundle savedInstanceState) { @@ -92,6 +102,88 @@ 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; + } + + if(id == R.id.action_about) + { + displayAboutDialog(); + return true; + } + return super.onOptionsItemSelected(item); } + + private void displayAboutDialog() + { + final String[][] USED_LIBRARIES = new String[][] + { + new String[] {"Commons CSV", "https://commons.apache.org/proper/commons-csv/"}, + new String[] {"ZXing", "https://github.com/zxing/zxing"}, + new String[] {"ZXing Android Embedded", "https://github.com/journeyapps/zxing-android-embedded"}, + }; + + StringBuilder libs = new StringBuilder().append(""); + + String appName = getString(R.string.app_name); + int year = Calendar.getInstance().get(Calendar.YEAR); + + String version = "?"; + try + { + PackageInfo pi = getPackageManager().getPackageInfo(getPackageName(), 0); + version = pi.versionName; + } + catch (PackageManager.NameNotFoundException e) + { + Log.w(TAG, "Package name not found", e); + } + + WebView wv = new WebView(this); + String html = + "" + + "\""" + + "

" + + String.format(getString(R.string.about_title_fmt), + "" + + appName + + "" + + "

" + + appName + + " " + + String.format(getString(R.string.debug_version_fmt), version) + + "

" + + String.format(getString(R.string.app_revision_fmt), + "" + + getString(R.string.app_revision_url) + + "") + + "


" + + String.format(getString(R.string.app_copyright_fmt), year) + + "


" + + getString(R.string.app_license) + + "


" + + String.format(getString(R.string.app_libraries), appName, libs.toString()); + + wv.loadDataWithBaseURL("file:///android_res/drawable/", html, "text/html", "utf-8", null); + new AlertDialog.Builder(this) + .setView(wv) + .setCancelable(true) + .setPositiveButton(R.string.ok, new DialogInterface.OnClickListener() + { + public void onClick(DialogInterface dialog, int which) + { + dialog.dismiss(); + } + }) + .show(); + } } \ No newline at end of file diff --git a/app/src/main/java/protect/card_locker/MultiFormatExporter.java b/app/src/main/java/protect/card_locker/MultiFormatExporter.java new file mode 100644 index 000000000..68b01e761 --- /dev/null +++ b/app/src/main/java/protect/card_locker/MultiFormatExporter.java @@ -0,0 +1,57 @@ +package protect.card_locker; + +import android.util.Log; + +import java.io.IOException; +import java.io.OutputStreamWriter; + +public class MultiFormatExporter +{ + private static final String TAG = "LoyaltyCardLocker"; + + /** + * Attempts to export data to the output stream in the + * given format, if possible. + * + * The output stream is closed on success. + * + * @return true if the database was successfully exported, + * false otherwise. If false, partial data may have been + * written to the output stream, and it should be discarded. + */ + public static boolean exportData(DBHelper db, OutputStreamWriter output, DataFormat format) + { + DatabaseExporter exporter = null; + + switch(format) + { + case CSV: + exporter = new CsvDatabaseExporter(); + break; + } + + if(exporter != null) + { + try + { + exporter.exportData(db, output); + return true; + } + catch(IOException e) + { + Log.e(TAG, "Failed to export data", e); + } + catch(InterruptedException e) + { + Log.e(TAG, "Failed to export data", e); + } + + return false; + } + else + { + Log.e(TAG, "Unsupported data format exported: " + format.name()); + return false; + } + } +} diff --git a/app/src/main/java/protect/card_locker/MultiFormatImporter.java b/app/src/main/java/protect/card_locker/MultiFormatImporter.java new file mode 100644 index 000000000..3d5593868 --- /dev/null +++ b/app/src/main/java/protect/card_locker/MultiFormatImporter.java @@ -0,0 +1,62 @@ +package protect.card_locker; + +import android.util.Log; + +import java.io.IOException; +import java.io.InputStreamReader; + +public class MultiFormatImporter +{ + private static final String TAG = "LoyaltyCardLocker"; + + /** + * Attempts to import data from the input stream of the + * given format into the database. + * + * 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 + * the database. + */ + public static boolean importData(DBHelper db, InputStreamReader input, DataFormat format) + { + DatabaseImporter importer = null; + + switch(format) + { + case CSV: + importer = new CsvDatabaseImporter(); + break; + } + + if(importer != null) + { + try + { + importer.importData(db, input); + return true; + } + catch(IOException e) + { + Log.e(TAG, "Failed to input data", e); + } + catch(FormatException e) + { + Log.e(TAG, "Failed to input data", e); + } + catch(InterruptedException e) + { + Log.e(TAG, "Failed to input data", e); + } + + return false; + } + else + { + Log.e(TAG, "Unsupported data format imported: " + format.name()); + return false; + } + } +} 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 000000000..5b6c02010 Binary files /dev/null and b/app/src/main/res/drawable-hdpi/ic_import_export_white_24dp.png differ diff --git a/app/src/main/res/drawable-mdpi/ic_import_export_white_24dp.png b/app/src/main/res/drawable-mdpi/ic_import_export_white_24dp.png new file mode 100644 index 000000000..151188cf8 Binary files /dev/null and b/app/src/main/res/drawable-mdpi/ic_import_export_white_24dp.png differ diff --git a/app/src/main/res/drawable-xhdpi/ic_import_export_white_24dp.png b/app/src/main/res/drawable-xhdpi/ic_import_export_white_24dp.png new file mode 100644 index 000000000..e22e18866 Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/ic_import_export_white_24dp.png differ diff --git a/app/src/main/res/drawable-xxhdpi/ic_import_export_white_24dp.png b/app/src/main/res/drawable-xxhdpi/ic_import_export_white_24dp.png new file mode 100644 index 000000000..33c21c5c4 Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/ic_import_export_white_24dp.png differ 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 000000000..a5e55a470 Binary files /dev/null and b/app/src/main/res/drawable-xxxhdpi/ic_import_export_white_24dp.png differ 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 @@ + + + + + + + + + + + + + +