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…