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 76399a2ee..ee230b49e 100644 --- a/app/src/main/java/protect/card_locker/ImportExportActivity.java +++ b/app/src/main/java/protect/card_locker/ImportExportActivity.java @@ -7,12 +7,15 @@ 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; import android.support.v7.app.ActionBar; import android.support.v7.app.AlertDialog; import android.support.v7.app.AppCompatActivity; @@ -24,6 +27,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 +139,34 @@ public class ImportExportActivity extends AppCompatActivity @Override public void onClick(View v) { - startImport(exportFile); + Uri uri = Uri.fromFile(exportFile); + try + { + FileInputStream stream = new FileInputStream(exportFile); + startImport(stream, uri); + } + catch(FileNotFoundException e) + { + Log.e(TAG, "Could not import file " + exportFile.getAbsolutePath(), e); + onImportComplete(false, uri); + } } }); } - private void startImport(File target) + private void startImport(final InputStream target, final Uri targetUri) { ImportExportTask.TaskCompleteListener listener = new ImportExportTask.TaskCompleteListener() { @Override - public void onTaskComplete(boolean success, File file) + public void onTaskComplete(boolean success) { - onImportComplete(success, file); + onImportComplete(success, targetUri); } }; importExporter = new ImportExportTask(ImportExportActivity.this, - true, DataFormat.CSV, target, listener); + DataFormat.CSV, target, listener); importExporter.execute(); } @@ -158,14 +175,14 @@ 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); } }; importExporter = new ImportExportTask(ImportExportActivity.this, - false, DataFormat.CSV, exportFile, listener); + DataFormat.CSV, exportFile, listener); importExporter.execute(); } @@ -220,7 +237,34 @@ public class ImportExportActivity extends AppCompatActivity return super.onOptionsItemSelected(item); } - private void onImportComplete(boolean success, File 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); @@ -236,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.getAbsolutePath()); + + // 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() { @@ -286,11 +338,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)); @@ -344,34 +399,28 @@ public class ImportExportActivity extends AppCompatActivity { super.onActivityResult(requestCode, resultCode, data); - 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)); - } - else - { - Log.e(TAG, "Fail to make sense of URI returned from activity: " + (uri != null ? uri.toString() : "null")); - } - } - 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); + } + catch (FileNotFoundException e) + { + Log.e(TAG, "Failed to import file: " + uri.toString(), e); } } } \ 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 index 5aceacb96..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 { @@ -117,7 +135,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 +148,7 @@ class ImportExportTask extends AsyncTask } interface TaskCompleteListener { - void onTaskComplete(boolean success, File file); + void onTaskComplete(boolean success); } } 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 diff --git a/app/src/test/java/protect/card_locker/ImportExportTest.java b/app/src/test/java/protect/card_locker/ImportExportTest.java index 312b2439c..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; @@ -211,17 +213,15 @@ 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; } } @Test - public void useImportExportTask() + public void useImportExportTask() throws FileNotFoundException { final int NUM_CARDS = 10; @@ -235,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 @@ -244,8 +244,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(); @@ -253,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 @@ -262,8 +262,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());