From 2ebc862e275fccc9065a88a9a46fae02401c3fc7 Mon Sep 17 00:00:00 2001 From: Branden Archer Date: Tue, 21 Nov 2017 16:38:54 -0500 Subject: [PATCH 1/5] Remove file arg from TaskCompleteListener Soon more than files will be imported, as content URIs will also be supported. This then makes the File argument for onTaskComplete() not always useful, as there may not be a direct file used. To this end, removing the File argument as the caller should know what was passed to the ImportExport task anyway. --- .../java/protect/card_locker/ImportExportActivity.java | 8 ++++---- .../main/java/protect/card_locker/ImportExportTask.java | 4 ++-- .../test/java/protect/card_locker/ImportExportTest.java | 8 +------- 3 files changed, 7 insertions(+), 13 deletions(-) diff --git a/app/src/main/java/protect/card_locker/ImportExportActivity.java b/app/src/main/java/protect/card_locker/ImportExportActivity.java index 76399a2ee..1e56b7a10 100644 --- a/app/src/main/java/protect/card_locker/ImportExportActivity.java +++ b/app/src/main/java/protect/card_locker/ImportExportActivity.java @@ -142,9 +142,9 @@ public class ImportExportActivity extends AppCompatActivity ImportExportTask.TaskCompleteListener listener = new ImportExportTask.TaskCompleteListener() { @Override - public void onTaskComplete(boolean success, File file) + public void onTaskComplete(boolean success) { - onImportComplete(success, file); + onImportComplete(success, exportFile); } }; @@ -158,9 +158,9 @@ public class ImportExportActivity extends AppCompatActivity ImportExportTask.TaskCompleteListener listener = new ImportExportTask.TaskCompleteListener() { @Override - public void onTaskComplete(boolean success, File file) + public void onTaskComplete(boolean success) { - onExportComplete(success, file); + onExportComplete(success, exportFile); } }; diff --git a/app/src/main/java/protect/card_locker/ImportExportTask.java b/app/src/main/java/protect/card_locker/ImportExportTask.java index 5aceacb96..c3d5f2944 100644 --- a/app/src/main/java/protect/card_locker/ImportExportTask.java +++ b/app/src/main/java/protect/card_locker/ImportExportTask.java @@ -117,7 +117,7 @@ class ImportExportTask extends AsyncTask protected void onPostExecute(Boolean result) { - listener.onTaskComplete(result, target); + listener.onTaskComplete(result); progress.dismiss(); Log.i(TAG, (doImport ? "Import" : "Export") + " Complete"); @@ -130,7 +130,7 @@ class ImportExportTask extends AsyncTask } interface TaskCompleteListener { - void onTaskComplete(boolean success, File file); + void onTaskComplete(boolean success); } } diff --git a/app/src/test/java/protect/card_locker/ImportExportTest.java b/app/src/test/java/protect/card_locker/ImportExportTest.java index 312b2439c..840278629 100644 --- a/app/src/test/java/protect/card_locker/ImportExportTest.java +++ b/app/src/test/java/protect/card_locker/ImportExportTest.java @@ -211,12 +211,10 @@ public class ImportExportTest class TestTaskCompleteListener implements ImportExportTask.TaskCompleteListener { Boolean success; - File file; - public void onTaskComplete(boolean success, File file) + public void onTaskComplete(boolean success) { this.success = success; - this.file = file; } } @@ -244,8 +242,6 @@ public class ImportExportTest // Check that the listener was executed assertNotNull(listener.success); assertEquals(true, listener.success); - assertNotNull(listener.file); - assertEquals(exportFile, listener.file); clearDatabase(); @@ -262,8 +258,6 @@ public class ImportExportTest // Check that the listener was executed assertNotNull(listener.success); assertEquals(true, listener.success); - assertNotNull(listener.file); - assertEquals(exportFile, listener.file); assertEquals(NUM_CARDS, db.getLoyaltyCardCount()); From 91e3f9f78541562169e6ffcfe49d2e84455324a0 Mon Sep 17 00:00:00 2001 From: Branden Archer Date: Tue, 21 Nov 2017 18:15:56 -0500 Subject: [PATCH 2/5] Support importing content URIs and file URIs When importing backed up settings other activities may provide data via a content URI. This is especially likely on Android 7+, where providing a file URI is flagged as a security issue. To support such activities, this commit enables supporting content URIs for importing settings. --- .../card_locker/ImportExportActivity.java | 53 +++++++++++-------- .../protect/card_locker/ImportExportTask.java | 32 ++++++++--- .../protect/card_locker/ImportExportTest.java | 10 ++-- 3 files changed, 64 insertions(+), 31 deletions(-) diff --git a/app/src/main/java/protect/card_locker/ImportExportActivity.java b/app/src/main/java/protect/card_locker/ImportExportActivity.java index 1e56b7a10..b60d5935c 100644 --- a/app/src/main/java/protect/card_locker/ImportExportActivity.java +++ b/app/src/main/java/protect/card_locker/ImportExportActivity.java @@ -24,6 +24,10 @@ import android.widget.Button; import android.widget.Toast; import java.io.File; +import java.io.FileInputStream; +import java.io.FileNotFoundException; +import java.io.InputStream; +import java.io.InputStreamReader; import java.util.List; public class ImportExportActivity extends AppCompatActivity @@ -132,24 +136,34 @@ public class ImportExportActivity extends AppCompatActivity @Override public void onClick(View v) { - startImport(exportFile); + String fileUri = exportFile.toURI().toString(); + try + { + FileInputStream stream = new FileInputStream(exportFile); + startImport(stream, fileUri); + } + catch(FileNotFoundException e) + { + Log.e(TAG, "Could not import file " + exportFile.getAbsolutePath(), e); + onImportComplete(false, fileUri); + } } }); } - private void startImport(File target) + private void startImport(final InputStream target, final String targetUri) { ImportExportTask.TaskCompleteListener listener = new ImportExportTask.TaskCompleteListener() { @Override public void onTaskComplete(boolean success) { - onImportComplete(success, exportFile); + onImportComplete(success, targetUri); } }; importExporter = new ImportExportTask(ImportExportActivity.this, - true, DataFormat.CSV, target, listener); + DataFormat.CSV, target, listener); importExporter.execute(); } @@ -165,7 +179,7 @@ public class ImportExportActivity extends AppCompatActivity }; importExporter = new ImportExportTask(ImportExportActivity.this, - false, DataFormat.CSV, exportFile, listener); + DataFormat.CSV, exportFile, listener); importExporter.execute(); } @@ -220,7 +234,7 @@ public class ImportExportActivity extends AppCompatActivity return super.onOptionsItemSelected(item); } - private void onImportComplete(boolean success, File path) + private void onImportComplete(boolean success, String path) { AlertDialog.Builder builder = new AlertDialog.Builder(this); @@ -236,7 +250,7 @@ public class ImportExportActivity extends AppCompatActivity int messageId = success ? R.string.importedFrom : R.string.importFailed; final String template = getResources().getString(messageId); - final String message = String.format(template, path.getAbsolutePath()); + final String message = String.format(template, path); builder.setMessage(message); builder.setNeutralButton(R.string.ok, new DialogInterface.OnClickListener() { @@ -346,27 +360,24 @@ public class ImportExportActivity extends AppCompatActivity if (resultCode == RESULT_OK && requestCode == CHOOSE_EXPORT_FILE) { - String path = null; - Uri uri = data.getData(); - if(uri != null && uri.toString().startsWith("/")) - { - uri = Uri.parse("file://" + uri.toString()); - } if(uri != null) { - path = uri.getPath(); - } - - if(path != null) - { - Log.e(TAG, "Starting file import with: " + uri.toString()); - startImport(new File(path)); + try + { + InputStream reader = getContentResolver().openInputStream(uri); + Log.e(TAG, "Starting file import with: " + uri.toString()); + startImport(reader, uri.toString()); + } + catch (FileNotFoundException e) + { + Log.e(TAG, "Failed to import file: " + uri.toString(), e); + } } else { - Log.e(TAG, "Fail to make sense of URI returned from activity: " + (uri != null ? uri.toString() : "null")); + Log.e(TAG, "Activity returned a NULL URI"); } } else diff --git a/app/src/main/java/protect/card_locker/ImportExportTask.java b/app/src/main/java/protect/card_locker/ImportExportTask.java index c3d5f2944..839e7c36d 100644 --- a/app/src/main/java/protect/card_locker/ImportExportTask.java +++ b/app/src/main/java/protect/card_locker/ImportExportTask.java @@ -12,6 +12,7 @@ import java.io.File; import java.io.FileInputStream; import java.io.FileOutputStream; import java.io.IOException; +import java.io.InputStream; import java.io.InputStreamReader; import java.io.OutputStreamWriter; import java.nio.charset.Charset; @@ -24,29 +25,46 @@ class ImportExportTask extends AsyncTask private boolean doImport; private DataFormat format; private File target; + private InputStream inputStream; private TaskCompleteListener listener; private ProgressDialog progress; - public ImportExportTask(Activity activity, boolean doImport, DataFormat format, File target, + /** + * Constructor which will setup a task for exporting to the given file + */ + ImportExportTask(Activity activity, DataFormat format, File target, TaskCompleteListener listener) { super(); this.activity = activity; - this.doImport = doImport; + this.doImport = false; this.format = format; this.target = target; this.listener = listener; } - private boolean performImport(File importFile, DBHelper db) + /** + * Constructor which will setup a task for importing from the given InputStream. + */ + ImportExportTask(Activity activity, DataFormat format, InputStream input, + TaskCompleteListener listener) + { + super(); + this.activity = activity; + this.doImport = true; + this.format = format; + this.inputStream = input; + this.listener = listener; + } + + private boolean performImport(InputStream stream, DBHelper db) { boolean result = false; try { - FileInputStream fileReader = new FileInputStream(importFile); - InputStreamReader reader = new InputStreamReader(fileReader, Charset.forName("UTF-8")); + InputStreamReader reader = new InputStreamReader(stream, Charset.forName("UTF-8")); result = MultiFormatImporter.importData(db, reader, format); reader.close(); } @@ -55,7 +73,7 @@ class ImportExportTask extends AsyncTask Log.e(TAG, "Unable to import file", e); } - Log.i(TAG, "Import of '" + importFile.getAbsolutePath() + "' result: " + result); + Log.i(TAG, "Import result: " + result); return result; } @@ -105,7 +123,7 @@ class ImportExportTask extends AsyncTask if(doImport) { - result = performImport(target, db); + result = performImport(inputStream, db); } else { diff --git a/app/src/test/java/protect/card_locker/ImportExportTest.java b/app/src/test/java/protect/card_locker/ImportExportTest.java index 840278629..7f96e5519 100644 --- a/app/src/test/java/protect/card_locker/ImportExportTest.java +++ b/app/src/test/java/protect/card_locker/ImportExportTest.java @@ -17,6 +17,8 @@ import org.robolectric.annotation.Config; import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; import java.io.File; +import java.io.FileInputStream; +import java.io.FileNotFoundException; import java.io.IOException; import java.io.InputStreamReader; import java.io.OutputStreamWriter; @@ -219,7 +221,7 @@ public class ImportExportTest } @Test - public void useImportExportTask() + public void useImportExportTask() throws FileNotFoundException { final int NUM_CARDS = 10; @@ -233,7 +235,7 @@ public class ImportExportTest TestTaskCompleteListener listener = new TestTaskCompleteListener(); // Export to the file - ImportExportTask task = new ImportExportTask(activity, false, format, exportFile, listener); + ImportExportTask task = new ImportExportTask(activity, format, exportFile, listener); task.execute(); // Actually run the task to completion @@ -249,7 +251,9 @@ public class ImportExportTest listener = new TestTaskCompleteListener(); - task = new ImportExportTask(activity, true, format, exportFile, listener); + FileInputStream fileStream = new FileInputStream(exportFile); + + task = new ImportExportTask(activity, format, fileStream, listener); task.execute(); // Actually run the task to completion From 04e0a5716e88798fa215956fce3ee1a277f334da Mon Sep 17 00:00:00 2001 From: Branden Archer Date: Tue, 21 Nov 2017 21:12:21 -0500 Subject: [PATCH 3/5] Flatten error handling when importing from an activity --- .../card_locker/ImportExportActivity.java | 43 +++++++++---------- 1 file changed, 20 insertions(+), 23 deletions(-) diff --git a/app/src/main/java/protect/card_locker/ImportExportActivity.java b/app/src/main/java/protect/card_locker/ImportExportActivity.java index b60d5935c..7176a9cb2 100644 --- a/app/src/main/java/protect/card_locker/ImportExportActivity.java +++ b/app/src/main/java/protect/card_locker/ImportExportActivity.java @@ -358,31 +358,28 @@ public class ImportExportActivity extends AppCompatActivity { super.onActivityResult(requestCode, resultCode, data); - if (resultCode == RESULT_OK && requestCode == CHOOSE_EXPORT_FILE) - { - Uri uri = data.getData(); - - if(uri != null) - { - try - { - InputStream reader = getContentResolver().openInputStream(uri); - Log.e(TAG, "Starting file import with: " + uri.toString()); - startImport(reader, uri.toString()); - } - catch (FileNotFoundException e) - { - Log.e(TAG, "Failed to import file: " + uri.toString(), e); - } - } - else - { - Log.e(TAG, "Activity returned a NULL URI"); - } - } - else + if (resultCode != RESULT_OK || requestCode != CHOOSE_EXPORT_FILE) { Log.w(TAG, "Failed onActivityResult(), result=" + resultCode); + return; + } + + Uri uri = data.getData(); + if(uri == null) + { + Log.e(TAG, "Activity returned a NULL URI"); + return; + } + + try + { + InputStream reader = getContentResolver().openInputStream(uri); + Log.e(TAG, "Starting file import with: " + uri.toString()); + startImport(reader, uri.toString()); + } + catch (FileNotFoundException e) + { + Log.e(TAG, "Failed to import file: " + uri.toString(), e); } } } \ No newline at end of file From 03a53349619864d34e319674ece651a1ae9ea16e Mon Sep 17 00:00:00 2001 From: Branden Archer Date: Tue, 21 Nov 2017 21:14:47 -0500 Subject: [PATCH 4/5] Export backups using FileProvider On Android 7+ providing another activity a file:// Uri is discouraged. This changes instead uses a content provider backed by a FileProvider to give backup data to other activities. --- app/src/main/AndroidManifest.xml | 9 +++++++++ .../protect/card_locker/ImportExportActivity.java | 14 +++++++++----- app/src/main/res/xml/file_provider_paths.xml | 3 +++ 3 files changed, 21 insertions(+), 5 deletions(-) create mode 100644 app/src/main/res/xml/file_provider_paths.xml diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index f0d083c60..a93a40cf7 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -68,6 +68,15 @@ + + + diff --git a/app/src/main/java/protect/card_locker/ImportExportActivity.java b/app/src/main/java/protect/card_locker/ImportExportActivity.java index 7176a9cb2..32677f53c 100644 --- a/app/src/main/java/protect/card_locker/ImportExportActivity.java +++ b/app/src/main/java/protect/card_locker/ImportExportActivity.java @@ -13,6 +13,7 @@ import android.os.Bundle; import android.os.Environment; import android.support.v4.app.ActivityCompat; import android.support.v4.content.ContextCompat; +import android.support.v4.content.FileProvider; import android.support.v7.app.ActionBar; import android.support.v7.app.AlertDialog; import android.support.v7.app.AppCompatActivity; @@ -136,22 +137,22 @@ public class ImportExportActivity extends AppCompatActivity @Override public void onClick(View v) { - String fileUri = exportFile.toURI().toString(); + Uri uri = Uri.fromFile(exportFile); try { FileInputStream stream = new FileInputStream(exportFile); - startImport(stream, fileUri); + startImport(stream, uri); } catch(FileNotFoundException e) { Log.e(TAG, "Could not import file " + exportFile.getAbsolutePath(), e); - onImportComplete(false, fileUri); + onImportComplete(false, uri); } } }); } - private void startImport(final InputStream target, final String targetUri) + private void startImport(final InputStream target, final Uri targetUri) { ImportExportTask.TaskCompleteListener listener = new ImportExportTask.TaskCompleteListener() { @@ -300,11 +301,14 @@ public class ImportExportActivity extends AppCompatActivity @Override public void onClick(DialogInterface dialog, int which) { - Uri outputUri = Uri.fromFile(path); + Uri outputUri = FileProvider.getUriForFile(ImportExportActivity.this, BuildConfig.APPLICATION_ID, path); Intent sendIntent = new Intent(Intent.ACTION_SEND); sendIntent.putExtra(Intent.EXTRA_STREAM, outputUri); sendIntent.setType("text/plain"); + // set flag to give temporary permission to external app to use the FileProvider + sendIntent.setFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION); + ImportExportActivity.this.startActivity(Intent.createChooser(sendIntent, sendLabel)); diff --git a/app/src/main/res/xml/file_provider_paths.xml b/app/src/main/res/xml/file_provider_paths.xml new file mode 100644 index 000000000..8bacf6557 --- /dev/null +++ b/app/src/main/res/xml/file_provider_paths.xml @@ -0,0 +1,3 @@ + + + \ No newline at end of file From 61443530792029a61ee7bd7d16d57f5bf13af487 Mon Sep 17 00:00:00 2001 From: Branden Archer Date: Tue, 21 Nov 2017 21:15:42 -0500 Subject: [PATCH 5/5] List correct file name on imports --- .../card_locker/ImportExportActivity.java | 43 +++++++++++++++++-- 1 file changed, 40 insertions(+), 3 deletions(-) diff --git a/app/src/main/java/protect/card_locker/ImportExportActivity.java b/app/src/main/java/protect/card_locker/ImportExportActivity.java index 32677f53c..ee230b49e 100644 --- a/app/src/main/java/protect/card_locker/ImportExportActivity.java +++ b/app/src/main/java/protect/card_locker/ImportExportActivity.java @@ -7,10 +7,12 @@ import android.content.DialogInterface; import android.content.Intent; import android.content.pm.PackageManager; import android.content.pm.ResolveInfo; +import android.database.Cursor; import android.net.Uri; import android.os.AsyncTask; import android.os.Bundle; import android.os.Environment; +import android.provider.OpenableColumns; import android.support.v4.app.ActivityCompat; import android.support.v4.content.ContextCompat; import android.support.v4.content.FileProvider; @@ -235,7 +237,34 @@ public class ImportExportActivity extends AppCompatActivity return super.onOptionsItemSelected(item); } - private void onImportComplete(boolean success, String path) + private String fileNameFromUri(Uri uri) + { + if("file".equals(uri.getScheme())) + { + return uri.getPath(); + } + + Cursor returnCursor = + getContentResolver().query(uri, null, null, null, null); + if(returnCursor == null) + { + return null; + } + + int nameIndex = returnCursor.getColumnIndex(OpenableColumns.DISPLAY_NAME); + if(returnCursor.moveToFirst() == false) + { + returnCursor.close(); + return null; + } + + String name = returnCursor.getString(nameIndex); + returnCursor.close(); + + return name; + } + + private void onImportComplete(boolean success, Uri path) { AlertDialog.Builder builder = new AlertDialog.Builder(this); @@ -251,7 +280,15 @@ public class ImportExportActivity extends AppCompatActivity int messageId = success ? R.string.importedFrom : R.string.importFailed; final String template = getResources().getString(messageId); - final String message = String.format(template, path); + + // Get the filename of the file being imported + String filename = fileNameFromUri(path); + if(filename == null) + { + filename = "(unknown)"; + } + + final String message = String.format(template, filename); builder.setMessage(message); builder.setNeutralButton(R.string.ok, new DialogInterface.OnClickListener() { @@ -379,7 +416,7 @@ public class ImportExportActivity extends AppCompatActivity { InputStream reader = getContentResolver().openInputStream(uri); Log.e(TAG, "Starting file import with: " + uri.toString()); - startImport(reader, uri.toString()); + startImport(reader, uri); } catch (FileNotFoundException e) {