mirror of
https://github.com/CatimaLoyalty/Android.git
synced 2025-12-28 09:37:52 -05:00
Compare commits
1 Commits
create-pul
...
importExpo
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
cd5ef267e8 |
@@ -94,6 +94,7 @@ dependencies {
|
|||||||
implementation("androidx.preference:preference:1.2.1")
|
implementation("androidx.preference:preference:1.2.1")
|
||||||
implementation("com.google.android.material:material:1.12.0")
|
implementation("com.google.android.material:material:1.12.0")
|
||||||
implementation("com.github.yalantis:ucrop:2.2.9")
|
implementation("com.github.yalantis:ucrop:2.2.9")
|
||||||
|
implementation("androidx.work:work-runtime:2.9.0")
|
||||||
coreLibraryDesugaring("com.android.tools:desugar_jdk_libs:2.0.4")
|
coreLibraryDesugaring("com.android.tools:desugar_jdk_libs:2.0.4")
|
||||||
|
|
||||||
// Splash Screen
|
// Splash Screen
|
||||||
|
|||||||
@@ -12,6 +12,8 @@
|
|||||||
<uses-sdk tools:overrideLibrary="com.google.zxing.client.android" />
|
<uses-sdk tools:overrideLibrary="com.google.zxing.client.android" />
|
||||||
|
|
||||||
<uses-permission android:name="android.permission.CAMERA" />
|
<uses-permission android:name="android.permission.CAMERA" />
|
||||||
|
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
|
||||||
|
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
|
||||||
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" android:maxSdkVersion="23" />
|
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" android:maxSdkVersion="23" />
|
||||||
|
|
||||||
<uses-feature
|
<uses-feature
|
||||||
@@ -188,5 +190,6 @@
|
|||||||
<action android:name="android.service.controls.ControlsProviderService" />
|
<action android:name="android.service.controls.ControlsProviderService" />
|
||||||
</intent-filter>
|
</intent-filter>
|
||||||
</service>
|
</service>
|
||||||
|
<service android:name=".importexport.ImportExportWorker"/>
|
||||||
</application>
|
</application>
|
||||||
</manifest>
|
</manifest>
|
||||||
|
|||||||
@@ -1,8 +1,9 @@
|
|||||||
package protect.card_locker;
|
package protect.card_locker;
|
||||||
|
|
||||||
import android.content.ActivityNotFoundException;
|
import android.content.ActivityNotFoundException;
|
||||||
import android.content.DialogInterface;
|
import android.content.Context;
|
||||||
import android.content.Intent;
|
import android.content.Intent;
|
||||||
|
import android.content.pm.PackageManager;
|
||||||
import android.net.Uri;
|
import android.net.Uri;
|
||||||
import android.os.Bundle;
|
import android.os.Bundle;
|
||||||
import android.text.InputType;
|
import android.text.InputType;
|
||||||
@@ -17,31 +18,31 @@ import android.widget.Toast;
|
|||||||
|
|
||||||
import androidx.activity.result.ActivityResultLauncher;
|
import androidx.activity.result.ActivityResultLauncher;
|
||||||
import androidx.activity.result.contract.ActivityResultContracts;
|
import androidx.activity.result.contract.ActivityResultContracts;
|
||||||
|
import androidx.annotation.NonNull;
|
||||||
import androidx.annotation.Nullable;
|
import androidx.annotation.Nullable;
|
||||||
import androidx.appcompat.app.AlertDialog;
|
import androidx.appcompat.app.AlertDialog;
|
||||||
import androidx.appcompat.widget.Toolbar;
|
import androidx.appcompat.widget.Toolbar;
|
||||||
|
import androidx.work.Data;
|
||||||
|
import androidx.work.ExistingWorkPolicy;
|
||||||
|
import androidx.work.OneTimeWorkRequest;
|
||||||
|
import androidx.work.OutOfQuotaPolicy;
|
||||||
|
import androidx.work.WorkManager;
|
||||||
|
|
||||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder;
|
import com.google.android.material.dialog.MaterialAlertDialogBuilder;
|
||||||
import com.google.android.material.textfield.TextInputLayout;
|
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.ArrayList;
|
||||||
|
import java.util.Arrays;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
import protect.card_locker.async.TaskHandler;
|
|
||||||
import protect.card_locker.databinding.ImportExportActivityBinding;
|
import protect.card_locker.databinding.ImportExportActivityBinding;
|
||||||
import protect.card_locker.importexport.DataFormat;
|
import protect.card_locker.importexport.DataFormat;
|
||||||
import protect.card_locker.importexport.ImportExportResult;
|
import protect.card_locker.importexport.ImportExportWorker;
|
||||||
import protect.card_locker.importexport.ImportExportResultType;
|
|
||||||
|
|
||||||
public class ImportExportActivity extends CatimaAppCompatActivity {
|
public class ImportExportActivity extends CatimaAppCompatActivity {
|
||||||
private ImportExportActivityBinding binding;
|
private ImportExportActivityBinding binding;
|
||||||
private static final String TAG = "Catima";
|
private static final String TAG = "Catima";
|
||||||
|
|
||||||
private ImportExportTask importExporter;
|
|
||||||
|
|
||||||
private String importAlertTitle;
|
private String importAlertTitle;
|
||||||
private String importAlertMessage;
|
private String importAlertMessage;
|
||||||
private DataFormat importDataFormat;
|
private DataFormat importDataFormat;
|
||||||
@@ -51,7 +52,10 @@ public class ImportExportActivity extends CatimaAppCompatActivity {
|
|||||||
private ActivityResultLauncher<String> fileOpenLauncher;
|
private ActivityResultLauncher<String> fileOpenLauncher;
|
||||||
private ActivityResultLauncher<Intent> filePickerLauncher;
|
private ActivityResultLauncher<Intent> filePickerLauncher;
|
||||||
|
|
||||||
final private TaskHandler mTasks = new TaskHandler();
|
private static final int PERMISSION_REQUEST_EXPORT = 100;
|
||||||
|
private static final int PERMISSION_REQUEST_IMPORT = 101;
|
||||||
|
|
||||||
|
private OneTimeWorkRequest mRequestedWorkRequest;
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected void onCreate(Bundle savedInstanceState) {
|
protected void onCreate(Bundle savedInstanceState) {
|
||||||
@@ -80,15 +84,20 @@ public class ImportExportActivity extends CatimaAppCompatActivity {
|
|||||||
Log.e(TAG, "Activity returned NULL uri");
|
Log.e(TAG, "Activity returned NULL uri");
|
||||||
return;
|
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 -> {
|
fileOpenLauncher = registerForActivityResult(new ActivityResultContracts.GetContent(), result -> {
|
||||||
if (result == null) {
|
if (result == null) {
|
||||||
@@ -159,15 +168,24 @@ public class ImportExportActivity extends CatimaAppCompatActivity {
|
|||||||
importApplication.setOnClickListener(v -> chooseImportType(true, null));
|
importApplication.setOnClickListener(v -> chooseImportType(true, null));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static OneTimeWorkRequest buildImportRequest(DataFormat dataFormat, Uri uri, char[] password) {
|
||||||
|
Data importRequestData = new Data.Builder()
|
||||||
|
.putString(ImportExportWorker.INPUT_URI, uri.toString())
|
||||||
|
.putString(ImportExportWorker.INPUT_ACTION, ImportExportWorker.ACTION_IMPORT)
|
||||||
|
.putString(ImportExportWorker.INPUT_FORMAT, dataFormat.name())
|
||||||
|
.putString(ImportExportWorker.INPUT_PASSWORD, Arrays.toString(password))
|
||||||
|
.build();
|
||||||
|
|
||||||
|
return new OneTimeWorkRequest.Builder(ImportExportWorker.class)
|
||||||
|
.setInputData(importRequestData)
|
||||||
|
.setExpedited(OutOfQuotaPolicy.RUN_AS_NON_EXPEDITED_WORK_REQUEST)
|
||||||
|
.build();
|
||||||
|
}
|
||||||
|
|
||||||
private void openFileForImport(Uri uri, char[] password) {
|
private void openFileForImport(Uri uri, char[] password) {
|
||||||
try {
|
mRequestedWorkRequest = buildImportRequest(importDataFormat, uri, password);
|
||||||
InputStream reader = getContentResolver().openInputStream(uri);
|
|
||||||
Log.e(TAG, "Starting file import with: " + uri.toString());
|
PermissionUtils.requestPostNotificationsPermission(this, PERMISSION_REQUEST_IMPORT);
|
||||||
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);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void chooseImportType(boolean choosePicker,
|
private void chooseImportType(boolean choosePicker,
|
||||||
@@ -232,20 +250,17 @@ public class ImportExportActivity extends CatimaAppCompatActivity {
|
|||||||
new MaterialAlertDialogBuilder(this)
|
new MaterialAlertDialogBuilder(this)
|
||||||
.setTitle(importAlertTitle)
|
.setTitle(importAlertTitle)
|
||||||
.setMessage(importAlertMessage)
|
.setMessage(importAlertMessage)
|
||||||
.setPositiveButton(R.string.ok, new DialogInterface.OnClickListener() {
|
.setPositiveButton(R.string.ok, (dialog1, which1) -> {
|
||||||
@Override
|
try {
|
||||||
public void onClick(DialogInterface dialog, int which) {
|
if (choosePicker) {
|
||||||
try {
|
final Intent intentPickAction = new Intent(Intent.ACTION_PICK);
|
||||||
if (choosePicker) {
|
filePickerLauncher.launch(intentPickAction);
|
||||||
final Intent intentPickAction = new Intent(Intent.ACTION_PICK);
|
} else {
|
||||||
filePickerLauncher.launch(intentPickAction);
|
fileOpenLauncher.launch("*/*");
|
||||||
} 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);
|
|
||||||
}
|
}
|
||||||
|
} 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)
|
.setNegativeButton(R.string.cancel, null)
|
||||||
@@ -254,60 +269,12 @@ public class ImportExportActivity extends CatimaAppCompatActivity {
|
|||||||
builder.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
|
@Override
|
||||||
public boolean onOptionsItemSelected(MenuItem item) {
|
public boolean onOptionsItemSelected(MenuItem item) {
|
||||||
int id = item.getItemId();
|
int id = item.getItemId();
|
||||||
|
|
||||||
if (id == android.R.id.home) {
|
if (id == android.R.id.home) {
|
||||||
|
setResult(RESULT_CANCELED);
|
||||||
finish();
|
finish();
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
@@ -315,19 +282,19 @@ public class ImportExportActivity extends CatimaAppCompatActivity {
|
|||||||
return super.onOptionsItemSelected(item);
|
return super.onOptionsItemSelected(item);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void retryWithPassword(DataFormat dataFormat, Uri uri) {
|
public static void retryWithPassword(Context context, DataFormat dataFormat, Uri uri) {
|
||||||
AlertDialog.Builder builder = new MaterialAlertDialogBuilder(this);
|
AlertDialog.Builder builder = new MaterialAlertDialogBuilder(context);
|
||||||
builder.setTitle(R.string.passwordRequired);
|
builder.setTitle(R.string.passwordRequired);
|
||||||
|
|
||||||
FrameLayout container = new FrameLayout(ImportExportActivity.this);
|
FrameLayout container = new FrameLayout(context);
|
||||||
|
|
||||||
final TextInputLayout textInputLayout = new TextInputLayout(ImportExportActivity.this);
|
final TextInputLayout textInputLayout = new TextInputLayout(context);
|
||||||
textInputLayout.setEndIconMode(TextInputLayout.END_ICON_PASSWORD_TOGGLE);
|
textInputLayout.setEndIconMode(TextInputLayout.END_ICON_PASSWORD_TOGGLE);
|
||||||
LinearLayout.LayoutParams params = new LinearLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT);
|
LinearLayout.LayoutParams params = new LinearLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT);
|
||||||
params.setMargins(50, 10, 50, 0);
|
params.setMargins(50, 10, 50, 0);
|
||||||
textInputLayout.setLayoutParams(params);
|
textInputLayout.setLayoutParams(params);
|
||||||
|
|
||||||
final EditText input = new EditText(ImportExportActivity.this);
|
final EditText input = new EditText(context);
|
||||||
input.setInputType(InputType.TYPE_CLASS_TEXT | InputType.TYPE_TEXT_VARIATION_PASSWORD);
|
input.setInputType(InputType.TYPE_CLASS_TEXT | InputType.TYPE_TEXT_VARIATION_PASSWORD);
|
||||||
input.setHint(R.string.exportPasswordHint);
|
input.setHint(R.string.exportPasswordHint);
|
||||||
|
|
||||||
@@ -336,75 +303,55 @@ public class ImportExportActivity extends CatimaAppCompatActivity {
|
|||||||
builder.setView(container);
|
builder.setView(container);
|
||||||
|
|
||||||
builder.setPositiveButton(R.string.ok, (dialogInterface, i) -> {
|
builder.setPositiveButton(R.string.ok, (dialogInterface, i) -> {
|
||||||
openFileForImport(uri, input.getText().toString().toCharArray());
|
OneTimeWorkRequest importRequest = ImportExportActivity.buildImportRequest(dataFormat, uri, input.getText().toString().toCharArray());
|
||||||
|
WorkManager.getInstance(context).enqueueUniqueWork(ImportExportWorker.ACTION_IMPORT, ExistingWorkPolicy.REPLACE, importRequest);
|
||||||
});
|
});
|
||||||
builder.setNegativeButton(R.string.cancel, (dialogInterface, i) -> dialogInterface.cancel());
|
builder.setNegativeButton(R.string.cancel, (dialogInterface, i) -> dialogInterface.cancel());
|
||||||
|
|
||||||
builder.show();
|
builder.show();
|
||||||
}
|
}
|
||||||
|
|
||||||
private String buildResultDialogMessage(ImportExportResult result, boolean isImport) {
|
@Override
|
||||||
int messageId;
|
public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
|
||||||
|
super.onRequestPermissionsResult(requestCode, permissions, grantResults);
|
||||||
|
|
||||||
if (result.resultType() == ImportExportResultType.Success) {
|
onMockedRequestPermissionsResult(requestCode, permissions, grantResults);
|
||||||
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) {
|
public void onMockedRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
|
||||||
ImportExportResultType resultType = result.resultType();
|
boolean granted = grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED;
|
||||||
|
Integer failureReason = null;
|
||||||
|
|
||||||
if (resultType == ImportExportResultType.BadPassword) {
|
if (requestCode == PERMISSION_REQUEST_EXPORT) {
|
||||||
retryWithPassword(dataFormat, path);
|
if (granted) {
|
||||||
return;
|
WorkManager.getInstance(this).enqueueUniqueWork(ImportExportWorker.ACTION_EXPORT, ExistingWorkPolicy.REPLACE, mRequestedWorkRequest);
|
||||||
|
|
||||||
|
Toast.makeText(this, R.string.exportStartedCheckNotifications, Toast.LENGTH_LONG).show();
|
||||||
|
|
||||||
|
// Import/export started
|
||||||
|
setResult(RESULT_OK);
|
||||||
|
finish();
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
failureReason = R.string.postNotificationsPermissionRequired;
|
||||||
|
} else if (requestCode == PERMISSION_REQUEST_IMPORT) {
|
||||||
|
if (granted) {
|
||||||
|
WorkManager.getInstance(this).enqueueUniqueWork(ImportExportWorker.ACTION_IMPORT, ExistingWorkPolicy.REPLACE, mRequestedWorkRequest);
|
||||||
|
|
||||||
|
// Import/export started
|
||||||
|
setResult(RESULT_OK);
|
||||||
|
finish();
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
failureReason = R.string.postNotificationsPermissionRequired;
|
||||||
}
|
}
|
||||||
|
|
||||||
AlertDialog.Builder builder = new MaterialAlertDialogBuilder(this);
|
if (failureReason != null) {
|
||||||
builder.setTitle(resultType == ImportExportResultType.Success ? R.string.importSuccessfulTitle : R.string.importFailedTitle);
|
Toast.makeText(this, failureReason, Toast.LENGTH_LONG).show();
|
||||||
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();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,143 +0,0 @@
|
|||||||
package protect.card_locker;
|
|
||||||
|
|
||||||
import android.app.Activity;
|
|
||||||
import android.app.ProgressDialog;
|
|
||||||
import android.content.Context;
|
|
||||||
import android.content.DialogInterface;
|
|
||||||
import android.database.sqlite.SQLiteDatabase;
|
|
||||||
import android.util.Log;
|
|
||||||
|
|
||||||
import java.io.IOException;
|
|
||||||
import java.io.InputStream;
|
|
||||||
import java.io.OutputStream;
|
|
||||||
import java.io.OutputStreamWriter;
|
|
||||||
import java.nio.charset.StandardCharsets;
|
|
||||||
|
|
||||||
import protect.card_locker.async.CompatCallable;
|
|
||||||
import protect.card_locker.importexport.DataFormat;
|
|
||||||
import protect.card_locker.importexport.ImportExportResult;
|
|
||||||
import protect.card_locker.importexport.ImportExportResultType;
|
|
||||||
import protect.card_locker.importexport.MultiFormatExporter;
|
|
||||||
import protect.card_locker.importexport.MultiFormatImporter;
|
|
||||||
|
|
||||||
public class ImportExportTask implements CompatCallable<ImportExportResult> {
|
|
||||||
private static final String TAG = "Catima";
|
|
||||||
|
|
||||||
private Activity activity;
|
|
||||||
private boolean doImport;
|
|
||||||
private DataFormat format;
|
|
||||||
private OutputStream outputStream;
|
|
||||||
private InputStream inputStream;
|
|
||||||
private char[] password;
|
|
||||||
private TaskCompleteListener listener;
|
|
||||||
|
|
||||||
private ProgressDialog progress;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Constructor which will setup a task for exporting to the given file
|
|
||||||
*/
|
|
||||||
ImportExportTask(Activity activity, DataFormat format, OutputStream output, char[] password,
|
|
||||||
TaskCompleteListener listener) {
|
|
||||||
super();
|
|
||||||
this.activity = activity;
|
|
||||||
this.doImport = false;
|
|
||||||
this.format = format;
|
|
||||||
this.outputStream = output;
|
|
||||||
this.password = password;
|
|
||||||
this.listener = listener;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Constructor which will setup a task for importing from the given InputStream.
|
|
||||||
*/
|
|
||||||
ImportExportTask(Activity activity, DataFormat format, InputStream input, char[] password,
|
|
||||||
TaskCompleteListener listener) {
|
|
||||||
super();
|
|
||||||
this.activity = activity;
|
|
||||||
this.doImport = true;
|
|
||||||
this.format = format;
|
|
||||||
this.inputStream = input;
|
|
||||||
this.password = password;
|
|
||||||
this.listener = listener;
|
|
||||||
}
|
|
||||||
|
|
||||||
private ImportExportResult performImport(Context context, InputStream stream, SQLiteDatabase database, char[] password) {
|
|
||||||
ImportExportResult importResult = MultiFormatImporter.importData(context, database, stream, format, password);
|
|
||||||
|
|
||||||
Log.i(TAG, "Import result: " + importResult);
|
|
||||||
|
|
||||||
return importResult;
|
|
||||||
}
|
|
||||||
|
|
||||||
private ImportExportResult performExport(Context context, OutputStream stream, SQLiteDatabase database, char[] password) {
|
|
||||||
ImportExportResult result;
|
|
||||||
|
|
||||||
try {
|
|
||||||
OutputStreamWriter writer = new OutputStreamWriter(stream, StandardCharsets.UTF_8);
|
|
||||||
result = MultiFormatExporter.exportData(context, database, stream, format, password);
|
|
||||||
writer.close();
|
|
||||||
} catch (IOException e) {
|
|
||||||
result = new ImportExportResult(ImportExportResultType.GenericFailure, e.toString());
|
|
||||||
Log.e(TAG, "Unable to export file", e);
|
|
||||||
}
|
|
||||||
|
|
||||||
Log.i(TAG, "Export result: " + result);
|
|
||||||
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void onPreExecute() {
|
|
||||||
progress = new ProgressDialog(activity);
|
|
||||||
progress.setTitle(doImport ? R.string.importing : R.string.exporting);
|
|
||||||
|
|
||||||
progress.setOnDismissListener(new DialogInterface.OnDismissListener() {
|
|
||||||
@Override
|
|
||||||
public void onDismiss(DialogInterface dialog) {
|
|
||||||
ImportExportTask.this.stop();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
progress.show();
|
|
||||||
}
|
|
||||||
|
|
||||||
protected ImportExportResult doInBackground(Void... nothing) {
|
|
||||||
final SQLiteDatabase database = new DBHelper(activity).getWritableDatabase();
|
|
||||||
ImportExportResult result;
|
|
||||||
|
|
||||||
if (doImport) {
|
|
||||||
result = performImport(activity.getApplicationContext(), inputStream, database, password);
|
|
||||||
} else {
|
|
||||||
result = performExport(activity.getApplicationContext(), outputStream, database, password);
|
|
||||||
}
|
|
||||||
|
|
||||||
database.close();
|
|
||||||
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void onPostExecute(Object castResult) {
|
|
||||||
listener.onTaskComplete((ImportExportResult) castResult, format);
|
|
||||||
|
|
||||||
progress.dismiss();
|
|
||||||
Log.i(TAG, (doImport ? "Import" : "Export") + " Complete");
|
|
||||||
}
|
|
||||||
|
|
||||||
protected void onCancelled() {
|
|
||||||
progress.dismiss();
|
|
||||||
Log.i(TAG, (doImport ? "Import" : "Export") + " Cancelled");
|
|
||||||
}
|
|
||||||
|
|
||||||
protected void stop() {
|
|
||||||
// Whelp
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public ImportExportResult call() {
|
|
||||||
return doInBackground();
|
|
||||||
}
|
|
||||||
|
|
||||||
interface TaskCompleteListener {
|
|
||||||
void onTaskComplete(ImportExportResult result, DataFormat format);
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -26,9 +26,13 @@ import androidx.appcompat.view.ActionMode;
|
|||||||
import androidx.appcompat.widget.SearchView;
|
import androidx.appcompat.widget.SearchView;
|
||||||
import androidx.core.splashscreen.SplashScreen;
|
import androidx.core.splashscreen.SplashScreen;
|
||||||
import androidx.recyclerview.widget.RecyclerView;
|
import androidx.recyclerview.widget.RecyclerView;
|
||||||
|
import androidx.work.Data;
|
||||||
|
import androidx.work.WorkInfo;
|
||||||
|
import androidx.work.WorkManager;
|
||||||
|
|
||||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder;
|
import com.google.android.material.dialog.MaterialAlertDialogBuilder;
|
||||||
import com.google.android.material.floatingactionbutton.FloatingActionButton;
|
import com.google.android.material.floatingactionbutton.FloatingActionButton;
|
||||||
|
import com.google.android.material.snackbar.Snackbar;
|
||||||
import com.google.android.material.tabs.TabLayout;
|
import com.google.android.material.tabs.TabLayout;
|
||||||
|
|
||||||
import java.io.UnsupportedEncodingException;
|
import java.io.UnsupportedEncodingException;
|
||||||
@@ -36,11 +40,15 @@ import java.util.ArrayList;
|
|||||||
import java.util.Arrays;
|
import java.util.Arrays;
|
||||||
import java.util.Collections;
|
import java.util.Collections;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
import java.util.Objects;
|
||||||
|
import java.util.concurrent.ExecutionException;
|
||||||
import java.util.concurrent.atomic.AtomicInteger;
|
import java.util.concurrent.atomic.AtomicInteger;
|
||||||
|
|
||||||
import protect.card_locker.databinding.ContentMainBinding;
|
import protect.card_locker.databinding.ContentMainBinding;
|
||||||
import protect.card_locker.databinding.MainActivityBinding;
|
import protect.card_locker.databinding.MainActivityBinding;
|
||||||
import protect.card_locker.databinding.SortingOptionBinding;
|
import protect.card_locker.databinding.SortingOptionBinding;
|
||||||
|
import protect.card_locker.importexport.DataFormat;
|
||||||
|
import protect.card_locker.importexport.ImportExportWorker;
|
||||||
import protect.card_locker.preferences.SettingsActivity;
|
import protect.card_locker.preferences.SettingsActivity;
|
||||||
|
|
||||||
public class MainActivity extends CatimaAppCompatActivity implements LoyaltyCardCursorAdapter.CardAdapterListener {
|
public class MainActivity extends CatimaAppCompatActivity implements LoyaltyCardCursorAdapter.CardAdapterListener {
|
||||||
@@ -71,6 +79,7 @@ public class MainActivity extends CatimaAppCompatActivity implements LoyaltyCard
|
|||||||
|
|
||||||
private ActivityResultLauncher<Intent> mBarcodeScannerLauncher;
|
private ActivityResultLauncher<Intent> mBarcodeScannerLauncher;
|
||||||
private ActivityResultLauncher<Intent> mSettingsLauncher;
|
private ActivityResultLauncher<Intent> mSettingsLauncher;
|
||||||
|
private ActivityResultLauncher<Intent> mImportExportLauncher;
|
||||||
|
|
||||||
private ActionMode.Callback mCurrentActionModeCallback = new ActionMode.Callback() {
|
private ActionMode.Callback mCurrentActionModeCallback = new ActionMode.Callback() {
|
||||||
@Override
|
@Override
|
||||||
@@ -304,6 +313,69 @@ public class MainActivity extends CatimaAppCompatActivity implements LoyaltyCard
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
mImportExportLauncher = registerForActivityResult(new ActivityResultContracts.StartActivityForResult(), result -> {
|
||||||
|
// User didn't ask for import or export
|
||||||
|
if (result.getResultCode() != RESULT_OK) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Watch for active imports/exports
|
||||||
|
new Thread(() -> {
|
||||||
|
WorkManager workManager = WorkManager.getInstance(MainActivity.this);
|
||||||
|
|
||||||
|
Snackbar importRunning = Snackbar.make(binding.getRoot(), R.string.importing, Snackbar.LENGTH_INDEFINITE);
|
||||||
|
|
||||||
|
while (true) {
|
||||||
|
try {
|
||||||
|
List<WorkInfo> activeImports = workManager.getWorkInfosForUniqueWork(ImportExportWorker.ACTION_IMPORT).get();
|
||||||
|
|
||||||
|
// We should only have one import running at a time, so it should be safe to always grab the latest
|
||||||
|
WorkInfo activeImport = activeImports.get(activeImports.size() - 1);
|
||||||
|
WorkInfo.State importState = activeImport.getState();
|
||||||
|
|
||||||
|
if (importState == WorkInfo.State.RUNNING || importState == WorkInfo.State.ENQUEUED || importState == WorkInfo.State.BLOCKED) {
|
||||||
|
importRunning.show();
|
||||||
|
} else if (importState == WorkInfo.State.SUCCEEDED) {
|
||||||
|
importRunning.dismiss();
|
||||||
|
runOnUiThread(() -> {
|
||||||
|
Toast.makeText(getApplicationContext(), getString(R.string.importSuccessful), Toast.LENGTH_LONG).show();
|
||||||
|
updateLoyaltyCardList(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
break;
|
||||||
|
} else {
|
||||||
|
importRunning.dismiss();
|
||||||
|
|
||||||
|
Data outputData = activeImport.getOutputData();
|
||||||
|
|
||||||
|
// FIXME: This dialog will asynchronously be accepted or declined and we don't know the status of it so we can't show the import state
|
||||||
|
// We want to get back into this function
|
||||||
|
// A cheap fix would be to keep looping but if the user dismissed the dialog that could mean we're looping forever...
|
||||||
|
if (Objects.equals(outputData.getString(ImportExportWorker.OUTPUT_ERROR_REASON), ImportExportWorker.ERROR_PASSWORD_REQUIRED)) {
|
||||||
|
runOnUiThread(() -> ImportExportActivity.retryWithPassword(
|
||||||
|
MainActivity.this,
|
||||||
|
DataFormat.valueOf(outputData.getString(ImportExportWorker.INPUT_FORMAT)),
|
||||||
|
Uri.parse(outputData.getString(ImportExportWorker.INPUT_URI))
|
||||||
|
));
|
||||||
|
} else {
|
||||||
|
runOnUiThread(() -> {
|
||||||
|
Toast.makeText(getApplicationContext(), getString(R.string.importFailed), Toast.LENGTH_LONG).show();
|
||||||
|
Toast.makeText(getApplicationContext(), activeImport.getOutputData().getString(ImportExportWorker.OUTPUT_ERROR_REASON), Toast.LENGTH_LONG).show();
|
||||||
|
Toast.makeText(getApplicationContext(), activeImport.getOutputData().getString(ImportExportWorker.OUTPUT_ERROR_DETAILS), Toast.LENGTH_LONG).show();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
} catch (ExecutionException e) {
|
||||||
|
throw new RuntimeException(e);
|
||||||
|
} catch (InterruptedException e) {
|
||||||
|
throw new RuntimeException(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}).start();
|
||||||
|
});
|
||||||
|
|
||||||
getOnBackPressedDispatcher().addCallback(this, new OnBackPressedCallback(true) {
|
getOnBackPressedDispatcher().addCallback(this, new OnBackPressedCallback(true) {
|
||||||
@Override
|
@Override
|
||||||
public void handleOnBackPressed() {
|
public void handleOnBackPressed() {
|
||||||
@@ -641,7 +713,7 @@ public class MainActivity extends CatimaAppCompatActivity implements LoyaltyCard
|
|||||||
|
|
||||||
if (id == R.id.action_import_export) {
|
if (id == R.id.action_import_export) {
|
||||||
Intent i = new Intent(getApplicationContext(), ImportExportActivity.class);
|
Intent i = new Intent(getApplicationContext(), ImportExportActivity.class);
|
||||||
startActivity(i);
|
mImportExportLauncher.launch(i);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -42,6 +42,16 @@ public class PermissionUtils {
|
|||||||
return ContextCompat.checkSelfPermission(activity, android.Manifest.permission.CAMERA) != PackageManager.PERMISSION_GRANTED;
|
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.
|
* Call onRequestPermissionsResult after storage read permission was granted.
|
||||||
* Mocks a successful grant if a grant is not necessary.
|
* Mocks a successful grant if a grant is not necessary.
|
||||||
@@ -91,4 +101,37 @@ public class PermissionUtils {
|
|||||||
activity.onMockedRequestPermissionsResult(requestCode, permissions, mockedResults);
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,212 @@
|
|||||||
|
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.util.Log;
|
||||||
|
|
||||||
|
import androidx.annotation.NonNull;
|
||||||
|
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 static final String OUTPUT_ERROR_REASON = "errorReason";
|
||||||
|
public static final String ERROR_GENERIC = "errorTypeGeneric";
|
||||||
|
public static final String ERROR_PASSWORD_REQUIRED = "errorTypePasswordRequired";
|
||||||
|
public static final String OUTPUT_ERROR_DETAILS = "errorDetails";
|
||||||
|
|
||||||
|
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());
|
||||||
|
|
||||||
|
Data failureData = new Data.Builder()
|
||||||
|
.putString(OUTPUT_ERROR_REASON, ERROR_GENERIC)
|
||||||
|
.putString(OUTPUT_ERROR_DETAILS, e.getLocalizedMessage())
|
||||||
|
.putString(INPUT_URI, uriString)
|
||||||
|
.putString(INPUT_ACTION, action)
|
||||||
|
.putString(INPUT_FORMAT, format)
|
||||||
|
.putString(INPUT_PASSWORD, password)
|
||||||
|
.build();
|
||||||
|
|
||||||
|
return Result.failure(failureData);
|
||||||
|
}
|
||||||
|
|
||||||
|
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());
|
||||||
|
|
||||||
|
Data failureData = new Data.Builder()
|
||||||
|
.putString(OUTPUT_ERROR_REASON, ERROR_PASSWORD_REQUIRED)
|
||||||
|
.putString(OUTPUT_ERROR_DETAILS, result.developerDetails())
|
||||||
|
.putString(INPUT_URI, uriString)
|
||||||
|
.putString(INPUT_ACTION, action)
|
||||||
|
.putString(INPUT_FORMAT, format)
|
||||||
|
.putString(INPUT_PASSWORD, password)
|
||||||
|
.build();
|
||||||
|
|
||||||
|
return Result.failure(failureData);
|
||||||
|
} 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());
|
||||||
|
|
||||||
|
Data failureData = new Data.Builder()
|
||||||
|
.putString(OUTPUT_ERROR_REASON, ERROR_GENERIC)
|
||||||
|
.putString(OUTPUT_ERROR_DETAILS, result.developerDetails())
|
||||||
|
.putString(INPUT_URI, uriString)
|
||||||
|
.putString(INPUT_ACTION, action)
|
||||||
|
.putString(INPUT_FORMAT, format)
|
||||||
|
.putString(INPUT_PASSWORD, password)
|
||||||
|
.build();
|
||||||
|
|
||||||
|
return Result.failure(failureData);
|
||||||
|
}
|
||||||
|
} 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());
|
||||||
|
|
||||||
|
Data failureData = new Data.Builder()
|
||||||
|
.putString(OUTPUT_ERROR_REASON, ERROR_GENERIC)
|
||||||
|
.putString(OUTPUT_ERROR_DETAILS, e.getLocalizedMessage())
|
||||||
|
.putString(INPUT_URI, uriString)
|
||||||
|
.putString(INPUT_ACTION, action)
|
||||||
|
.putString(INPUT_FORMAT, format)
|
||||||
|
.putString(INPUT_PASSWORD, password)
|
||||||
|
.build();
|
||||||
|
|
||||||
|
return Result.failure(failureData);
|
||||||
|
}
|
||||||
|
|
||||||
|
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());
|
||||||
|
|
||||||
|
Data failureData = new Data.Builder()
|
||||||
|
.putString(OUTPUT_ERROR_REASON, ERROR_GENERIC)
|
||||||
|
.putString(OUTPUT_ERROR_DETAILS, result.developerDetails())
|
||||||
|
.putString(INPUT_URI, uriString)
|
||||||
|
.putString(INPUT_ACTION, action)
|
||||||
|
.putString(INPUT_FORMAT, format)
|
||||||
|
.putString(INPUT_PASSWORD, password)
|
||||||
|
.build();
|
||||||
|
|
||||||
|
return Result.failure(failureData);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -45,7 +45,7 @@ public class MultiFormatImporter {
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
String error = null;
|
String error;
|
||||||
if (importer != null) {
|
if (importer != null) {
|
||||||
File inputFile;
|
File inputFile;
|
||||||
try {
|
try {
|
||||||
|
|||||||
@@ -346,4 +346,7 @@
|
|||||||
<string name="failedLaunchingFileManager">Could not find a supported file manager</string>
|
<string name="failedLaunchingFileManager">Could not find a supported file manager</string>
|
||||||
<string name="multipleBarcodesFoundPleaseChooseOne">Which of the found barcodes do you want to use?</string>
|
<string name="multipleBarcodesFoundPleaseChooseOne">Which of the found barcodes do you want to use?</string>
|
||||||
<string name="pageWithNumber">Page <xliff:g>%d</xliff:g></string>
|
<string name="pageWithNumber">Page <xliff:g>%d</xliff:g></string>
|
||||||
|
<string name="exportStartedCheckNotifications">Export started, check your notifications for the result</string>
|
||||||
|
<string name="importStartedCheckNotifications">Import started, check your notifications for the result</string>
|
||||||
|
<string name="postNotificationsPermissionRequired">Permission to show notifications needed for this action…</string>
|
||||||
</resources>
|
</resources>
|
||||||
|
|||||||
@@ -574,72 +574,6 @@ public class ImportExportTest {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class TestTaskCompleteListener implements ImportExportTask.TaskCompleteListener {
|
|
||||||
ImportExportResult result;
|
|
||||||
|
|
||||||
public void onTaskComplete(ImportExportResult result, DataFormat dataFormat) {
|
|
||||||
this.result = result;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
@LooperMode(LooperMode.Mode.PAUSED)
|
|
||||||
public void useImportExportTask() throws FileNotFoundException {
|
|
||||||
final int NUM_CARDS = 10;
|
|
||||||
|
|
||||||
final File sdcardDir = Environment.getExternalStorageDirectory();
|
|
||||||
final File exportFile = new File(sdcardDir, "Catima.csv");
|
|
||||||
|
|
||||||
TestHelpers.addLoyaltyCards(mDatabase, NUM_CARDS);
|
|
||||||
|
|
||||||
TestTaskCompleteListener listener = new TestTaskCompleteListener();
|
|
||||||
|
|
||||||
// Export to the file
|
|
||||||
final String password = "123456789";
|
|
||||||
FileOutputStream fileOutputStream = new FileOutputStream(exportFile);
|
|
||||||
ImportExportTask task = new ImportExportTask(activity, DataFormat.Catima, fileOutputStream, password.toCharArray(), listener);
|
|
||||||
TaskHandler mTasks = new TaskHandler();
|
|
||||||
mTasks.executeTask(TaskHandler.TYPE.EXPORT, task);
|
|
||||||
|
|
||||||
// Actually run the task to completion
|
|
||||||
mTasks.flushTaskList(TaskHandler.TYPE.EXPORT, false, false, true);
|
|
||||||
shadowOf(Looper.getMainLooper()).idleFor(Duration.ofMillis(5000));
|
|
||||||
ShadowLooper.runUiThreadTasksIncludingDelayedTasks();
|
|
||||||
|
|
||||||
|
|
||||||
// Check that the listener was executed
|
|
||||||
assertNotNull(listener.result);
|
|
||||||
assertEquals(ImportExportResultType.Success, listener.result.resultType());
|
|
||||||
|
|
||||||
TestHelpers.getEmptyDb(activity);
|
|
||||||
|
|
||||||
// Import everything back from the default location
|
|
||||||
|
|
||||||
listener = new TestTaskCompleteListener();
|
|
||||||
|
|
||||||
FileInputStream fileStream = new FileInputStream(exportFile);
|
|
||||||
|
|
||||||
task = new ImportExportTask(activity, DataFormat.Catima, fileStream, password.toCharArray(), listener);
|
|
||||||
mTasks.executeTask(TaskHandler.TYPE.IMPORT, task);
|
|
||||||
|
|
||||||
// Actually run the task to completion
|
|
||||||
// I am CONVINCED there must be a better way than to wait on this Queue with a flush.
|
|
||||||
mTasks.flushTaskList(TaskHandler.TYPE.IMPORT, false, false, true);
|
|
||||||
shadowOf(Looper.getMainLooper()).idleFor(Duration.ofMillis(5000));
|
|
||||||
ShadowLooper.runUiThreadTasksIncludingDelayedTasks();
|
|
||||||
|
|
||||||
// Check that the listener was executed
|
|
||||||
assertNotNull(listener.result);
|
|
||||||
assertEquals(ImportExportResultType.Success, listener.result.resultType());
|
|
||||||
|
|
||||||
assertEquals(NUM_CARDS, DBHelper.getLoyaltyCardCount(mDatabase));
|
|
||||||
|
|
||||||
checkLoyaltyCards();
|
|
||||||
|
|
||||||
// Clear the database for the next format under test
|
|
||||||
TestHelpers.getEmptyDb(activity);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void importWithoutColorsV1() {
|
public void importWithoutColorsV1() {
|
||||||
InputStream inputStream = getClass().getResourceAsStream("catima_v1_no_colors.csv");
|
InputStream inputStream = getClass().getResourceAsStream("catima_v1_no_colors.csv");
|
||||||
|
|||||||
Reference in New Issue
Block a user