diff --git a/app/src/main/java/protect/card_locker/ImportExportActivity.java b/app/src/main/java/protect/card_locker/ImportExportActivity.java deleted file mode 100644 index aeef6d0f5..000000000 --- a/app/src/main/java/protect/card_locker/ImportExportActivity.java +++ /dev/null @@ -1,395 +0,0 @@ -package protect.card_locker; - -import android.content.ActivityNotFoundException; -import android.content.DialogInterface; -import android.content.Intent; -import android.net.Uri; -import android.os.Bundle; -import android.text.InputType; -import android.util.Log; -import android.view.MenuItem; -import android.view.ViewGroup; -import android.view.WindowManager; -import android.widget.Button; -import android.widget.EditText; -import android.widget.FrameLayout; -import android.widget.LinearLayout; -import android.widget.Toast; - -import androidx.activity.result.ActivityResultLauncher; -import androidx.activity.result.contract.ActivityResultContracts; -import androidx.annotation.Nullable; -import androidx.appcompat.app.AlertDialog; -import androidx.appcompat.widget.Toolbar; - -import com.google.android.material.dialog.MaterialAlertDialogBuilder; -import com.google.android.material.textfield.TextInputLayout; - -import java.io.IOException; -import java.io.InputStream; -import java.io.OutputStream; -import java.util.ArrayList; -import java.util.List; - -import protect.card_locker.async.TaskHandler; -import protect.card_locker.databinding.ImportExportActivityBinding; -import protect.card_locker.importexport.DataFormat; -import protect.card_locker.importexport.ImportExportResult; -import protect.card_locker.importexport.ImportExportResultType; - -public class ImportExportActivity extends CatimaAppCompatActivity { - private ImportExportActivityBinding binding; - private static final String TAG = "Catima"; - - private ImportExportTask importExporter; - - private String importAlertTitle; - private String importAlertMessage; - private DataFormat importDataFormat; - private String exportPassword; - - private ActivityResultLauncher fileCreateLauncher; - private ActivityResultLauncher fileOpenLauncher; - - final private TaskHandler mTasks = new TaskHandler(); - - @Override - protected void onCreate(Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - binding = ImportExportActivityBinding.inflate(getLayoutInflater()); - setTitle(R.string.importExport); - setContentView(binding.getRoot()); - Utils.applyWindowInsets(binding.getRoot()); - Toolbar toolbar = binding.toolbar; - setSupportActionBar(toolbar); - enableToolbarBackButton(); - - Intent fileIntent = getIntent(); - if (fileIntent != null && fileIntent.getType() != null) { - chooseImportType(fileIntent.getData()); - } - - // would use ActivityResultContracts.CreateDocument() but mime type cannot be set - fileCreateLauncher = registerForActivityResult(new ActivityResultContracts.StartActivityForResult(), result -> { - Intent intent = result.getData(); - if (intent == null) { - Log.e(TAG, "Activity returned NULL data"); - return; - } - Uri uri = intent.getData(); - if (uri == null) { - Log.e(TAG, "Activity returned NULL uri"); - return; - } - // Running this in a thread prevents Android from throwing a NetworkOnMainThreadException for large files - // FIXME: This is still suboptimal, because showing that the export started is delayed until the network request finishes - new Thread() { - @Override - public void run() { - try { - OutputStream writer = getContentResolver().openOutputStream(uri); - Log.d(TAG, "Starting file export with: " + result); - startExport(writer, uri, exportPassword.toCharArray(), true); - } catch (IOException e) { - Log.e(TAG, "Failed to export file: " + result, e); - onExportComplete(new ImportExportResult(ImportExportResultType.GenericFailure, result.toString()), uri); - } - } - }.start(); - }); - fileOpenLauncher = registerForActivityResult(new ActivityResultContracts.GetContent(), result -> { - if (result == null) { - Log.e(TAG, "Activity returned NULL data"); - return; - } - openFileForImport(result, null); - }); - - // Check that there is a file manager available - final Intent intentCreateDocumentAction = new Intent(Intent.ACTION_CREATE_DOCUMENT); - intentCreateDocumentAction.addCategory(Intent.CATEGORY_OPENABLE); - intentCreateDocumentAction.setType("application/zip"); - intentCreateDocumentAction.putExtra(Intent.EXTRA_TITLE, "catima.zip"); - - Button exportButton = binding.exportButton; - exportButton.setOnClickListener(v -> { - AlertDialog.Builder builder = new MaterialAlertDialogBuilder(ImportExportActivity.this); - builder.setTitle(R.string.exportPassword); - - FrameLayout container = new FrameLayout(ImportExportActivity.this); - - final TextInputLayout textInputLayout = new TextInputLayout(ImportExportActivity.this); - textInputLayout.setEndIconMode(TextInputLayout.END_ICON_PASSWORD_TOGGLE); - LinearLayout.LayoutParams params = new LinearLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT); - params.setMargins(50, 10, 50, 0); - textInputLayout.setLayoutParams(params); - - final EditText input = new EditText(ImportExportActivity.this); - input.setInputType(InputType.TYPE_CLASS_TEXT | InputType.TYPE_TEXT_VARIATION_PASSWORD); - input.setHint(R.string.exportPasswordHint); - - textInputLayout.addView(input); - container.addView(textInputLayout); - builder.setView(container); - builder.setPositiveButton(R.string.ok, (dialogInterface, i) -> { - exportPassword = input.getText().toString(); - try { - fileCreateLauncher.launch(intentCreateDocumentAction); - } catch (ActivityNotFoundException e) { - Toast.makeText(getApplicationContext(), R.string.failedOpeningFileManager, Toast.LENGTH_LONG).show(); - Log.e(TAG, "No activity found to handle intent", e); - } - }); - builder.setNegativeButton(R.string.cancel, (dialogInterface, i) -> dialogInterface.cancel()); - builder.show(); - }); - - // Check that there is a file manager available - Button importFilesystem = binding.importOptionFilesystemButton; - importFilesystem.setOnClickListener(v -> chooseImportType(null)); - - // FIXME: The importer/exporter is currently quite broken - // To prevent the screen from turning off during import/export and some devices killing Catima as it's no longer foregrounded, force the screen to stay on here - getWindow().addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON); - } - - private void openFileForImport(Uri uri, char[] password) { - // Running this in a thread prevents Android from throwing a NetworkOnMainThreadException for large files - // FIXME: This is still suboptimal, because showing that the import started is delayed until the network request finishes - new Thread() { - @Override - public void run() { - try { - InputStream reader = getContentResolver().openInputStream(uri); - Log.d(TAG, "Starting file import with: " + uri); - startImport(reader, uri, importDataFormat, password, true); - } catch (IOException e) { - Log.e(TAG, "Failed to import file: " + uri, e); - onImportComplete(new ImportExportResult(ImportExportResultType.GenericFailure, e.toString()), uri, importDataFormat); - } - } - }.start(); - } - - private void chooseImportType(@Nullable Uri fileData) { - - List betaImportOptions = new ArrayList<>(); - betaImportOptions.add("Fidme"); - List importOptions = new ArrayList<>(); - - for (String importOption : getResources().getStringArray(R.array.import_types_array)) { - if (betaImportOptions.contains(importOption)) { - importOption = importOption + " (BETA)"; - } - - importOptions.add(importOption); - } - - AlertDialog.Builder builder = new MaterialAlertDialogBuilder(this); - builder.setTitle(R.string.chooseImportType) - .setItems(importOptions.toArray(new CharSequence[importOptions.size()]), (dialog, which) -> { - switch (which) { - // Catima - case 0: - importAlertTitle = getString(R.string.importCatima); - importAlertMessage = getString(R.string.importCatimaMessage); - importDataFormat = DataFormat.Catima; - break; - // Fidme - case 1: - importAlertTitle = getString(R.string.importFidme); - importAlertMessage = getString(R.string.importFidmeMessage); - importDataFormat = DataFormat.Fidme; - break; - // Loyalty Card Keychain - case 2: - importAlertTitle = getString(R.string.importLoyaltyCardKeychain); - importAlertMessage = getString(R.string.importLoyaltyCardKeychainMessage); - importDataFormat = DataFormat.Catima; - break; - // Voucher Vault - case 3: - importAlertTitle = getString(R.string.importVoucherVault); - importAlertMessage = getString(R.string.importVoucherVaultMessage); - importDataFormat = DataFormat.VoucherVault; - break; - default: - throw new IllegalArgumentException("Unknown DataFormat"); - } - - if (fileData != null) { - openFileForImport(fileData, null); - return; - } - - new MaterialAlertDialogBuilder(this) - .setTitle(importAlertTitle) - .setMessage(importAlertMessage) - .setPositiveButton(R.string.ok, (dialog1, which1) -> { - try { - fileOpenLauncher.launch("*/*"); - } catch (ActivityNotFoundException e) { - Toast.makeText(getApplicationContext(), R.string.failedOpeningFileManager, Toast.LENGTH_LONG).show(); - Log.e(TAG, "No activity found to handle intent", e); - } - }) - .setNegativeButton(R.string.cancel, null) - .show(); - }); - builder.show(); - } - - private void startImport(final InputStream target, final Uri targetUri, final DataFormat dataFormat, final char[] password, final boolean closeWhenDone) { - mTasks.flushTaskList(TaskHandler.TYPE.IMPORT, true, false, false); - ImportExportTask.TaskCompleteListener listener = new ImportExportTask.TaskCompleteListener() { - @Override - public void onTaskComplete(ImportExportResult result, DataFormat dataFormat) { - onImportComplete(result, targetUri, dataFormat); - if (closeWhenDone) { - try { - target.close(); - } catch (IOException ioException) { - ioException.printStackTrace(); - } - } - } - }; - - importExporter = new ImportExportTask(ImportExportActivity.this, - dataFormat, target, password, listener); - mTasks.executeTask(TaskHandler.TYPE.IMPORT, importExporter); - } - - private void startExport(final OutputStream target, final Uri targetUri, char[] password, final boolean closeWhenDone) { - mTasks.flushTaskList(TaskHandler.TYPE.EXPORT, true, false, false); - ImportExportTask.TaskCompleteListener listener = new ImportExportTask.TaskCompleteListener() { - @Override - public void onTaskComplete(ImportExportResult result, DataFormat dataFormat) { - onExportComplete(result, targetUri); - if (closeWhenDone) { - try { - target.close(); - } catch (IOException ioException) { - ioException.printStackTrace(); - } - } - } - }; - - importExporter = new ImportExportTask(ImportExportActivity.this, - DataFormat.Catima, target, password, listener); - mTasks.executeTask(TaskHandler.TYPE.EXPORT, importExporter); - } - - @Override - protected void onDestroy() { - mTasks.flushTaskList(TaskHandler.TYPE.IMPORT, true, false, false); - mTasks.flushTaskList(TaskHandler.TYPE.EXPORT, true, false, false); - super.onDestroy(); - } - - @Override - public boolean onOptionsItemSelected(MenuItem item) { - int id = item.getItemId(); - - if (id == android.R.id.home) { - finish(); - return true; - } - - return super.onOptionsItemSelected(item); - } - - private void retryWithPassword(DataFormat dataFormat, Uri uri) { - AlertDialog.Builder builder = new MaterialAlertDialogBuilder(this); - builder.setTitle(R.string.passwordRequired); - - FrameLayout container = new FrameLayout(ImportExportActivity.this); - - final TextInputLayout textInputLayout = new TextInputLayout(ImportExportActivity.this); - textInputLayout.setEndIconMode(TextInputLayout.END_ICON_PASSWORD_TOGGLE); - LinearLayout.LayoutParams params = new LinearLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT); - params.setMargins(50, 10, 50, 0); - textInputLayout.setLayoutParams(params); - - final EditText input = new EditText(ImportExportActivity.this); - input.setInputType(InputType.TYPE_CLASS_TEXT | InputType.TYPE_TEXT_VARIATION_PASSWORD); - input.setHint(R.string.exportPasswordHint); - - textInputLayout.addView(input); - container.addView(textInputLayout); - builder.setView(container); - - builder.setPositiveButton(R.string.ok, (dialogInterface, i) -> { - openFileForImport(uri, input.getText().toString().toCharArray()); - }); - builder.setNegativeButton(R.string.cancel, (dialogInterface, i) -> dialogInterface.cancel()); - - builder.show(); - } - - private String buildResultDialogMessage(ImportExportResult result, boolean isImport) { - int messageId; - - if (result.resultType() == ImportExportResultType.Success) { - messageId = isImport ? R.string.importSuccessful : R.string.exportSuccessful; - } else { - messageId = isImport ? R.string.importFailed : R.string.exportFailed; - } - - StringBuilder messageBuilder = new StringBuilder(getResources().getString(messageId)); - if (result.developerDetails() != null) { - messageBuilder.append("\n\n"); - messageBuilder.append(getResources().getString(R.string.include_if_asking_support)); - messageBuilder.append("\n\n"); - messageBuilder.append(result.developerDetails()); - } - - return messageBuilder.toString(); - } - - private void onImportComplete(ImportExportResult result, Uri path, DataFormat dataFormat) { - ImportExportResultType resultType = result.resultType(); - - if (resultType == ImportExportResultType.BadPassword) { - retryWithPassword(dataFormat, path); - return; - } - - AlertDialog.Builder builder = new MaterialAlertDialogBuilder(this); - builder.setTitle(resultType == ImportExportResultType.Success ? R.string.importSuccessfulTitle : R.string.importFailedTitle); - builder.setMessage(buildResultDialogMessage(result, true)); - builder.setNeutralButton(R.string.ok, (dialog, which) -> dialog.dismiss()); - - builder.create().show(); - } - - private void onExportComplete(ImportExportResult result, final Uri path) { - ImportExportResultType resultType = result.resultType(); - - AlertDialog.Builder builder = new MaterialAlertDialogBuilder(this); - builder.setTitle(resultType == ImportExportResultType.Success ? R.string.exportSuccessfulTitle : R.string.exportFailedTitle); - builder.setMessage(buildResultDialogMessage(result, false)); - builder.setNeutralButton(R.string.ok, (dialog, which) -> dialog.dismiss()); - - if (resultType == ImportExportResultType.Success) { - final CharSequence sendLabel = ImportExportActivity.this.getResources().getText(R.string.sendLabel); - - builder.setPositiveButton(sendLabel, (dialog, which) -> { - Intent sendIntent = new Intent(Intent.ACTION_SEND); - sendIntent.putExtra(Intent.EXTRA_STREAM, path); - sendIntent.setType("text/csv"); - - // 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)); - - dialog.dismiss(); - }); - } - - builder.create().show(); - } -} \ No newline at end of file diff --git a/app/src/main/java/protect/card_locker/ImportExportActivity.kt b/app/src/main/java/protect/card_locker/ImportExportActivity.kt new file mode 100644 index 000000000..aa8a2aa13 --- /dev/null +++ b/app/src/main/java/protect/card_locker/ImportExportActivity.kt @@ -0,0 +1,416 @@ +package protect.card_locker + +import android.content.ActivityNotFoundException +import android.content.Intent +import android.net.Uri +import android.os.Bundle +import android.text.InputType +import android.util.Log +import android.view.MenuItem +import android.view.ViewGroup +import android.view.WindowManager +import android.widget.Button +import android.widget.EditText +import android.widget.FrameLayout +import android.widget.LinearLayout +import android.widget.Toast +import androidx.activity.result.ActivityResultLauncher +import androidx.activity.result.contract.ActivityResultContracts +import androidx.appcompat.widget.Toolbar +import com.google.android.material.dialog.MaterialAlertDialogBuilder +import com.google.android.material.textfield.TextInputLayout +import protect.card_locker.async.TaskHandler +import protect.card_locker.databinding.ImportExportActivityBinding +import protect.card_locker.importexport.DataFormat +import protect.card_locker.importexport.ImportExportResult +import protect.card_locker.importexport.ImportExportResultType +import java.io.IOException +import java.io.InputStream +import java.io.OutputStream + +class ImportExportActivity : CatimaAppCompatActivity() { + private lateinit var binding: ImportExportActivityBinding + + private var importExporter: ImportExportTask? = null + + private var importAlertTitle: String? = null + private var importAlertMessage: String? = null + private var importDataFormat: DataFormat? = null + private var exportPassword: String? = null + + private lateinit var fileCreateLauncher: ActivityResultLauncher + private lateinit var fileOpenLauncher: ActivityResultLauncher + + private val mTasks = TaskHandler() + + companion object { + private const val TAG = "Catima" + } + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + binding = ImportExportActivityBinding.inflate(layoutInflater) + setTitle(R.string.importExport) + setContentView(binding.root) + Utils.applyWindowInsets(binding.root) + val toolbar: Toolbar = binding.toolbar + setSupportActionBar(toolbar) + enableToolbarBackButton() + + val fileIntent = intent + if (fileIntent?.type != null) { + chooseImportType(fileIntent.data) + } + + // would use ActivityResultContracts.CreateDocument() but mime type cannot be set + fileCreateLauncher = + registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result -> + val intent = result.data + if (intent == null) { + Log.e(TAG, "Activity returned NULL data") + return@registerForActivityResult + } + val uri = intent.data + if (uri == null) { + Log.e(TAG, "Activity returned NULL uri") + return@registerForActivityResult + } + // Running this in a thread prevents Android from throwing a NetworkOnMainThreadException for large files + // FIXME: This is still suboptimal, because showing that the export started is delayed until the network request finishes + Thread { + try { + val writer = contentResolver.openOutputStream(uri) + Log.d(TAG, "Starting file export with: $result") + startExport(writer, uri, exportPassword?.toCharArray(), true) + } catch (e: IOException) { + Log.e(TAG, "Failed to export file: $result", e) + onExportComplete( + ImportExportResult( + ImportExportResultType.GenericFailure, + result.toString() + ), uri + ) + } + }.start() + } + + fileOpenLauncher = + registerForActivityResult(ActivityResultContracts.GetContent()) { result -> + if (result == null) { + Log.e(TAG, "Activity returned NULL data") + return@registerForActivityResult + } + openFileForImport(result, null) + } + + // Check that there is a file manager available + val intentCreateDocumentAction = Intent(Intent.ACTION_CREATE_DOCUMENT).apply { + addCategory(Intent.CATEGORY_OPENABLE) + type = "application/zip" + putExtra(Intent.EXTRA_TITLE, "catima.zip") + } + + val exportButton: Button = binding.exportButton + exportButton.setOnClickListener { + val builder = MaterialAlertDialogBuilder(this@ImportExportActivity) + builder.setTitle(R.string.exportPassword) + + val container = FrameLayout(this@ImportExportActivity) + + val textInputLayout = TextInputLayout(this@ImportExportActivity).apply { + endIconMode = TextInputLayout.END_ICON_PASSWORD_TOGGLE + layoutParams = LinearLayout.LayoutParams( + ViewGroup.LayoutParams.MATCH_PARENT, + ViewGroup.LayoutParams.WRAP_CONTENT + ).apply { + setMargins(50, 10, 50, 0) + } + } + + val input = EditText(this@ImportExportActivity).apply { + inputType = InputType.TYPE_CLASS_TEXT or InputType.TYPE_TEXT_VARIATION_PASSWORD + setHint(R.string.exportPasswordHint) + } + + textInputLayout.addView(input) + container.addView(textInputLayout) + builder.setView(container) + builder.setPositiveButton(R.string.ok) { _, _ -> + exportPassword = input.text.toString() + try { + fileCreateLauncher.launch(intentCreateDocumentAction) + } catch (e: ActivityNotFoundException) { + Toast.makeText( + applicationContext, + R.string.failedOpeningFileManager, + Toast.LENGTH_LONG + ).show() + Log.e(TAG, "No activity found to handle intent", e) + } + } + builder.setNegativeButton(R.string.cancel) { dialogInterface, _ -> dialogInterface.cancel() } + builder.show() + } + + // Check that there is a file manager available + val importFilesystem: Button = binding.importOptionFilesystemButton + importFilesystem.setOnClickListener { chooseImportType(null) } + + // FIXME: The importer/exporter is currently quite broken + // To prevent the screen from turning off during import/export and some devices killing Catima as it's no longer foregrounded, force the screen to stay on here + window.addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON) + } + + private fun openFileForImport(uri: Uri, password: CharArray?) { + // Running this in a thread prevents Android from throwing a NetworkOnMainThreadException for large files + // FIXME: This is still suboptimal, because showing that the import started is delayed until the network request finishes + Thread { + try { + val reader = contentResolver.openInputStream(uri) + Log.d(TAG, "Starting file import with: $uri") + startImport(reader, uri, importDataFormat, password, true) + } catch (e: IOException) { + Log.e(TAG, "Failed to import file: $uri", e) + onImportComplete( + ImportExportResult( + ImportExportResultType.GenericFailure, + e.toString() + ), uri, importDataFormat + ) + } + }.start() + } + + private fun chooseImportType(fileData: Uri?) { + val betaImportOptions = mutableListOf() + betaImportOptions.add("Fidme") + val importOptions = mutableListOf() + + for (importOption in resources.getStringArray(R.array.import_types_array)) { + var option = importOption + if (betaImportOptions.contains(importOption)) { + option = "$importOption (BETA)" + } + importOptions.add(option) + } + + val builder = MaterialAlertDialogBuilder(this) + builder.setTitle(R.string.chooseImportType) + .setItems(importOptions.toTypedArray()) { _, which -> + when (which) { + // Catima + 0 -> { + importAlertTitle = getString(R.string.importCatima) + importAlertMessage = getString(R.string.importCatimaMessage) + importDataFormat = DataFormat.Catima + } + // Fidme + 1 -> { + importAlertTitle = getString(R.string.importFidme) + importAlertMessage = getString(R.string.importFidmeMessage) + importDataFormat = DataFormat.Fidme + } + // Loyalty Card Keychain + 2 -> { + importAlertTitle = getString(R.string.importLoyaltyCardKeychain) + importAlertMessage = getString(R.string.importLoyaltyCardKeychainMessage) + importDataFormat = DataFormat.Catima + } + // Voucher Vault + 3 -> { + importAlertTitle = getString(R.string.importVoucherVault) + importAlertMessage = getString(R.string.importVoucherVaultMessage) + importDataFormat = DataFormat.VoucherVault + } + + else -> throw IllegalArgumentException("Unknown DataFormat") + } + + if (fileData != null) { + openFileForImport(fileData, null) + return@setItems + } + + MaterialAlertDialogBuilder(this) + .setTitle(importAlertTitle) + .setMessage(importAlertMessage) + .setPositiveButton(R.string.ok) { _, _ -> + try { + fileOpenLauncher.launch("*/*") + } catch (e: ActivityNotFoundException) { + Toast.makeText( + applicationContext, + R.string.failedOpeningFileManager, + Toast.LENGTH_LONG + ).show() + Log.e(TAG, "No activity found to handle intent", e) + } + } + .setNegativeButton(R.string.cancel, null) + .show() + } + builder.show() + } + + private fun startImport( + target: InputStream?, + targetUri: Uri, + dataFormat: DataFormat?, + password: CharArray?, + closeWhenDone: Boolean + ) { + mTasks.flushTaskList(TaskHandler.TYPE.IMPORT, true, false, false) + val listener = ImportExportTask.TaskCompleteListener { result, dataFormat -> + onImportComplete(result, targetUri, dataFormat) + if (closeWhenDone) { + try { + target?.close() + } catch (ioException: IOException) { + ioException.printStackTrace() + } + } + } + + importExporter = ImportExportTask( + this@ImportExportActivity, + dataFormat, target, password, listener + ) + mTasks.executeTask(TaskHandler.TYPE.IMPORT, importExporter) + } + + private fun startExport( + target: OutputStream?, + targetUri: Uri, + password: CharArray?, + closeWhenDone: Boolean + ) { + mTasks.flushTaskList(TaskHandler.TYPE.EXPORT, true, false, false) + val listener = ImportExportTask.TaskCompleteListener { result, dataFormat -> + onExportComplete(result, targetUri) + if (closeWhenDone) { + try { + target?.close() + } catch (ioException: IOException) { + ioException.printStackTrace() + } + } + } + + importExporter = ImportExportTask( + this@ImportExportActivity, + DataFormat.Catima, target, password, listener + ) + mTasks.executeTask(TaskHandler.TYPE.EXPORT, importExporter) + } + + override fun onDestroy() { + mTasks.flushTaskList(TaskHandler.TYPE.IMPORT, true, false, false) + mTasks.flushTaskList(TaskHandler.TYPE.EXPORT, true, false, false) + super.onDestroy() + } + + override fun onOptionsItemSelected(item: MenuItem): Boolean { + val id = item.itemId + + if (id == android.R.id.home) { + finish() + return true + } + + return super.onOptionsItemSelected(item) + } + + private fun retryWithPassword(dataFormat: DataFormat, uri: Uri) { + val builder = MaterialAlertDialogBuilder(this) + builder.setTitle(R.string.passwordRequired) + + val container = FrameLayout(this@ImportExportActivity) + + val textInputLayout = TextInputLayout(this@ImportExportActivity).apply { + endIconMode = TextInputLayout.END_ICON_PASSWORD_TOGGLE + layoutParams = LinearLayout.LayoutParams( + ViewGroup.LayoutParams.MATCH_PARENT, + ViewGroup.LayoutParams.WRAP_CONTENT + ).apply { + setMargins(50, 10, 50, 0) + } + } + + val input = EditText(this@ImportExportActivity).apply { + inputType = InputType.TYPE_CLASS_TEXT or InputType.TYPE_TEXT_VARIATION_PASSWORD + setHint(R.string.exportPasswordHint) + } + + textInputLayout.addView(input) + container.addView(textInputLayout) + builder.setView(container) + + builder.setPositiveButton(R.string.ok) { _, _ -> + openFileForImport(uri, input.text.toString().toCharArray()) + } + builder.setNegativeButton(R.string.cancel) { dialogInterface, _ -> dialogInterface.cancel() } + + builder.show() + } + + private fun buildResultDialogMessage(result: ImportExportResult, isImport: Boolean): String { + val messageId = if (result.resultType() == ImportExportResultType.Success) { + if (isImport) R.string.importSuccessful else R.string.exportSuccessful + } else { + if (isImport) R.string.importFailed else R.string.exportFailed + } + + val messageBuilder = StringBuilder(resources.getString(messageId)) + if (result.developerDetails() != null) { + messageBuilder.append("\n\n") + messageBuilder.append(resources.getString(R.string.include_if_asking_support)) + messageBuilder.append("\n\n") + messageBuilder.append(result.developerDetails()) + } + + return messageBuilder.toString() + } + + private fun onImportComplete(result: ImportExportResult, path: Uri, dataFormat: DataFormat?) { + val resultType = result.resultType() + + if (resultType == ImportExportResultType.BadPassword) { + retryWithPassword(dataFormat!!, path) + return + } + + val builder = MaterialAlertDialogBuilder(this) + builder.setTitle(if (resultType == ImportExportResultType.Success) R.string.importSuccessfulTitle else R.string.importFailedTitle) + builder.setMessage(buildResultDialogMessage(result, true)) + builder.setNeutralButton(R.string.ok) { dialog, _ -> dialog.dismiss() } + + builder.create().show() + } + + private fun onExportComplete(result: ImportExportResult, path: Uri) { + val resultType = result.resultType() + + val builder = MaterialAlertDialogBuilder(this) + builder.setTitle(if (resultType == ImportExportResultType.Success) R.string.exportSuccessfulTitle else R.string.exportFailedTitle) + builder.setMessage(buildResultDialogMessage(result, false)) + builder.setNeutralButton(R.string.ok) { dialog, _ -> dialog.dismiss() } + + if (resultType == ImportExportResultType.Success) { + val sendLabel = this@ImportExportActivity.resources.getText(R.string.sendLabel) + + builder.setPositiveButton(sendLabel) { dialog, _ -> + val sendIntent = Intent(Intent.ACTION_SEND).apply { + putExtra(Intent.EXTRA_STREAM, path) + type = "text/csv" + // set flag to give temporary permission to external app to use the FileProvider + flags = Intent.FLAG_GRANT_READ_URI_PERMISSION + } + + this@ImportExportActivity.startActivity(Intent.createChooser(sendIntent, sendLabel)) + dialog.dismiss() + } + } + + builder.create().show() + } +} \ No newline at end of file diff --git a/app/src/test/java/protect/card_locker/ImportExportActivityTest.java b/app/src/test/java/protect/card_locker/ImportExportActivityTest.java deleted file mode 100644 index bf5f937e0..000000000 --- a/app/src/test/java/protect/card_locker/ImportExportActivityTest.java +++ /dev/null @@ -1,69 +0,0 @@ -package protect.card_locker; - -import static org.junit.Assert.assertEquals; -import static org.robolectric.Shadows.shadowOf; - -import android.app.Activity; -import android.content.Intent; -import android.content.pm.ActivityInfo; -import android.content.pm.ApplicationInfo; -import android.content.pm.PackageManager; -import android.content.pm.ResolveInfo; -import android.view.View; - -import org.junit.Test; -import org.junit.runner.RunWith; -import org.robolectric.Robolectric; -import org.robolectric.RobolectricTestRunner; -import org.robolectric.RuntimeEnvironment; - -@RunWith(RobolectricTestRunner.class) -public class ImportExportActivityTest { - private void registerIntentHandler(String handler) { - // Add something that will 'handle' the given intent type - PackageManager packageManager = RuntimeEnvironment.application.getPackageManager(); - - ResolveInfo info = new ResolveInfo(); - info.isDefault = true; - - ApplicationInfo applicationInfo = new ApplicationInfo(); - applicationInfo.packageName = "does.not.matter"; - info.activityInfo = new ActivityInfo(); - info.activityInfo.applicationInfo = applicationInfo; - info.activityInfo.name = "DoesNotMatter"; - info.activityInfo.exported = true; - - Intent intent = new Intent(handler); - - if (handler.equals(Intent.ACTION_GET_CONTENT)) { - intent.addCategory(Intent.CATEGORY_OPENABLE); - intent.setType("*/*"); - } - - shadowOf(packageManager).addResolveInfoForIntent(intent, info); - } - - private void checkVisibility(Activity activity, int state, int divider, int title, int message, int button) { - View dividerView = activity.findViewById(divider); - View titleView = activity.findViewById(title); - View messageView = activity.findViewById(message); - View buttonView = activity.findViewById(button); - - assertEquals(state, dividerView.getVisibility()); - assertEquals(state, titleView.getVisibility()); - assertEquals(state, messageView.getVisibility()); - assertEquals(state, buttonView.getVisibility()); - } - - @Test - public void testAllOptionsAvailable() { - registerIntentHandler(Intent.ACTION_PICK); - registerIntentHandler(Intent.ACTION_GET_CONTENT); - - Activity activity = Robolectric.setupActivity(ImportExportActivity.class); - - checkVisibility(activity, View.VISIBLE, R.id.dividerImportFilesystem, - R.id.importOptionFilesystemTitle, R.id.importOptionFilesystemExplanation, - R.id.importOptionFilesystemButton); - } -} diff --git a/app/src/test/java/protect/card_locker/ImportExportActivityTest.kt b/app/src/test/java/protect/card_locker/ImportExportActivityTest.kt new file mode 100644 index 000000000..ecd5cc240 --- /dev/null +++ b/app/src/test/java/protect/card_locker/ImportExportActivityTest.kt @@ -0,0 +1,77 @@ +package protect.card_locker + +import android.app.Activity +import android.content.Intent +import android.content.pm.ActivityInfo +import android.content.pm.ApplicationInfo +import android.content.pm.ResolveInfo +import android.view.View +import org.junit.Assert.assertEquals +import org.junit.Test +import org.junit.runner.RunWith +import org.robolectric.Robolectric +import org.robolectric.RobolectricTestRunner +import org.robolectric.RuntimeEnvironment +import org.robolectric.Shadows.shadowOf + +@RunWith(RobolectricTestRunner::class) +class ImportExportActivityTest { + + private fun registerIntentHandler(handler: String) { + // Add something that will 'handle' the given intent type + val packageManager = RuntimeEnvironment.application.packageManager + + val info = ResolveInfo().apply { + isDefault = true + activityInfo = ActivityInfo().apply { + applicationInfo = ApplicationInfo().apply { + packageName = "does.not.matter" + } + name = "DoesNotMatter" + exported = true + } + } + + val intent = Intent(handler) + + if (handler == Intent.ACTION_GET_CONTENT) { + intent.addCategory(Intent.CATEGORY_OPENABLE) + intent.type = "*/*" + } + + shadowOf(packageManager).addResolveInfoForIntent(intent, info) + } + + private fun checkVisibility( + activity: Activity, + state: Int, + divider: Int, + title: Int, + message: Int, + button: Int + ) { + val dividerView = activity.findViewById(divider) + val titleView = activity.findViewById(title) + val messageView = activity.findViewById(message) + val buttonView = activity.findViewById(button) + + assertEquals(state, dividerView.visibility) + assertEquals(state, titleView.visibility) + assertEquals(state, messageView.visibility) + assertEquals(state, buttonView.visibility) + } + + @Test + fun testAllOptionsAvailable() { + registerIntentHandler(Intent.ACTION_PICK) + registerIntentHandler(Intent.ACTION_GET_CONTENT) + + val activity = Robolectric.setupActivity(ImportExportActivity::class.java) + + checkVisibility( + activity, View.VISIBLE, R.id.dividerImportFilesystem, + R.id.importOptionFilesystemTitle, R.id.importOptionFilesystemExplanation, + R.id.importOptionFilesystemButton + ) + } +} \ No newline at end of file