Compare commits

..

18 Commits

Author SHA1 Message Date
Sylvia van Os
cd5ef267e8 WIP 2024-05-21 18:49:52 +02:00
Sylvia van Os
4b1d1f4541 Merge pull request #1896 from CatimaLoyalty/dependabot/gradle/com.android.application-8.4.1
Bump com.android.application from 8.4.0 to 8.4.1
2024-05-21 08:25:15 +02:00
dependabot[bot]
801d3fa8cd ---
updated-dependencies:
- dependency-name: com.android.application
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-05-21 02:19:26 +00:00
Sylvia van Os
d15a46fc6f Fix typo 2024-05-20 20:04:01 +02:00
Sylvia van Os
7f46a267b6 Merge pull request #1895 from CatimaLoyalty/create-pull-request/patch-1716220734
Update Fastlane changelogs
2024-05-20 18:12:04 +02:00
TheLastProject
195cb8d5ee Update Fastlane changelogs 2024-05-20 15:58:53 +00:00
Sylvia van Os
7454a965bc Update CHANGELOG 2024-05-20 17:58:40 +02:00
Sylvia van Os
9ef988c259 Merge pull request #1894 from CatimaLoyalty/feature/showImageTypeInCardIdVield
Show image type on view screen when not viewing barcode
2024-05-20 17:54:09 +02:00
Sylvia van Os
7a2ff0995f Show image type on view screen when not viewing barcode 2024-05-20 17:47:19 +02:00
Sylvia van Os
9b65d3926b Merge pull request #1893 from CatimaLoyalty/create-pull-request/patch-1716210906
Update Fastlane changelogs
2024-05-20 15:20:35 +02:00
TheLastProject
06b3536079 Update Fastlane changelogs 2024-05-20 13:15:06 +00:00
Sylvia van Os
315396fd42 Merge pull request #1892 from CatimaLoyalty/feature/1860
Support for creating a card from shared text
2024-05-20 15:14:53 +02:00
Sylvia van Os
b90c43f667 Support for creating a card from shared text 2024-05-20 14:53:10 +02:00
Aglag257
6d97a29e9c Fix describeContents() in LoyaltyCard Class (#1887) 2024-05-20 13:19:13 +02:00
Sylvia van Os
7f0e2acab9 Merge pull request #1891 from CatimaLoyalty/dependabot/gradle/org.robolectric-robolectric-4.12.2
Bump org.robolectric:robolectric from 4.12.1 to 4.12.2
2024-05-20 11:52:00 +02:00
Sylvia van Os
be35886c92 Merge pull request #1890 from CatimaLoyalty/dependabot/github_actions/actions/checkout-4.1.6
Bump actions/checkout from 4.1.5 to 4.1.6
2024-05-20 11:51:23 +02:00
dependabot[bot]
73ad9c5365 Bump org.robolectric:robolectric from 4.12.1 to 4.12.2
Bumps [org.robolectric:robolectric](https://github.com/robolectric/robolectric) from 4.12.1 to 4.12.2.
- [Release notes](https://github.com/robolectric/robolectric/releases)
- [Commits](https://github.com/robolectric/robolectric/compare/robolectric-4.12.1...robolectric-4.12.2)

---
updated-dependencies:
- dependency-name: org.robolectric:robolectric
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-05-20 02:34:37 +00:00
dependabot[bot]
818751ffad Bump actions/checkout from 4.1.5 to 4.1.6
Bumps [actions/checkout](https://github.com/actions/checkout) from 4.1.5 to 4.1.6.
- [Release notes](https://github.com/actions/checkout/releases)
- [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md)
- [Commits](https://github.com/actions/checkout/compare/v4.1.5...v4.1.6)

---
updated-dependencies:
- dependency-name: actions/checkout
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-05-20 02:12:05 +00:00
24 changed files with 537 additions and 379 deletions

View File

@@ -29,7 +29,7 @@ jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4.1.5
- uses: actions/checkout@v4.1.6
- name: Fail on bad translations
run: if grep -ri "&lt;xliff" app/src/main/res/values*/strings.xml; then echo "Invalidly escaped translations found"; exit 1; fi
- uses: gradle/actions/wrapper-validation@v3

View File

@@ -27,7 +27,7 @@ jobs:
steps:
- name: Checkout repo
id: checkout
uses: actions/checkout@v4.1.5
uses: actions/checkout@v4.1.6
- name: Setup Python
uses: actions/setup-python@v5.1.0
with:

View File

@@ -25,7 +25,7 @@ jobs:
steps:
- name: Checkout repo
id: checkout
uses: actions/checkout@v4.1.5
uses: actions/checkout@v4.1.6
- name: Update contributors
id: update_contributors
uses: TheLastProject/contributors-to-file-action@v3.2.0

View File

@@ -24,7 +24,7 @@ jobs:
generate-feature-graphic:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4.1.5
- uses: actions/checkout@v4.1.6
- name: Install requirements
run: |
sudo apt-get update

View File

@@ -21,7 +21,7 @@ jobs:
gradle-update:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4.1.5
- uses: actions/checkout@v4.1.6
- uses: obfusk/gradle-update-action@v2.0.0
id: gradle-update
- uses: gradle/actions/wrapper-validation@v3

View File

@@ -25,7 +25,7 @@ jobs:
update-locales:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4.1.5
- uses: actions/checkout@v4.1.6
- name: Add new locales
run: .scripts/new-locales.py
- name: Update locales

View File

@@ -1,5 +1,10 @@
# Changelog
## Unreleased - 136
- Support for creating a card when sharing plain text
- Display image type instead of barcode below images
## v2.29.1 - 135 (2024-05-19)
- Various fixes and improvements to balance handling

View File

@@ -94,6 +94,7 @@ dependencies {
implementation("androidx.preference:preference:1.2.1")
implementation("com.google.android.material:material:1.12.0")
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")
// Splash Screen
@@ -112,7 +113,7 @@ dependencies {
// Testing
testImplementation("androidx.test:core:1.5.0")
testImplementation("junit:junit:4.13.2")
testImplementation("org.robolectric:robolectric:4.12.1")
testImplementation("org.robolectric:robolectric:4.12.2")
}
tasks.withType<SpotBugsTask>().configureEach {

View File

@@ -12,6 +12,8 @@
<uses-sdk tools:overrideLibrary="com.google.zxing.client.android" />
<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-feature
@@ -43,6 +45,7 @@
<action android:name="android.intent.action.SEND" />
<category android:name="android.intent.category.DEFAULT" />
<data android:mimeType="text/plain" />
<data android:mimeType="image/*" />
<data android:mimeType="application/pdf" />
</intent-filter>
@@ -187,5 +190,6 @@
<action android:name="android.service.controls.ControlsProviderService" />
</intent-filter>
</service>
<service android:name=".importexport.ImportExportWorker"/>
</application>
</manifest>

View File

@@ -1,8 +1,9 @@
package protect.card_locker;
import android.content.ActivityNotFoundException;
import android.content.DialogInterface;
import android.content.Context;
import android.content.Intent;
import android.content.pm.PackageManager;
import android.net.Uri;
import android.os.Bundle;
import android.text.InputType;
@@ -17,31 +18,31 @@ import android.widget.Toast;
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.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.textfield.TextInputLayout;
import java.io.IOException;
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;
import protect.card_locker.databinding.ImportExportActivityBinding;
import protect.card_locker.importexport.DataFormat;
import protect.card_locker.importexport.ImportExportResult;
import protect.card_locker.importexport.ImportExportResultType;
import 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 +52,10 @@ public class ImportExportActivity extends CatimaAppCompatActivity {
private ActivityResultLauncher<String> fileOpenLauncher;
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
protected void onCreate(Bundle savedInstanceState) {
@@ -80,15 +84,20 @@ public class ImportExportActivity extends CatimaAppCompatActivity {
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) {
@@ -159,15 +168,24 @@ public class ImportExportActivity extends CatimaAppCompatActivity {
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) {
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);
}
mRequestedWorkRequest = buildImportRequest(importDataFormat, uri, password);
PermissionUtils.requestPostNotificationsPermission(this, PERMISSION_REQUEST_IMPORT);
}
private void chooseImportType(boolean choosePicker,
@@ -232,20 +250,17 @@ public class ImportExportActivity extends CatimaAppCompatActivity {
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,60 +269,12 @@ public class ImportExportActivity extends CatimaAppCompatActivity {
builder.show();
}
private void startImport(final InputStream target, final Uri targetUri, final DataFormat dataFormat, final char[] password, final boolean closeWhenDone) {
mTasks.flushTaskList(TaskHandler.TYPE.IMPORT, true, false, false);
ImportExportTask.TaskCompleteListener listener = new ImportExportTask.TaskCompleteListener() {
@Override
public void onTaskComplete(ImportExportResult result, DataFormat dataFormat) {
onImportComplete(result, targetUri, dataFormat);
if (closeWhenDone) {
try {
target.close();
} catch (IOException ioException) {
ioException.printStackTrace();
}
}
}
};
importExporter = new ImportExportTask(ImportExportActivity.this,
dataFormat, target, password, listener);
mTasks.executeTask(TaskHandler.TYPE.IMPORT, importExporter);
}
private void startExport(final OutputStream target, final Uri targetUri, char[] password, final boolean closeWhenDone) {
mTasks.flushTaskList(TaskHandler.TYPE.EXPORT, true, false, false);
ImportExportTask.TaskCompleteListener listener = new ImportExportTask.TaskCompleteListener() {
@Override
public void onTaskComplete(ImportExportResult result, DataFormat dataFormat) {
onExportComplete(result, targetUri);
if (closeWhenDone) {
try {
target.close();
} catch (IOException ioException) {
ioException.printStackTrace();
}
}
}
};
importExporter = new ImportExportTask(ImportExportActivity.this,
DataFormat.Catima, target, password, listener);
mTasks.executeTask(TaskHandler.TYPE.EXPORT, importExporter);
}
@Override
protected void onDestroy() {
mTasks.flushTaskList(TaskHandler.TYPE.IMPORT, true, false, false);
mTasks.flushTaskList(TaskHandler.TYPE.EXPORT, true, false, false);
super.onDestroy();
}
@Override
public boolean onOptionsItemSelected(MenuItem item) {
int id = item.getItemId();
if (id == android.R.id.home) {
setResult(RESULT_CANCELED);
finish();
return true;
}
@@ -315,19 +282,19 @@ public class ImportExportActivity extends CatimaAppCompatActivity {
return super.onOptionsItemSelected(item);
}
private void retryWithPassword(DataFormat dataFormat, Uri uri) {
AlertDialog.Builder builder = new MaterialAlertDialogBuilder(this);
public static void retryWithPassword(Context context, DataFormat dataFormat, Uri uri) {
AlertDialog.Builder builder = new MaterialAlertDialogBuilder(context);
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);
LinearLayout.LayoutParams params = new LinearLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT);
params.setMargins(50, 10, 50, 0);
textInputLayout.setLayoutParams(params);
final EditText input = new EditText(ImportExportActivity.this);
final EditText input = new EditText(context);
input.setInputType(InputType.TYPE_CLASS_TEXT | InputType.TYPE_TEXT_VARIATION_PASSWORD);
input.setHint(R.string.exportPasswordHint);
@@ -336,75 +303,55 @@ public class ImportExportActivity extends CatimaAppCompatActivity {
builder.setView(container);
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.show();
}
private String buildResultDialogMessage(ImportExportResult result, boolean isImport) {
int messageId;
@Override
public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
super.onRequestPermissionsResult(requestCode, permissions, grantResults);
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();
onMockedRequestPermissionsResult(requestCode, permissions, grantResults);
}
private void onImportComplete(ImportExportResult result, Uri path, DataFormat dataFormat) {
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;
if (resultType == ImportExportResultType.BadPassword) {
retryWithPassword(dataFormat, path);
return;
if (requestCode == PERMISSION_REQUEST_EXPORT) {
if (granted) {
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);
builder.setTitle(resultType == ImportExportResultType.Success ? R.string.importSuccessfulTitle : R.string.importFailedTitle);
builder.setMessage(buildResultDialogMessage(result, true));
builder.setNeutralButton(R.string.ok, (dialog, which) -> dialog.dismiss());
builder.create().show();
}
private void onExportComplete(ImportExportResult result, final Uri path) {
ImportExportResultType resultType = result.resultType();
AlertDialog.Builder builder = new MaterialAlertDialogBuilder(this);
builder.setTitle(resultType == ImportExportResultType.Success ? R.string.exportSuccessfulTitle : R.string.exportFailedTitle);
builder.setMessage(buildResultDialogMessage(result, false));
builder.setNeutralButton(R.string.ok, (dialog, which) -> dialog.dismiss());
if (resultType == ImportExportResultType.Success) {
final CharSequence sendLabel = ImportExportActivity.this.getResources().getText(R.string.sendLabel);
builder.setPositiveButton(sendLabel, (dialog, which) -> {
Intent sendIntent = new Intent(Intent.ACTION_SEND);
sendIntent.putExtra(Intent.EXTRA_STREAM, path);
sendIntent.setType("text/csv");
// set flag to give temporary permission to external app to use the FileProvider
sendIntent.setFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
ImportExportActivity.this.startActivity(Intent.createChooser(sendIntent,
sendLabel));
dialog.dismiss();
});
if (failureReason != null) {
Toast.makeText(this, failureReason, Toast.LENGTH_LONG).show();
}
builder.create().show();
}
}

View File

@@ -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);
}
}

View File

@@ -164,7 +164,7 @@ public class LoyaltyCard implements Parcelable {
@Override
public int describeContents() {
return id;
return 0;
}
@NonNull

View File

@@ -651,10 +651,15 @@ public class LoyaltyCardViewActivity extends CatimaAppCompatActivity implements
cardIdString = loyaltyCard.cardId;
barcodeIdString = loyaltyCard.barcodeId;
binding.cardIdView.setText(loyaltyCard.cardId);
binding.mainImageDescription.setText(loyaltyCard.cardId);
// Display full text on click in case it doesn't fit in a single line
binding.cardIdView.setOnClickListener(v -> {
binding.mainImageDescription.setOnClickListener(v -> {
if (mainImageIndex != 0) {
// Don't show cardId dialog, we're displaying something else
return;
}
TextView cardIdView = new TextView(LoyaltyCardViewActivity.this);
cardIdView.setText(loyaltyCard.cardId);
cardIdView.setTextIsSelectable(true);
@@ -927,7 +932,9 @@ public class LoyaltyCardViewActivity extends CatimaAppCompatActivity implements
if (imageTypes.isEmpty()) {
barcodeRenderTarget.setVisibility(View.GONE);
binding.mainCardView.setCardBackgroundColor(Color.TRANSPARENT);
binding.cardIdView.setTextColor(MaterialColors.getColor(binding.cardIdView, com.google.android.material.R.attr.colorOnSurfaceVariant));
binding.mainImageDescription.setTextColor(MaterialColors.getColor(binding.mainImageDescription, com.google.android.material.R.attr.colorOnSurfaceVariant));
binding.mainImageDescription.setText(loyaltyCard.cardId);
return;
}
@@ -936,7 +943,7 @@ public class LoyaltyCardViewActivity extends CatimaAppCompatActivity implements
if (wantedImageType == ImageType.BARCODE) {
barcodeRenderTarget.setBackgroundColor(Color.WHITE);
binding.mainCardView.setCardBackgroundColor(Color.WHITE);
binding.cardIdView.setTextColor(getResources().getColor(R.color.md_theme_light_onSurfaceVariant));
binding.mainImageDescription.setTextColor(getResources().getColor(R.color.md_theme_light_onSurfaceVariant));
if (waitForResize) {
redrawBarcodeAfterResize(!isFullscreen);
@@ -944,18 +951,23 @@ public class LoyaltyCardViewActivity extends CatimaAppCompatActivity implements
drawBarcode(!isFullscreen);
}
binding.mainImageDescription.setText(loyaltyCard.cardId);
barcodeRenderTarget.setContentDescription(getString(R.string.barcodeImageDescriptionWithType, format.prettyName()));
} else if (wantedImageType == ImageType.IMAGE_FRONT) {
barcodeRenderTarget.setImageBitmap(frontImageBitmap);
barcodeRenderTarget.setBackgroundColor(Color.TRANSPARENT);
binding.mainCardView.setCardBackgroundColor(Color.TRANSPARENT);
binding.cardIdView.setTextColor(MaterialColors.getColor(binding.cardIdView, com.google.android.material.R.attr.colorOnSurfaceVariant));
binding.mainImageDescription.setTextColor(MaterialColors.getColor(binding.mainImageDescription, com.google.android.material.R.attr.colorOnSurfaceVariant));
binding.mainImageDescription.setText(getString(R.string.frontImageDescription));
barcodeRenderTarget.setContentDescription(getString(R.string.frontImageDescription));
} else if (wantedImageType == ImageType.IMAGE_BACK) {
barcodeRenderTarget.setImageBitmap(backImageBitmap);
barcodeRenderTarget.setBackgroundColor(Color.TRANSPARENT);
binding.mainCardView.setCardBackgroundColor(Color.TRANSPARENT);
binding.cardIdView.setTextColor(MaterialColors.getColor(binding.cardIdView, com.google.android.material.R.attr.colorOnSurfaceVariant));
binding.mainImageDescription.setTextColor(MaterialColors.getColor(binding.mainImageDescription, com.google.android.material.R.attr.colorOnSurfaceVariant));
binding.mainImageDescription.setText(getString(R.string.backImageDescription));
barcodeRenderTarget.setContentDescription(getString(R.string.backImageDescription));
} else {
throw new IllegalArgumentException("Unknown image type: " + wantedImageType);

View File

@@ -26,20 +26,29 @@ import androidx.appcompat.view.ActionMode;
import androidx.appcompat.widget.SearchView;
import androidx.core.splashscreen.SplashScreen;
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.floatingactionbutton.FloatingActionButton;
import com.google.android.material.snackbar.Snackbar;
import com.google.android.material.tabs.TabLayout;
import java.io.UnsupportedEncodingException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.Objects;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.atomic.AtomicInteger;
import protect.card_locker.databinding.ContentMainBinding;
import protect.card_locker.databinding.MainActivityBinding;
import protect.card_locker.databinding.SortingOptionBinding;
import protect.card_locker.importexport.DataFormat;
import protect.card_locker.importexport.ImportExportWorker;
import protect.card_locker.preferences.SettingsActivity;
public class MainActivity extends CatimaAppCompatActivity implements LoyaltyCardCursorAdapter.CardAdapterListener {
@@ -70,6 +79,7 @@ public class MainActivity extends CatimaAppCompatActivity implements LoyaltyCard
private ActivityResultLauncher<Intent> mBarcodeScannerLauncher;
private ActivityResultLauncher<Intent> mSettingsLauncher;
private ActivityResultLauncher<Intent> mImportExportLauncher;
private ActionMode.Callback mCurrentActionModeCallback = new ActionMode.Callback() {
@Override
@@ -303,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) {
@Override
public void handleOnBackPressed() {
@@ -482,7 +555,9 @@ public class MainActivity extends CatimaAppCompatActivity implements LoyaltyCard
if (Intent.ACTION_SEND.equals(receivedAction)) {
List<BarcodeValues> barcodeValuesList;
if (receivedType.startsWith("image/")) {
if (receivedType.equals("text/plain")) {
barcodeValuesList = Collections.singletonList(new BarcodeValues(null, intent.getStringExtra(Intent.EXTRA_TEXT)));
} else if (receivedType.startsWith("image/")) {
barcodeValuesList = Utils.retrieveBarcodesFromImage(this, intent.getParcelableExtra(Intent.EXTRA_STREAM));
} else if (receivedType.equals("application/pdf")) {
barcodeValuesList = Utils.retrieveBarcodesFromPdf(this, intent.getParcelableExtra(Intent.EXTRA_STREAM));
@@ -638,7 +713,7 @@ public class MainActivity extends CatimaAppCompatActivity implements LoyaltyCard
if (id == R.id.action_import_export) {
Intent i = new Intent(getApplicationContext(), ImportExportActivity.class);
startActivity(i);
mImportExportLauncher.launch(i);
return true;
}

View File

@@ -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");
}
}
}

View File

@@ -42,6 +42,16 @@ public class PermissionUtils {
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 class PermissionUtils {
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);
}
}
}

View File

@@ -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);
}
}

View File

@@ -45,7 +45,7 @@ public class MultiFormatImporter {
break;
}
String error = null;
String error;
if (importer != null) {
File inputFile;
try {

View File

@@ -121,7 +121,7 @@
android:layout_weight="1"/>
<TextView
android:id="@+id/card_id_view"
android:id="@+id/main_image_description"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:textSize="@dimen/text_size_large"

View File

@@ -346,4 +346,7 @@
<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="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>

View File

@@ -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
public void importWithoutColorsV1() {
InputStream inputStream = getClass().getResourceAsStream("catima_v1_no_colors.csv");

View File

@@ -308,7 +308,7 @@ public class LoyaltyCardViewActivityTest {
final String barcodeId, final String barcodeType,
final Bitmap frontImage, final Bitmap backImage) {
if (mode == ViewMode.VIEW_CARD) {
checkFieldProperties(activity, R.id.card_id_view, View.VISIBLE, cardId, FieldTypeView.TextView);
checkFieldProperties(activity, R.id.main_image_description, View.VISIBLE, cardId, FieldTypeView.TextView);
} else {
int editVisibility = View.VISIBLE;

View File

@@ -1,7 +1,7 @@
// Top-level build file where you can add configuration options common to all sub-projects/modules.
plugins {
id("com.android.application") version "8.4.0" apply false
id("com.android.application") version "8.4.1" apply false
id("com.github.spotbugs") version "5.1.4" apply false
}

View File

@@ -0,0 +1,2 @@
- Support for creating a card when sharing plain text
- Display image type instead of barcode below images