From eeb41376c5052993ae38be087586e77966ac9334 Mon Sep 17 00:00:00 2001 From: Branden Archer Date: Mon, 16 Jan 2017 22:05:14 -0500 Subject: [PATCH] Add ability to select file to import To date the only way to import a file is to hope the exported file is in the correct location. To remove this requirement, this change checks if there are any activities available on the system which can look in the file system. If there are, new options for importing data will be available. In addition, the import/export activity's layout was updated, adding more explanation. Translation contributions: Clonewayx: cs PanderMusubi: nl pbeckmann: de Airon90: it arno-github : fr --- .../card_locker/ImportExportActivity.java | 303 +++++++++++++++--- .../protect/card_locker/ImportExportTask.java | 64 ++-- .../res/layout/import_export_activity.xml | 163 ++++++++-- app/src/main/res/values-cs/strings.xml | 17 +- app/src/main/res/values-de/strings.xml | 17 +- app/src/main/res/values-fr/strings.xml | 16 +- app/src/main/res/values-it/strings.xml | 16 +- app/src/main/res/values-lt/strings.xml | 16 +- app/src/main/res/values-nl/strings.xml | 17 +- app/src/main/res/values/dimens.xml | 4 + app/src/main/res/values/strings.xml | 16 +- .../card_locker/ImportExportActivityTest.java | 154 +++++++++ .../protect/card_locker/ImportExportTest.java | 40 ++- 13 files changed, 722 insertions(+), 121 deletions(-) create mode 100644 app/src/test/java/protect/card_locker/ImportExportActivityTest.java diff --git a/app/src/main/java/protect/card_locker/ImportExportActivity.java b/app/src/main/java/protect/card_locker/ImportExportActivity.java index 95d9750ab..95829b1c9 100644 --- a/app/src/main/java/protect/card_locker/ImportExportActivity.java +++ b/app/src/main/java/protect/card_locker/ImportExportActivity.java @@ -1,25 +1,42 @@ package protect.card_locker; import android.Manifest; +import android.content.ActivityNotFoundException; +import android.content.Context; +import android.content.DialogInterface; +import android.content.Intent; import android.content.pm.PackageManager; +import android.content.pm.ResolveInfo; +import android.net.Uri; import android.os.AsyncTask; import android.os.Bundle; +import android.os.Environment; import android.support.v4.app.ActivityCompat; import android.support.v4.content.ContextCompat; import android.support.v7.app.ActionBar; +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.MenuItem; import android.view.View; import android.widget.Button; import android.widget.Toast; +import java.io.File; +import java.util.List; + public class ImportExportActivity extends AppCompatActivity { - private static final int PERMISSIONS_EXTERNAL_STORAGE_IMPORT = 1; - private static final int PERMISSIONS_EXTERNAL_STORAGE_EXPORT = 2; + private static final String TAG = "LoyaltyCardLocker"; - ImportExportTask importExporter; + private static final int PERMISSIONS_EXTERNAL_STORAGE = 1; + private static final int CHOOSE_EXPORT_FILE = 2; + + private ImportExportTask importExporter; + + private final File sdcardDir = Environment.getExternalStorageDirectory(); + private final File exportFile = new File(sdcardDir, "LoyaltyCardLocker.csv"); @Override protected void onCreate(Bundle savedInstanceState) @@ -34,25 +51,20 @@ public class ImportExportActivity extends AppCompatActivity actionBar.setDisplayHomeAsUpEnabled(true); } - Button importButton = (Button)findViewById(R.id.importButton); - importButton.setOnClickListener(new View.OnClickListener() + // If the application does not have permissions to external + // storage, as for it now + + if (ContextCompat.checkSelfPermission(ImportExportActivity.this, + Manifest.permission.READ_EXTERNAL_STORAGE) != PackageManager.PERMISSION_GRANTED || + ContextCompat.checkSelfPermission(ImportExportActivity.this, + Manifest.permission.WRITE_EXTERNAL_STORAGE) != PackageManager.PERMISSION_GRANTED) { - @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); - } - } - }); + ActivityCompat.requestPermissions(ImportExportActivity.this, + new String[]{Manifest.permission.READ_EXTERNAL_STORAGE, + Manifest.permission.WRITE_EXTERNAL_STORAGE}, + PERMISSIONS_EXTERNAL_STORAGE); + } + Button exportButton = (Button)findViewById(R.id.exportButton); exportButton.setOnClickListener(new View.OnClickListener() @@ -60,56 +72,120 @@ public class ImportExportActivity extends AppCompatActivity @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); - } + startExport(); + } + }); + + + // Check that there is an activity that can bring up a file chooser + final Intent intentPickAction = new Intent(Intent.ACTION_PICK); + intentPickAction.setData(Uri.parse("file://")); + + Button importFilesystem = (Button) findViewById(R.id.importOptionFilesystemButton); + importFilesystem.setOnClickListener(new View.OnClickListener() + { + @Override + public void onClick(View v) + { + chooseFileWithIntent(intentPickAction); + } + }); + + if(isCallable(getApplicationContext(), intentPickAction) == false) + { + findViewById(R.id.dividerImportFilesystem).setVisibility(View.GONE); + findViewById(R.id.importOptionFilesystemTitle).setVisibility(View.GONE); + findViewById(R.id.importOptionFilesystemExplanation).setVisibility(View.GONE); + importFilesystem.setVisibility(View.GONE); + } + + + // Check that there is an application that can find content + final Intent intentGetContentAction = new Intent(Intent.ACTION_GET_CONTENT); + intentGetContentAction.addCategory(Intent.CATEGORY_OPENABLE); + intentGetContentAction.setType("*/*"); + + Button importApplication = (Button) findViewById(R.id.importOptionApplicationButton); + importApplication.setOnClickListener(new View.OnClickListener() + { + @Override + public void onClick(View v) + { + chooseFileWithIntent(intentGetContentAction); + } + }); + + if(isCallable(getApplicationContext(), intentGetContentAction) == false) + { + findViewById(R.id.dividerImportApplication).setVisibility(View.GONE); + findViewById(R.id.importOptionApplicationTitle).setVisibility(View.GONE); + findViewById(R.id.importOptionApplicationExplanation).setVisibility(View.GONE); + importApplication.setVisibility(View.GONE); + } + + + // This option, to import from the fixed location, should always be present + + Button importButton = (Button)findViewById(R.id.importOptionFixedButton); + importButton.setOnClickListener(new View.OnClickListener() + { + @Override + public void onClick(View v) + { + startImport(exportFile); } }); } - private void startImport() + private void startImport(File target) { + ImportExportTask.TaskCompleteListener listener = new ImportExportTask.TaskCompleteListener() + { + @Override + public void onTaskComplete(boolean success, File file) + { + onImportComplete(success, file); + } + }; + importExporter = new ImportExportTask(ImportExportActivity.this, - true, DataFormat.CSV); + true, DataFormat.CSV, target, listener); importExporter.execute(); } private void startExport() { + ImportExportTask.TaskCompleteListener listener = new ImportExportTask.TaskCompleteListener() + { + @Override + public void onTaskComplete(boolean success, File file) + { + onExportComplete(success, file); + } + }; + importExporter = new ImportExportTask(ImportExportActivity.this, - false, DataFormat.CSV); + false, DataFormat.CSV, exportFile, listener); importExporter.execute(); } @Override public void onRequestPermissionsResult(int requestCode, String permissions[], int[] grantResults) { - if(requestCode == PERMISSIONS_EXTERNAL_STORAGE_IMPORT || - requestCode == PERMISSIONS_EXTERNAL_STORAGE_EXPORT) + if(requestCode == PERMISSIONS_EXTERNAL_STORAGE) { // If request is cancelled, the result arrays are empty. - if (grantResults.length > 0 && - grantResults[0] == PackageManager.PERMISSION_GRANTED) + boolean success = grantResults.length > 0; + + for(int grant : grantResults) { - // permission was granted. - if(requestCode == PERMISSIONS_EXTERNAL_STORAGE_IMPORT) + if(grant != PackageManager.PERMISSION_GRANTED) { - startImport(); - } - else - { - startExport(); + success = false; } } - else + + if(success == false) { // External storage permission rejected, inform user that // import/export is prevented @@ -143,4 +219,137 @@ public class ImportExportActivity extends AppCompatActivity return super.onOptionsItemSelected(item); } + + private void onImportComplete(boolean success, File path) + { + AlertDialog.Builder builder = new AlertDialog.Builder(this); + + if(success) + { + builder.setTitle(R.string.importSuccessfulTitle); + } + else + { + builder.setTitle(R.string.importFailedTitle); + } + + int messageId = success ? R.string.importedFrom : R.string.importFailed; + + final String template = getResources().getString(messageId); + final String message = String.format(template, path.getAbsolutePath()); + builder.setMessage(message); + builder.setNeutralButton(R.string.ok, new DialogInterface.OnClickListener() + { + @Override + public void onClick(DialogInterface dialog, int which) + { + dialog.dismiss(); + } + }); + + builder.create().show(); + } + + private void onExportComplete(boolean success, File path) + { + AlertDialog.Builder builder = new AlertDialog.Builder(this); + + if(success) + { + builder.setTitle(R.string.exportSuccessfulTitle); + } + else + { + builder.setTitle(R.string.exportFailedTitle); + } + + int messageId = success ? R.string.exportedTo : R.string.exportFailed; + + final String template = getResources().getString(messageId); + final String message = String.format(template, path.getAbsolutePath()); + builder.setMessage(message); + builder.setNeutralButton(R.string.ok, new DialogInterface.OnClickListener() + { + @Override + public void onClick(DialogInterface dialog, int which) + { + dialog.dismiss(); + } + }); + + builder.create().show(); + } + + /** + * Determines if there is at least one activity that can perform the given intent + */ + private boolean isCallable(Context context, final Intent intent) + { + PackageManager manager = context.getPackageManager(); + if(manager == null) + { + return false; + } + + List list = manager.queryIntentActivities(intent, PackageManager.MATCH_DEFAULT_ONLY); + + for(ResolveInfo info : list) + { + if(info.activityInfo.exported) + { + // There is one activity which is available to be called + return true; + } + } + + return false; + } + + private void chooseFileWithIntent(Intent intent) + { + try + { + startActivityForResult(intent, CHOOSE_EXPORT_FILE); + } + catch (ActivityNotFoundException e) + { + Log.e(TAG, "No activity found to handle intent", e); + } + } + + @Override + protected void onActivityResult(int requestCode, int resultCode, Intent data) + { + 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 + { + Log.w(TAG, "Failed onActivityResult(), result=" + resultCode); + } + } } \ 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 ee0edab72..a883d9761 100644 --- a/app/src/main/java/protect/card_locker/ImportExportTask.java +++ b/app/src/main/java/protect/card_locker/ImportExportTask.java @@ -16,49 +16,31 @@ import java.io.InputStreamReader; import java.io.OutputStreamWriter; import java.nio.charset.Charset; -class ImportExportTask extends AsyncTask +class ImportExportTask extends AsyncTask { private static final String TAG = "LoyaltyCardLocker"; - private static final String TARGET_FILE = "LoyaltyCardLocker.csv"; - private Activity activity; private boolean doImport; private DataFormat format; + private File target; + private TaskCompleteListener listener; private ProgressDialog progress; - public ImportExportTask(Activity activity, boolean doImport, DataFormat format) + public ImportExportTask(Activity activity, boolean doImport, DataFormat format, File target, + TaskCompleteListener listener) { super(); this.activity = activity; this.doImport = doImport; this.format = format; + this.target = target; + this.listener = listener; } - private void toastWithArg(int stringId, String argument) + private boolean performImport(File importFile, DBHelper db) { - 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 @@ -73,11 +55,12 @@ class ImportExportTask extends AsyncTask Log.e(TAG, "Unable to import file", e); } - int messageId = result ? R.string.importedFrom : R.string.importFailed; - toastWithArg(messageId, importFile.getAbsolutePath()); + Log.i(TAG, "Import of '" + importFile.getAbsolutePath() + "' result: " + result); + + return result; } - private void performExport(File exportFile, DBHelper db) + private boolean performExport(File exportFile, DBHelper db) { boolean result = false; @@ -93,8 +76,7 @@ class ImportExportTask extends AsyncTask Log.e(TAG, "Unable to export file", e); } - int messageId = result ? R.string.exportedTo : R.string.exportFailed; - toastWithArg(messageId, exportFile.getAbsolutePath()); + return result; } protected void onPreExecute() @@ -114,26 +96,27 @@ class ImportExportTask extends AsyncTask progress.show(); } - protected Void doInBackground(Void... nothing) + protected Boolean doInBackground(Void... nothing) { - final File sdcardDir = Environment.getExternalStorageDirectory(); - final File importExportFile = new File(sdcardDir, TARGET_FILE); final DBHelper db = new DBHelper(activity); + boolean result; if(doImport) { - performImport(importExportFile, db); + result = performImport(target, db); } else { - performExport(importExportFile, db); + result = performExport(target, db); } - return null; + return result; } - protected void onPostExecute(Void result) + protected void onPostExecute(Boolean result) { + listener.onTaskComplete(result, target); + progress.dismiss(); Log.i(TAG, (doImport ? "Import" : "Export") + " Complete"); } @@ -143,4 +126,9 @@ class ImportExportTask extends AsyncTask progress.dismiss(); Log.i(TAG, (doImport ? "Import" : "Export") + " Cancelled"); } + + interface TaskCompleteListener + { + void onTaskComplete(boolean success, File file); + } } diff --git a/app/src/main/res/layout/import_export_activity.xml b/app/src/main/res/layout/import_export_activity.xml index 3291f87f3..64a3789ea 100644 --- a/app/src/main/res/layout/import_export_activity.xml +++ b/app/src/main/res/layout/import_export_activity.xml @@ -19,36 +19,149 @@ - - + - -