mirror of
https://github.com/CatimaLoyalty/Android.git
synced 2025-12-31 11:07:51 -05:00
Merge pull request #2755 from amlwin/main
Convert ImportExportActivity to Kotlin
This commit is contained in:
@@ -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<Intent> fileCreateLauncher;
|
||||
private ActivityResultLauncher<String> 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<CharSequence> betaImportOptions = new ArrayList<>();
|
||||
betaImportOptions.add("Fidme");
|
||||
List<CharSequence> 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();
|
||||
}
|
||||
}
|
||||
416
app/src/main/java/protect/card_locker/ImportExportActivity.kt
Normal file
416
app/src/main/java/protect/card_locker/ImportExportActivity.kt
Normal file
@@ -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<Intent>
|
||||
private lateinit var fileOpenLauncher: ActivityResultLauncher<String>
|
||||
|
||||
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<CharSequence>()
|
||||
betaImportOptions.add("Fidme")
|
||||
val importOptions = mutableListOf<CharSequence>()
|
||||
|
||||
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()
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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<View>(divider)
|
||||
val titleView = activity.findViewById<View>(title)
|
||||
val messageView = activity.findViewById<View>(message)
|
||||
val buttonView = activity.findViewById<View>(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
|
||||
)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user