diff --git a/app/build.gradle.kts b/app/build.gradle.kts
index 7f6e72d66b..63f7372e68 100644
--- a/app/build.gradle.kts
+++ b/app/build.gradle.kts
@@ -94,6 +94,7 @@ dependencies {
implementation("androidx.preference:preference:1.2.1")
implementation("com.google.android.material:material:1.11.0")
implementation("com.github.yalantis:ucrop:2.2.8")
+ implementation("androidx.work:work-runtime:2.9.0")
coreLibraryDesugaring("com.android.tools:desugar_jdk_libs:2.0.4")
// Splash Screen
diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index 7c3db457ef..cfa4b28ae4 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -12,6 +12,8 @@
+
+
+
diff --git a/app/src/main/java/protect/card_locker/ImportExportActivity.java b/app/src/main/java/protect/card_locker/ImportExportActivity.java
index 6335902dfd..ab26aed769 100644
--- a/app/src/main/java/protect/card_locker/ImportExportActivity.java
+++ b/app/src/main/java/protect/card_locker/ImportExportActivity.java
@@ -3,6 +3,7 @@
import android.content.ActivityNotFoundException;
import android.content.DialogInterface;
import android.content.Intent;
+import android.content.pm.PackageManager;
import android.net.Uri;
import android.os.Bundle;
import android.text.InputType;
@@ -17,9 +18,15 @@
import androidx.activity.result.ActivityResultLauncher;
import androidx.activity.result.contract.ActivityResultContracts;
+import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.appcompat.app.AlertDialog;
import androidx.appcompat.widget.Toolbar;
+import androidx.work.Data;
+import androidx.work.OneTimeWorkRequest;
+import androidx.work.OutOfQuotaPolicy;
+import androidx.work.WorkManager;
+import androidx.work.WorkRequest;
import com.google.android.material.dialog.MaterialAlertDialogBuilder;
import com.google.android.material.textfield.TextInputLayout;
@@ -28,6 +35,7 @@
import java.io.InputStream;
import java.io.OutputStream;
import java.util.ArrayList;
+import java.util.Arrays;
import java.util.List;
import protect.card_locker.async.TaskHandler;
@@ -35,13 +43,12 @@
import protect.card_locker.importexport.DataFormat;
import protect.card_locker.importexport.ImportExportResult;
import protect.card_locker.importexport.ImportExportResultType;
+import protect.card_locker.importexport.ImportExportWorker;
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;
@@ -51,7 +58,10 @@ public class ImportExportActivity extends CatimaAppCompatActivity {
private ActivityResultLauncher fileOpenLauncher;
private ActivityResultLauncher filePickerLauncher;
- final private TaskHandler mTasks = new TaskHandler();
+ private static final int PERMISSION_REQUEST_EXPORT = 100;
+ private static final int PERMISSION_REQUEST_IMPORT = 101;
+
+ private WorkRequest mRequestedWorkRequest;
@Override
protected void onCreate(Bundle savedInstanceState) {
@@ -80,15 +90,20 @@ protected void onCreate(Bundle savedInstanceState) {
Log.e(TAG, "Activity returned NULL uri");
return;
}
- try {
- OutputStream writer = getContentResolver().openOutputStream(uri);
- Log.e(TAG, "Starting file export with: " + result.toString());
- startExport(writer, uri, exportPassword.toCharArray(), true);
- } catch (IOException e) {
- Log.e(TAG, "Failed to export file: " + result.toString(), e);
- onExportComplete(new ImportExportResult(ImportExportResultType.GenericFailure, result.toString()), uri);
- }
+ Data exportRequestData = new Data.Builder()
+ .putString(ImportExportWorker.INPUT_URI, uri.toString())
+ .putString(ImportExportWorker.INPUT_ACTION, ImportExportWorker.ACTION_EXPORT)
+ .putString(ImportExportWorker.INPUT_FORMAT, DataFormat.Catima.name())
+ .putString(ImportExportWorker.INPUT_PASSWORD, exportPassword)
+ .build();
+
+ mRequestedWorkRequest = new OneTimeWorkRequest.Builder(ImportExportWorker.class)
+ .setInputData(exportRequestData)
+ .setExpedited(OutOfQuotaPolicy.RUN_AS_NON_EXPEDITED_WORK_REQUEST)
+ .build();
+
+ PermissionUtils.requestPostNotificationsPermission(this, PERMISSION_REQUEST_EXPORT);
});
fileOpenLauncher = registerForActivityResult(new ActivityResultContracts.GetContent(), result -> {
if (result == null) {
@@ -160,14 +175,19 @@ protected void onCreate(Bundle savedInstanceState) {
}
private void openFileForImport(Uri uri, char[] password) {
- try {
- InputStream reader = getContentResolver().openInputStream(uri);
- Log.e(TAG, "Starting file import with: " + uri.toString());
- startImport(reader, uri, importDataFormat, password, true);
- } catch (IOException e) {
- Log.e(TAG, "Failed to import file: " + uri.toString(), e);
- onImportComplete(new ImportExportResult(ImportExportResultType.GenericFailure, e.toString()), uri, importDataFormat);
- }
+ Data importRequestData = new Data.Builder()
+ .putString(ImportExportWorker.INPUT_URI, uri.toString())
+ .putString(ImportExportWorker.INPUT_ACTION, ImportExportWorker.ACTION_IMPORT)
+ .putString(ImportExportWorker.INPUT_FORMAT, importDataFormat.name())
+ .putString(ImportExportWorker.INPUT_PASSWORD, Arrays.toString(password))
+ .build();
+
+ mRequestedWorkRequest = new OneTimeWorkRequest.Builder(ImportExportWorker.class)
+ .setInputData(importRequestData)
+ .setExpedited(OutOfQuotaPolicy.RUN_AS_NON_EXPEDITED_WORK_REQUEST)
+ .build();
+
+ PermissionUtils.requestPostNotificationsPermission(this, PERMISSION_REQUEST_IMPORT);
}
private void chooseImportType(boolean choosePicker,
@@ -232,20 +252,17 @@ private void chooseImportType(boolean choosePicker,
new MaterialAlertDialogBuilder(this)
.setTitle(importAlertTitle)
.setMessage(importAlertMessage)
- .setPositiveButton(R.string.ok, new DialogInterface.OnClickListener() {
- @Override
- public void onClick(DialogInterface dialog, int which) {
- try {
- if (choosePicker) {
- final Intent intentPickAction = new Intent(Intent.ACTION_PICK);
- filePickerLauncher.launch(intentPickAction);
- } else {
- 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);
+ .setPositiveButton(R.string.ok, (dialog1, which1) -> {
+ try {
+ if (choosePicker) {
+ final Intent intentPickAction = new Intent(Intent.ACTION_PICK);
+ filePickerLauncher.launch(intentPickAction);
+ } else {
+ 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)
@@ -254,55 +271,6 @@ public void onClick(DialogInterface dialog, int which) {
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();
@@ -343,68 +311,39 @@ private void retryWithPassword(DataFormat dataFormat, Uri uri) {
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());
+ @Override
+ public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
+ super.onRequestPermissionsResult(requestCode, permissions, grantResults);
- builder.create().show();
+ onMockedRequestPermissionsResult(requestCode, permissions, grantResults);
}
- private void onExportComplete(ImportExportResult result, final Uri path) {
- ImportExportResultType resultType = result.resultType();
+ public void onMockedRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
+ boolean granted = grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED;
+ Integer failureReason = null;
- 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 (requestCode == PERMISSION_REQUEST_EXPORT) {
+ if (granted) {
+ WorkManager.getInstance(this).enqueue(mRequestedWorkRequest);
- 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");
+ Toast.makeText(this, R.string.exportStartedCheckNotifications, Toast.LENGTH_LONG).show();
+ return;
+ }
- // set flag to give temporary permission to external app to use the FileProvider
- sendIntent.setFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
+ failureReason = R.string.postNotificationsPermissionRequired;
+ } else if (requestCode == PERMISSION_REQUEST_IMPORT) {
+ if (granted) {
+ WorkManager.getInstance(this).enqueue(mRequestedWorkRequest);
- ImportExportActivity.this.startActivity(Intent.createChooser(sendIntent,
- sendLabel));
+ Toast.makeText(this, R.string.importStartedCheckNotifications, Toast.LENGTH_LONG).show();
+ return;
+ }
- dialog.dismiss();
- });
+ failureReason = R.string.postNotificationsPermissionRequired;
}
- builder.create().show();
+ if (failureReason != null) {
+ Toast.makeText(this, failureReason, Toast.LENGTH_LONG).show();
+ }
}
}
\ 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 8284edff72..e5be80fc21 100644
--- a/app/src/main/java/protect/card_locker/ImportExportTask.java
+++ b/app/src/main/java/protect/card_locker/ImportExportTask.java
@@ -136,7 +136,7 @@ public ImportExportResult call() {
return doInBackground();
}
- interface TaskCompleteListener {
+ public interface TaskCompleteListener {
void onTaskComplete(ImportExportResult result, DataFormat format);
}
diff --git a/app/src/main/java/protect/card_locker/NotificationHelper.java b/app/src/main/java/protect/card_locker/NotificationHelper.java
new file mode 100644
index 0000000000..fe957d73d0
--- /dev/null
+++ b/app/src/main/java/protect/card_locker/NotificationHelper.java
@@ -0,0 +1,63 @@
+package protect.card_locker;
+
+import static android.content.Context.NOTIFICATION_SERVICE;
+
+import android.app.Notification;
+import android.app.NotificationChannel;
+import android.app.NotificationManager;
+import android.content.Context;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+
+public class NotificationHelper {
+
+ // Do not change these IDs!
+ public static final String CHANNEL_IMPORT = "import";
+
+ public static final String CHANNEL_EXPORT = "export";
+
+ public static final int IMPORT_ID = 100;
+ public static final int IMPORT_PROGRESS_ID = 101;
+ public static final int EXPORT_ID = 103;
+ public static final int EXPORT_PROGRESS_ID = 104;
+
+
+ public static Notification.Builder createNotificationBuilder(@NonNull Context context, @NonNull String channel, @NonNull int icon, @NonNull String title, @Nullable String message) {
+ Notification.Builder notificationBuilder = new Notification.Builder(context)
+ .setSmallIcon(icon)
+ .setTicker(title)
+ .setContentTitle(title);
+
+ if (message != null) {
+ notificationBuilder.setContentText(message);
+ }
+
+ if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.O) {
+ NotificationManager notificationManager = (NotificationManager) context.getSystemService(NOTIFICATION_SERVICE);
+ NotificationChannel notificationChannel = new NotificationChannel(channel, getChannelName(channel), NotificationManager.IMPORTANCE_DEFAULT);
+ notificationManager.createNotificationChannel(notificationChannel);
+
+ notificationBuilder.setChannelId(channel);
+ }
+
+ return notificationBuilder;
+ }
+
+ public static void sendNotification(@NonNull Context context, @NonNull int notificationId, @NonNull Notification notification) {
+ NotificationManager notificationManager = (NotificationManager) context.getSystemService(NOTIFICATION_SERVICE);
+
+ notificationManager.notify(notificationId, notification);
+ }
+
+ private static String getChannelName(@NonNull String channel) {
+ switch(channel) {
+ case CHANNEL_IMPORT:
+ return "Import";
+ case CHANNEL_EXPORT:
+ return "Export";
+ default:
+ throw new IllegalArgumentException("Unknown notification channel");
+ }
+ }
+}
diff --git a/app/src/main/java/protect/card_locker/PermissionUtils.java b/app/src/main/java/protect/card_locker/PermissionUtils.java
index 71cf75f523..057de6d1eb 100644
--- a/app/src/main/java/protect/card_locker/PermissionUtils.java
+++ b/app/src/main/java/protect/card_locker/PermissionUtils.java
@@ -42,6 +42,16 @@ public static boolean needsCameraPermission(Activity activity) {
return ContextCompat.checkSelfPermission(activity, android.Manifest.permission.CAMERA) != PackageManager.PERMISSION_GRANTED;
}
+ /**
+ * Check if post notifications permission is needed
+ *
+ * @param activity
+ * @return
+ */
+ public static boolean needsPostNotificationsPermission(Activity activity) {
+ return ContextCompat.checkSelfPermission(activity, Manifest.permission.POST_NOTIFICATIONS) != PackageManager.PERMISSION_GRANTED;
+ }
+
/**
* Call onRequestPermissionsResult after storage read permission was granted.
* Mocks a successful grant if a grant is not necessary.
@@ -91,4 +101,37 @@ public static void requestCameraPermission(CatimaAppCompatActivity activity, int
activity.onMockedRequestPermissionsResult(requestCode, permissions, mockedResults);
}
}
+
+
+ /**
+ * Call onRequestPermissionsResult after notification permission was granted.
+ * Mocks a successful grant if a grant is not necessary.
+ *
+ * @param activity
+ * @param requestCode
+ */
+ public static void requestPostNotificationsPermission(CatimaAppCompatActivity activity, int requestCode) {
+ int[] mockedResults = new int[]{ PackageManager.PERMISSION_GRANTED };
+
+ if (android.os.Build.VERSION.SDK_INT < android.os.Build.VERSION_CODES.TIRAMISU) {
+ String[] permissions = new String[0];
+ activity.onMockedRequestPermissionsResult(requestCode, permissions, mockedResults);
+ return;
+ }
+
+ String[] permissions = new String[]{ Manifest.permission.POST_NOTIFICATIONS};
+
+ if (needsPostNotificationsPermission(activity)) {
+ ActivityCompat.requestPermissions(activity, permissions, requestCode);
+ } else {
+ // FIXME: This points to onMockedRequestPermissionResult instead of to
+ // onRequestPermissionResult because onRequestPermissionResult was only introduced in
+ // Android 6.0 (SDK 23) and we and to support Android 5.0 (SDK 21) too.
+ //
+ // When minSdk becomes 23, this should point to onRequestPermissionResult directly and
+ // the activity input variable should be changed from CatimaAppCompatActivity to
+ // Activity.
+ activity.onMockedRequestPermissionsResult(requestCode, permissions, mockedResults);
+ }
+ }
}
\ No newline at end of file
diff --git a/app/src/main/java/protect/card_locker/importexport/ImportExportWorker.java b/app/src/main/java/protect/card_locker/importexport/ImportExportWorker.java
new file mode 100644
index 0000000000..2a488eaa91
--- /dev/null
+++ b/app/src/main/java/protect/card_locker/importexport/ImportExportWorker.java
@@ -0,0 +1,165 @@
+package protect.card_locker.importexport;
+
+import android.app.Notification;
+import android.app.PendingIntent;
+import android.content.Context;
+import android.database.sqlite.SQLiteDatabase;
+import android.net.Uri;
+import android.os.Build;
+import android.util.Log;
+import android.widget.Toast;
+
+import androidx.annotation.NonNull;
+import androidx.core.app.NotificationManagerCompat;
+import androidx.work.Data;
+import androidx.work.ForegroundInfo;
+import androidx.work.WorkManager;
+import androidx.work.Worker;
+import androidx.work.WorkerParameters;
+
+import java.io.FileNotFoundException;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+import java.io.OutputStream;
+import java.io.OutputStreamWriter;
+import java.nio.charset.StandardCharsets;
+
+import protect.card_locker.DBHelper;
+import protect.card_locker.NotificationHelper;
+import protect.card_locker.R;
+
+public class ImportExportWorker extends Worker {
+ private final String TAG = "Catima";
+
+ public static final String INPUT_URI = "uri";
+ public static final String INPUT_ACTION = "action";
+ public static final String INPUT_FORMAT = "format";
+ public static final String INPUT_PASSWORD = "password";
+
+ public static final String ACTION_IMPORT = "import";
+ public static final String ACTION_EXPORT = "export";
+
+ public ImportExportWorker(@NonNull Context context, @NonNull WorkerParameters workerParams) {
+ super(context, workerParams);
+ }
+
+ @NonNull
+ @Override
+ public Result doWork() {
+ Log.e("CATIMA", "Started import/export worker");
+
+ Context context = getApplicationContext();
+
+ Data inputData = getInputData();
+
+ String uriString = inputData.getString(INPUT_URI);
+ String action = inputData.getString(INPUT_ACTION);
+ String format = inputData.getString(INPUT_FORMAT);
+ String password = inputData.getString(INPUT_PASSWORD);
+
+ if (action.equals(ACTION_IMPORT)) {
+ Log.e("CATIMA", "Import requested");
+
+ setForegroundAsync(createForegroundInfo(NotificationHelper.CHANNEL_IMPORT, NotificationHelper.IMPORT_PROGRESS_ID, R.string.importing));
+
+ ImportExportResult result;
+
+ InputStream stream;
+ try {
+ stream = context.getContentResolver().openInputStream(Uri.parse(uriString));
+ } catch (FileNotFoundException e) {
+ throw new RuntimeException(e);
+ }
+
+ final SQLiteDatabase database = new DBHelper(context).getWritableDatabase();
+
+ try {
+ InputStreamReader writer = new InputStreamReader(stream, StandardCharsets.UTF_8);
+ result = MultiFormatImporter.importData(context, database, stream, DataFormat.valueOf(format), password.toCharArray());
+ writer.close();
+ } catch (IOException e) {
+ Log.e(TAG, "Unable to import file", e);
+ NotificationHelper.sendNotification(context, NotificationHelper.IMPORT_ID, NotificationHelper.createNotificationBuilder(context, NotificationHelper.CHANNEL_IMPORT, R.drawable.ic_import_export_white_24dp, context.getString(R.string.importFailedTitle), e.getLocalizedMessage()).build());
+
+ return Result.failure();
+ }
+
+ Log.i(TAG, "Import result: " + result);
+
+ if (result.resultType() == ImportExportResultType.Success) {
+ NotificationHelper.sendNotification(context, NotificationHelper.IMPORT_ID, NotificationHelper.createNotificationBuilder(context, NotificationHelper.CHANNEL_IMPORT, R.drawable.ic_import_export_white_24dp, context.getString(R.string.importSuccessfulTitle), context.getString(R.string.importSuccessful)).build());
+
+ return Result.success();
+ } else if (result.resultType() == ImportExportResultType.BadPassword) {
+ Log.e(TAG, "Needs password, unhandled for now");
+ NotificationHelper.sendNotification(context, NotificationHelper.IMPORT_ID, NotificationHelper.createNotificationBuilder(context, NotificationHelper.CHANNEL_IMPORT, R.drawable.ic_import_export_white_24dp, context.getString(R.string.importing), context.getString(R.string.passwordRequired)).build());
+
+ return Result.failure();
+ } else {
+ NotificationHelper.sendNotification(context, NotificationHelper.IMPORT_ID, NotificationHelper.createNotificationBuilder(context, NotificationHelper.CHANNEL_IMPORT, R.drawable.ic_import_export_white_24dp, context.getString(R.string.importFailedTitle), context.getString(R.string.importFailed)).build());
+
+ return Result.failure();
+ }
+ } else {
+ Log.e("CATIMA", "Export requested");
+
+ setForegroundAsync(createForegroundInfo(NotificationHelper.CHANNEL_EXPORT, NotificationHelper.EXPORT_PROGRESS_ID, R.string.exporting));
+
+ ImportExportResult result;
+
+ OutputStream stream;
+ try {
+ stream = context.getContentResolver().openOutputStream(Uri.parse(uriString));
+ } catch (FileNotFoundException e) {
+ throw new RuntimeException(e);
+ }
+
+ final SQLiteDatabase database = new DBHelper(context).getReadableDatabase();
+
+ try {
+ OutputStreamWriter writer = new OutputStreamWriter(stream, StandardCharsets.UTF_8);
+ result = MultiFormatExporter.exportData(context, database, stream, DataFormat.valueOf(format), password.toCharArray());
+ writer.close();
+ } catch (IOException e) {
+ Log.e(TAG, "Unable to export file", e);
+ NotificationHelper.sendNotification(context, NotificationHelper.EXPORT_ID, NotificationHelper.createNotificationBuilder(context, NotificationHelper.CHANNEL_EXPORT, R.drawable.ic_import_export_white_24dp, context.getString(R.string.exportFailedTitle), e.getLocalizedMessage()).build());
+
+ return Result.failure();
+ }
+
+ Log.i(TAG, "Export result: " + result);
+
+ if (result.resultType() == ImportExportResultType.Success) {
+ NotificationHelper.sendNotification(context, NotificationHelper.EXPORT_ID, NotificationHelper.createNotificationBuilder(context, NotificationHelper.CHANNEL_EXPORT, R.drawable.ic_import_export_white_24dp, context.getString(R.string.exportSuccessfulTitle), context.getString(R.string.exportSuccessful)).build());
+
+ return Result.success();
+ } else {
+ NotificationHelper.sendNotification(context, NotificationHelper.EXPORT_ID, NotificationHelper.createNotificationBuilder(context, NotificationHelper.CHANNEL_EXPORT, R.drawable.ic_import_export_white_24dp, context.getString(R.string.exportFailedTitle), context.getString(R.string.exportFailed)).build());
+
+ return Result.failure();
+ }
+ }
+ }
+
+ @NonNull
+ private ForegroundInfo createForegroundInfo(@NonNull String channel, int notificationId, int title) {
+ Context context = getApplicationContext();
+
+ String cancel = context.getString(R.string.cancel);
+ // This PendingIntent can be used to cancel the worker
+ PendingIntent intent = WorkManager.getInstance(context)
+ .createCancelPendingIntent(getId());
+
+ Notification.Builder notificationBuilder = NotificationHelper.createNotificationBuilder(context, channel, R.drawable.ic_import_export_white_24dp, context.getString(title), null);
+
+ Notification notification = notificationBuilder
+ .setOngoing(true)
+ // Add the cancel action to the notification which can
+ // be used to cancel the worker
+ .addAction(android.R.drawable.ic_delete, cancel, intent)
+ .build();
+
+ return new ForegroundInfo(notificationId, notification);
+ }
+}
diff --git a/app/src/main/java/protect/card_locker/importexport/MultiFormatImporter.java b/app/src/main/java/protect/card_locker/importexport/MultiFormatImporter.java
index 99eb3ef469..feca2d8864 100644
--- a/app/src/main/java/protect/card_locker/importexport/MultiFormatImporter.java
+++ b/app/src/main/java/protect/card_locker/importexport/MultiFormatImporter.java
@@ -45,7 +45,7 @@ public static ImportExportResult importData(Context context, SQLiteDatabase data
break;
}
- String error = null;
+ String error;
if (importer != null) {
File inputFile;
try {
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
index b6e009e194..0f7ffc3db9 100644
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -341,4 +341,7 @@
Spend
Receive
Invalid amount
+ Export started, check your notifications for the result
+ Import started, check your notifications for the result
+ Permission to show notifications needed for this action…