diff --git a/CHANGELOG.md b/CHANGELOG.md index 73ff9aa57..360e28bd4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,23 @@ # Changelog -## Unreleased - 90 +## v2.10.0 - 92 (2021-11-20) + +- New main screen layout +- Fix bottomsheet sizing issues when switching in and out of fullscreen + +## v2.9.0 - 91 (2021-11-14) + +- Improved group management support +- Support cropping images +- Fix image data loss when saving after rotating in edit view +- Ability to set a custom image as card icon + +## v2.8.1 - 90 (2021-10-27) + +- Fix dots in card view having the wrong colour when changing theme manually +- Fix crash in card view on rotation/theme change +- Fix flashing of cards list +- Fix text overlaying star icon ## v2.8.0 - 89 (2021-10-25) diff --git a/app/build.gradle b/app/build.gradle index d409fcb67..21f7435be 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -18,8 +18,8 @@ android { applicationId "me.hackerchick.catima" minSdkVersion 21 targetSdkVersion 31 - versionCode 89 - versionName "2.8.0" + versionCode 92 + versionName "2.10.0" vectorDrawables.useSupportLibrary true multiDexEnabled true @@ -80,11 +80,13 @@ android { dependencies { // AndroidX - implementation 'androidx.appcompat:appcompat:1.3.1' - implementation 'androidx.constraintlayout:constraintlayout:2.1.1' + implementation 'androidx.appcompat:appcompat:1.4.0' + implementation 'androidx.constraintlayout:constraintlayout:2.1.2' implementation 'androidx.exifinterface:exifinterface:1.3.3' + implementation 'androidx.palette:palette:1.0.0' implementation 'androidx.preference:preference:1.1.1' implementation 'com.google.android.material:material:1.4.0' + implementation 'com.github.yalantis:ucrop:2.2.7' coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:1.1.5' // Splash Screen @@ -96,7 +98,7 @@ dependencies { implementation 'org.apache.commons:commons-csv:1.9.0' implementation 'com.jaredrummler:colorpicker:1.1.0' implementation 'com.github.invissvenska:NumberPickerPreference:1.0.3' - implementation 'net.lingala.zip4j:zip4j:2.9.0' + implementation 'net.lingala.zip4j:zip4j:2.9.1' // SpotBugs implementation 'io.wcm.tooling.spotbugs:io.wcm.tooling.spotbugs.annotations:1.0.0' @@ -104,7 +106,7 @@ dependencies { // Testing testImplementation 'androidx.test:core:1.4.0' testImplementation 'junit:junit:4.13.2' - testImplementation 'org.robolectric:robolectric:4.6.1' + testImplementation 'org.robolectric:robolectric:4.7.2' } tasks.withType(SpotBugsTask) { diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 48ab22cb0..2f7674721 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -1,14 +1,13 @@ - + - - - + + + + + - - + android:theme="@style/Theme.App.Starting"> - + - + - - + android:theme="@style/AppTheme.NoActionBar"> - + android:theme="@style/AppTheme.NoActionBar"> + + android:windowSoftInputMode="stateHidden" /> - + android:windowSoftInputMode="stateHidden"> + + - - + - - - + android:theme="@style/AppTheme.NoActionBar" /> + android:windowSoftInputMode="stateHidden" /> + android:theme="@style/AppTheme.NoActionBar" /> + android:theme="@style/AppTheme.NoActionBar" /> + android:theme="@style/AppTheme.NoActionBar"> - - + + + + + + + android:grantUriPermissions="true"> + android:resource="@xml/file_provider_paths" /> @@ -128,4 +137,4 @@ - + \ No newline at end of file diff --git a/app/src/main/java/protect/card_locker/AboutActivity.java b/app/src/main/java/protect/card_locker/AboutActivity.java index 739d5ef24..2a56836f0 100644 --- a/app/src/main/java/protect/card_locker/AboutActivity.java +++ b/app/src/main/java/protect/card_locker/AboutActivity.java @@ -24,22 +24,19 @@ import androidx.appcompat.widget.Toolbar; import androidx.constraintlayout.widget.ConstraintLayout; import androidx.core.text.HtmlCompat; -public class AboutActivity extends CatimaAppCompatActivity implements View.OnClickListener -{ +public class AboutActivity extends CatimaAppCompatActivity implements View.OnClickListener { private static final String TAG = "Catima"; ConstraintLayout version_history, translate, license, repo, privacy, error, credits, rate; @Override - protected void onCreate(Bundle savedInstanceState) - { + protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setTitle(R.string.about); setContentView(R.layout.about_activity); Toolbar toolbar = findViewById(R.id.toolbar); setSupportActionBar(toolbar); ActionBar actionBar = getSupportActionBar(); - if(actionBar != null) - { + if (actionBar != null) { actionBar.setDisplayHomeAsUpEnabled(true); } @@ -59,7 +56,8 @@ public class AboutActivity extends CatimaAppCompatActivity implements View.OnCli contributors.append("
"); contributors.append(tmp); } - } catch (IOException ignored) {} + } catch (IOException ignored) { + } final List USED_LIBRARIES = new ArrayList<>(); USED_LIBRARIES.add(new ThirdPartyInfo("Color Picker", "https://github.com/jaredrummler/ColorPicker", "Apache 2.0")); @@ -73,14 +71,12 @@ public class AboutActivity extends CatimaAppCompatActivity implements View.OnCli USED_ASSETS.add(new ThirdPartyInfo("Android icons", "https://fonts.google.com/icons?selected=Material+Icons", "Apache 2.0")); StringBuilder libs = new StringBuilder().append("
"); - for (ThirdPartyInfo entry : USED_LIBRARIES) - { + for (ThirdPartyInfo entry : USED_LIBRARIES) { libs.append("
").append(entry.name()).append(" (").append(entry.license()).append(")"); } StringBuilder resources = new StringBuilder().append("
"); - for (ThirdPartyInfo entry : USED_ASSETS) - { + for (ThirdPartyInfo entry : USED_ASSETS) { resources.append("
").append(entry.name()).append(" (").append(entry.license()).append(")"); } @@ -88,13 +84,10 @@ public class AboutActivity extends CatimaAppCompatActivity implements View.OnCli int year = Calendar.getInstance().get(Calendar.YEAR); String version = "?"; - try - { + try { PackageInfo pi = getPackageManager().getPackageInfo(getPackageName(), 0); version = pi.versionName; - } - catch (PackageManager.NameNotFoundException e) - { + } catch (PackageManager.NameNotFoundException e) { Log.w(TAG, "Package name not found", e); } @@ -134,13 +127,13 @@ public class AboutActivity extends CatimaAppCompatActivity implements View.OnCli credits.setOnClickListener(view -> new AlertDialog.Builder(this) .setTitle(R.string.credits) .setMessage(contributorInfo.toString()) - .setPositiveButton(R.string.ok, (dialogInterface, i) -> {}) + .setPositiveButton(R.string.ok, (dialogInterface, i) -> { + }) .show()); } @Override - public boolean onOptionsItemSelected(MenuItem item) - { + public boolean onOptionsItemSelected(MenuItem item) { int id = item.getItemId(); if (id == android.R.id.home) { finish(); diff --git a/app/src/main/java/protect/card_locker/BarcodeSelectorActivity.java b/app/src/main/java/protect/card_locker/BarcodeSelectorActivity.java index 0251e9230..e33df1614 100644 --- a/app/src/main/java/protect/card_locker/BarcodeSelectorActivity.java +++ b/app/src/main/java/protect/card_locker/BarcodeSelectorActivity.java @@ -6,45 +6,37 @@ import android.os.Bundle; import android.os.Handler; import android.os.Looper; import android.util.Log; -import android.util.Pair; import android.view.MenuItem; import android.view.View; -import android.view.ViewTreeObserver; import android.widget.EditText; -import android.widget.ImageView; -import android.widget.TextView; +import android.widget.ListView; import android.widget.Toast; import com.google.zxing.BarcodeFormat; -import java.util.HashMap; -import java.util.Map; +import java.util.ArrayList; import androidx.appcompat.app.ActionBar; import androidx.appcompat.widget.Toolbar; -import protect.card_locker.async.TaskHandler; - /** * This activity is callable and will allow a user to enter * barcode data and generate all barcodes possible for * the data. The user may then select any barcode, where its * data and type will be returned to the caller. */ -public class BarcodeSelectorActivity extends CatimaAppCompatActivity { +public class BarcodeSelectorActivity extends CatimaAppCompatActivity implements BarcodeSelectorAdapter.BarcodeSelectorListener { private static final String TAG = "Catima"; // Result this activity will return public static final String BARCODE_CONTENTS = "contents"; public static final String BARCODE_FORMAT = "format"; - private Map> barcodeViewMap; - - final private TaskHandler mTasks = new TaskHandler(); - private final Handler typingDelayHandler = new Handler(Looper.getMainLooper()); public static final Integer INPUT_DELAY = 250; + private BarcodeSelectorAdapter mAdapter; + @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); @@ -57,21 +49,10 @@ public class BarcodeSelectorActivity extends CatimaAppCompatActivity { actionBar.setDisplayHomeAsUpEnabled(true); } - barcodeViewMap = new HashMap<>(); - barcodeViewMap.put(BarcodeFormat.AZTEC.name(), new Pair<>(R.id.aztecBarcode, R.id.aztecBarcodeText)); - barcodeViewMap.put(BarcodeFormat.CODE_39.name(), new Pair<>(R.id.code39Barcode, R.id.code39BarcodeText)); - barcodeViewMap.put(BarcodeFormat.CODE_128.name(), new Pair<>(R.id.code128Barcode, R.id.code128BarcodeText)); - barcodeViewMap.put(BarcodeFormat.CODABAR.name(), new Pair<>(R.id.codabarBarcode, R.id.codabarBarcodeText)); - barcodeViewMap.put(BarcodeFormat.DATA_MATRIX.name(), new Pair<>(R.id.datamatrixBarcode, R.id.datamatrixBarcodeText)); - barcodeViewMap.put(BarcodeFormat.EAN_8.name(), new Pair<>(R.id.ean8Barcode, R.id.ean8BarcodeText)); - barcodeViewMap.put(BarcodeFormat.EAN_13.name(), new Pair<>(R.id.ean13Barcode, R.id.ean13BarcodeText)); - barcodeViewMap.put(BarcodeFormat.ITF.name(), new Pair<>(R.id.itfBarcode, R.id.itfBarcodeText)); - barcodeViewMap.put(BarcodeFormat.PDF_417.name(), new Pair<>(R.id.pdf417Barcode, R.id.pdf417BarcodeText)); - barcodeViewMap.put(BarcodeFormat.QR_CODE.name(), new Pair<>(R.id.qrcodeBarcode, R.id.qrcodeBarcodeText)); - barcodeViewMap.put(BarcodeFormat.UPC_A.name(), new Pair<>(R.id.upcaBarcode, R.id.upcaBarcodeText)); - barcodeViewMap.put(BarcodeFormat.UPC_E.name(), new Pair<>(R.id.upceBarcode, R.id.upceBarcodeText)); - EditText cardId = findViewById(R.id.cardId); + ListView mBarcodeList = findViewById(R.id.barcodes); + mAdapter = new BarcodeSelectorAdapter(this, new ArrayList<>(), this); + mBarcodeList.setAdapter(mAdapter); cardId.addTextChangedListener(new SimpleTextWatcher() { @Override @@ -104,16 +85,13 @@ public class BarcodeSelectorActivity extends CatimaAppCompatActivity { } private void generateBarcodes(String value) { - // Attempt to stop any async tasks which may not have been started yet - // TODO this can be very much optimized by only generating Barcodes visible to the User - mTasks.flushTaskList(TaskHandler.TYPE.BARCODE, true, false, false); - // Update barcodes - for (Map.Entry> entry : barcodeViewMap.entrySet()) { - ImageView image = findViewById(entry.getValue().first); - TextView text = findViewById(entry.getValue().second); - createBarcodeOption(image, entry.getKey(), value, text); + ArrayList barcodes = new ArrayList<>(); + for (BarcodeFormat barcodeFormat : CatimaBarcode.barcodeFormats) { + CatimaBarcode catimaBarcode = CatimaBarcode.fromBarcode(barcodeFormat); + barcodes.add(new CatimaBarcodeWithValue(catimaBarcode, value)); } + mAdapter.setBarcodes(barcodes); } private void setButtonListener(final View button, final String cardId) { @@ -127,48 +105,6 @@ public class BarcodeSelectorActivity extends CatimaAppCompatActivity { }); } - private void createBarcodeOption(final ImageView image, final String formatType, final String cardId, final TextView text) { - final CatimaBarcode format = CatimaBarcode.fromName(formatType); - - image.setImageBitmap(null); - image.setOnClickListener(v -> { - Log.d(TAG, "Selected barcode type " + formatType); - - if (!((boolean) image.getTag())) { - Toast.makeText(BarcodeSelectorActivity.this, getString(R.string.wrongValueForBarcodeType), Toast.LENGTH_LONG).show(); - return; - } - - Intent result = new Intent(); - result.putExtra(BARCODE_FORMAT, formatType); - result.putExtra(BARCODE_CONTENTS, cardId); - BarcodeSelectorActivity.this.setResult(RESULT_OK, result); - finish(); - }); - - if (image.getHeight() == 0) { - // The size of the ImageView is not yet available as it has not - // yet been drawn. Wait for it to be drawn so the size is available. - image.getViewTreeObserver().addOnGlobalLayoutListener( - new ViewTreeObserver.OnGlobalLayoutListener() { - @Override - public void onGlobalLayout() { - Log.d(TAG, "Global layout finished, type: + " + formatType + ", width: " + image.getWidth()); - image.getViewTreeObserver().removeOnGlobalLayoutListener(this); - - Log.d(TAG, "Generating barcode for type " + formatType); - - BarcodeImageWriterTask barcodeWriter = new BarcodeImageWriterTask(getApplicationContext(), image, cardId, format, text, true, null); - mTasks.executeTask(TaskHandler.TYPE.BARCODE, barcodeWriter); - } - }); - } else { - Log.d(TAG, "Generating barcode for type " + formatType); - BarcodeImageWriterTask barcodeWriter = new BarcodeImageWriterTask(getApplicationContext(), image, cardId, format, text, true, null); - mTasks.executeTask(TaskHandler.TYPE.BARCODE, barcodeWriter); - } - } - @Override public boolean onOptionsItemSelected(MenuItem item) { if (item.getItemId() == android.R.id.home) { @@ -179,4 +115,26 @@ public class BarcodeSelectorActivity extends CatimaAppCompatActivity { return super.onOptionsItemSelected(item); } + + @Override + public void onRowClicked(int inputPosition, View view) { + CatimaBarcodeWithValue barcodeWithValue = mAdapter.getItem(inputPosition); + CatimaBarcode catimaBarcode = barcodeWithValue.catimaBarcode(); + + if (!mAdapter.isValid(view)) { + Toast.makeText(this, getString(R.string.wrongValueForBarcodeType), Toast.LENGTH_LONG).show(); + return; + } + + String barcodeFormat = catimaBarcode.format().name(); + String value = barcodeWithValue.value(); + + Log.d(TAG, "Selected barcode type " + barcodeFormat); + + Intent result = new Intent(); + result.putExtra(BARCODE_FORMAT, barcodeFormat); + result.putExtra(BARCODE_CONTENTS, value); + BarcodeSelectorActivity.this.setResult(RESULT_OK, result); + finish(); + } } diff --git a/app/src/main/java/protect/card_locker/BarcodeSelectorAdapter.java b/app/src/main/java/protect/card_locker/BarcodeSelectorAdapter.java new file mode 100644 index 000000000..ff0e223a4 --- /dev/null +++ b/app/src/main/java/protect/card_locker/BarcodeSelectorAdapter.java @@ -0,0 +1,102 @@ +package protect.card_locker; + +import android.content.Context; +import android.util.Log; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.view.ViewTreeObserver; +import android.widget.ArrayAdapter; +import android.widget.ImageView; +import android.widget.TextView; + +import java.util.ArrayList; + +import protect.card_locker.async.TaskHandler; + +public class BarcodeSelectorAdapter extends ArrayAdapter { + private static final String TAG = "Catima"; + + private final TaskHandler mTasks = new TaskHandler(); + private final BarcodeSelectorListener mListener; + + private static class ViewHolder { + ImageView image; + TextView text; + } + + public interface BarcodeSelectorListener { + void onRowClicked(int inputPosition, View view); + } + + public BarcodeSelectorAdapter(Context context, ArrayList barcodes, BarcodeSelectorListener barcodeSelectorListener) { + super(context, 0, barcodes); + mListener = barcodeSelectorListener; + } + + public void setBarcodes(ArrayList barcodes) { + clear(); + addAll(barcodes); + notifyDataSetChanged(); + mTasks.flushTaskList(TaskHandler.TYPE.BARCODE, true, false, false); + } + + @Override + public View getView(int position, View convertView, ViewGroup parent) { + CatimaBarcodeWithValue catimaBarcodeWithValue = getItem(position); + CatimaBarcode catimaBarcode = catimaBarcodeWithValue.catimaBarcode(); + String value = catimaBarcodeWithValue.value(); + + ViewHolder viewHolder; + if (convertView == null) { + viewHolder = new ViewHolder(); + LayoutInflater inflater = LayoutInflater.from(getContext()); + convertView = inflater.inflate(R.layout.barcode_layout, parent, false); + viewHolder.image = convertView.findViewById(R.id.barcodeImage); + viewHolder.text = convertView.findViewById(R.id.barcodeName); + convertView.setTag(viewHolder); + } else { + viewHolder = (ViewHolder) convertView.getTag(); + } + + createBarcodeOption(viewHolder.image, catimaBarcode.format().name(), value, viewHolder.text); + + View finalConvertView = convertView; + convertView.setOnClickListener(view -> mListener.onRowClicked(position, finalConvertView)); + + return convertView; + } + + public boolean isValid(View view) { + ViewHolder viewHolder = (ViewHolder) view.getTag(); + return viewHolder.image.getTag() != null && (boolean) viewHolder.image.getTag(); + } + + private void createBarcodeOption(final ImageView image, final String formatType, final String cardId, final TextView text) { + final CatimaBarcode format = CatimaBarcode.fromName(formatType); + + image.setImageBitmap(null); + + if (image.getHeight() == 0) { + // The size of the ImageView is not yet available as it has not + // yet been drawn. Wait for it to be drawn so the size is available. + image.getViewTreeObserver().addOnGlobalLayoutListener( + new ViewTreeObserver.OnGlobalLayoutListener() { + @Override + public void onGlobalLayout() { + Log.d(TAG, "Global layout finished, type: + " + formatType + ", width: " + image.getWidth()); + image.getViewTreeObserver().removeOnGlobalLayoutListener(this); + + Log.d(TAG, "Generating barcode for type " + formatType); + + BarcodeImageWriterTask barcodeWriter = new BarcodeImageWriterTask(getContext(), image, cardId, format, text, true, null); + mTasks.executeTask(TaskHandler.TYPE.BARCODE, barcodeWriter); + } + }); + } else { + Log.d(TAG, "Generating barcode for type " + formatType); + BarcodeImageWriterTask barcodeWriter = new BarcodeImageWriterTask(getContext(), image, cardId, format, text, true, null); + mTasks.executeTask(TaskHandler.TYPE.BARCODE, barcodeWriter); + } + } +} diff --git a/app/src/main/java/protect/card_locker/BaseCursorAdapter.java b/app/src/main/java/protect/card_locker/BaseCursorAdapter.java index a20041e1d..7c9d77899 100644 --- a/app/src/main/java/protect/card_locker/BaseCursorAdapter.java +++ b/app/src/main/java/protect/card_locker/BaseCursorAdapter.java @@ -4,14 +4,12 @@ import android.database.Cursor; import androidx.recyclerview.widget.RecyclerView; -public abstract class BaseCursorAdapter extends RecyclerView.Adapter -{ +public abstract class BaseCursorAdapter extends RecyclerView.Adapter { private Cursor mCursor; private boolean mDataValid; private int mRowIDColumn; - public BaseCursorAdapter(Cursor inputCursor) - { + public BaseCursorAdapter(Cursor inputCursor) { setHasStableIds(true); swapCursor(inputCursor); } @@ -19,15 +17,12 @@ public abstract class BaseCursorAdapter exten public abstract void onBindViewHolder(V inputHolder, Cursor inputCursor); @Override - public void onBindViewHolder(V inputHolder, int inputPosition) - { - if (!mDataValid) - { + public void onBindViewHolder(V inputHolder, int inputPosition) { + if (!mDataValid) { throw new IllegalStateException("Cannot bind view holder when cursor is in invalid state."); } - if (!mCursor.moveToPosition(inputPosition)) - { + if (!mCursor.moveToPosition(inputPosition)) { throw new IllegalStateException("Could not move cursor to position " + inputPosition + " when trying to bind view holder"); } @@ -35,49 +30,37 @@ public abstract class BaseCursorAdapter exten } @Override - public int getItemCount() - { - if (mDataValid) - { + public int getItemCount() { + if (mDataValid) { return mCursor.getCount(); - } - else - { + } else { return 0; } } @Override - public long getItemId(int inputPosition) - { - if (!mDataValid) - { + public long getItemId(int inputPosition) { + if (!mDataValid) { throw new IllegalStateException("Cannot lookup item id when cursor is in invalid state."); } - if (!mCursor.moveToPosition(inputPosition)) - { + if (!mCursor.moveToPosition(inputPosition)) { throw new IllegalStateException("Could not move cursor to position " + inputPosition + " when trying to get an item id"); } return mCursor.getLong(mRowIDColumn); } - public void swapCursor(Cursor inputCursor) - { - if (inputCursor == mCursor) - { + public void swapCursor(Cursor inputCursor) { + if (inputCursor == mCursor) { return; } - if (inputCursor != null) - { + if (inputCursor != null) { mCursor = inputCursor; mDataValid = true; notifyDataSetChanged(); - } - else - { + } else { notifyItemRangeRemoved(0, getItemCount()); mCursor = null; mRowIDColumn = -1; diff --git a/app/src/main/java/protect/card_locker/CardShortcutConfigure.java b/app/src/main/java/protect/card_locker/CardShortcutConfigure.java index 1b8d771a5..be9cc84d5 100644 --- a/app/src/main/java/protect/card_locker/CardShortcutConfigure.java +++ b/app/src/main/java/protect/card_locker/CardShortcutConfigure.java @@ -19,8 +19,7 @@ import androidx.recyclerview.widget.RecyclerView; /** * The configuration screen for creating a shortcut. */ -public class CardShortcutConfigure extends AppCompatActivity implements LoyaltyCardCursorAdapter.CardAdapterListener -{ +public class CardShortcutConfigure extends AppCompatActivity implements LoyaltyCardCursorAdapter.CardAdapterListener { static final String TAG = "Catima"; final DBHelper mDb = new DBHelper(this); diff --git a/app/src/main/java/protect/card_locker/CatimaBarcode.java b/app/src/main/java/protect/card_locker/CatimaBarcode.java index 2b295f5c5..c402f7991 100644 --- a/app/src/main/java/protect/card_locker/CatimaBarcode.java +++ b/app/src/main/java/protect/card_locker/CatimaBarcode.java @@ -8,33 +8,33 @@ import java.util.List; public class CatimaBarcode { public static final List barcodeFormats = Collections.unmodifiableList(Arrays.asList( - BarcodeFormat.AZTEC, - BarcodeFormat.CODE_39, - BarcodeFormat.CODE_128, - BarcodeFormat.CODABAR, - BarcodeFormat.DATA_MATRIX, - BarcodeFormat.EAN_8, - BarcodeFormat.EAN_13, - BarcodeFormat.ITF, - BarcodeFormat.PDF_417, - BarcodeFormat.QR_CODE, - BarcodeFormat.UPC_A, - BarcodeFormat.UPC_E + BarcodeFormat.AZTEC, + BarcodeFormat.CODE_39, + BarcodeFormat.CODE_128, + BarcodeFormat.CODABAR, + BarcodeFormat.DATA_MATRIX, + BarcodeFormat.EAN_8, + BarcodeFormat.EAN_13, + BarcodeFormat.ITF, + BarcodeFormat.PDF_417, + BarcodeFormat.QR_CODE, + BarcodeFormat.UPC_A, + BarcodeFormat.UPC_E )); public static final List barcodePrettyNames = Collections.unmodifiableList(Arrays.asList( - "Aztec", - "Code 39", - "Code 128", - "Codabar", - "Data Matrix", - "EAN 8", - "EAN 13", - "ITF", - "PDF 417", - "QR Code", - "UPC A", - "UPC E" + "Aztec", + "Code 39", + "Code 128", + "Codabar", + "Data Matrix", + "EAN 8", + "EAN 13", + "ITF", + "PDF 417", + "QR Code", + "UPC A", + "UPC E" )); private final BarcodeFormat mBarcodeFormat; @@ -63,7 +63,7 @@ public class CatimaBarcode { return barcodeFormats.contains(mBarcodeFormat); } - public boolean isSquare(){ + public boolean isSquare() { return mBarcodeFormat == BarcodeFormat.AZTEC || mBarcodeFormat == BarcodeFormat.DATA_MATRIX || mBarcodeFormat == BarcodeFormat.MAXICODE diff --git a/app/src/main/java/protect/card_locker/CatimaBarcodeWithValue.java b/app/src/main/java/protect/card_locker/CatimaBarcodeWithValue.java new file mode 100644 index 000000000..cd02ea3ee --- /dev/null +++ b/app/src/main/java/protect/card_locker/CatimaBarcodeWithValue.java @@ -0,0 +1,19 @@ +package protect.card_locker; + +public class CatimaBarcodeWithValue { + private final CatimaBarcode mCatimaBarcode; + private final String mValue; + + public CatimaBarcodeWithValue(CatimaBarcode catimaBarcode, String value) { + mCatimaBarcode = catimaBarcode; + mValue = value; + } + + public CatimaBarcode catimaBarcode() { + return mCatimaBarcode; + } + + public String value() { + return mValue; + } +} diff --git a/app/src/main/java/protect/card_locker/DBHelper.java b/app/src/main/java/protect/card_locker/DBHelper.java index 505f3be52..6b9aa515d 100644 --- a/app/src/main/java/protect/card_locker/DBHelper.java +++ b/app/src/main/java/protect/card_locker/DBHelper.java @@ -18,21 +18,18 @@ import java.util.Currency; import java.util.Date; import java.util.List; -public class DBHelper extends SQLiteOpenHelper -{ +public class DBHelper extends SQLiteOpenHelper { public static final String DATABASE_NAME = "Catima.db"; public static final int ORIGINAL_DATABASE_VERSION = 1; public static final int DATABASE_VERSION = 14; - public static class LoyaltyCardDbGroups - { + public static class LoyaltyCardDbGroups { public static final String TABLE = "groups"; public static final String ID = "_id"; public static final String ORDER = "orderId"; } - public static class LoyaltyCardDbIds - { + public static class LoyaltyCardDbIds { public static final String TABLE = "cards"; public static final String ID = "_id"; public static final String STORE = "store"; @@ -50,15 +47,13 @@ public class DBHelper extends SQLiteOpenHelper public static final String ZOOM_LEVEL = "zoomlevel"; } - public static class LoyaltyCardDbIdsGroups - { + public static class LoyaltyCardDbIdsGroups { public static final String TABLE = "cardsGroups"; public static final String cardID = "cardId"; public static final String groupID = "groupId"; } - public static class LoyaltyCardDbFTS - { + public static class LoyaltyCardDbFTS { public static final String TABLE = "fts"; public static final String ID = "rowid"; // This should NEVER be changed public static final String STORE = "store"; @@ -78,16 +73,14 @@ public class DBHelper extends SQLiteOpenHelper private Context mContext; - public DBHelper(Context context) - { + public DBHelper(Context context) { super(context, DATABASE_NAME, null, DATABASE_VERSION); mContext = context; } @Override - public void onCreate(SQLiteDatabase db) - { + public void onCreate(SQLiteDatabase db) { // create table for card groups db.execSQL("CREATE TABLE " + LoyaltyCardDbGroups.TABLE + "(" + LoyaltyCardDbGroups.ID + " TEXT primary key not null," + @@ -107,14 +100,14 @@ public class DBHelper extends SQLiteOpenHelper LoyaltyCardDbIds.BARCODE_ID + " TEXT," + LoyaltyCardDbIds.BARCODE_TYPE + " TEXT," + LoyaltyCardDbIds.STAR_STATUS + " INTEGER DEFAULT '0'," + - LoyaltyCardDbIds.LAST_USED + " INTEGER DEFAULT '0', "+ + LoyaltyCardDbIds.LAST_USED + " INTEGER DEFAULT '0', " + LoyaltyCardDbIds.ZOOM_LEVEL + " INTEGER DEFAULT '100' )"); // create associative table for cards in groups db.execSQL("CREATE TABLE " + LoyaltyCardDbIdsGroups.TABLE + "(" + LoyaltyCardDbIdsGroups.cardID + " INTEGER," + LoyaltyCardDbIdsGroups.groupID + " TEXT," + - "primary key (" + LoyaltyCardDbIdsGroups.cardID + "," + LoyaltyCardDbIdsGroups.groupID +"))"); + "primary key (" + LoyaltyCardDbIdsGroups.cardID + "," + LoyaltyCardDbIdsGroups.groupID + "))"); // create FTS search table db.execSQL("CREATE VIRTUAL TABLE " + LoyaltyCardDbFTS.TABLE + " USING fts4(" + @@ -124,67 +117,57 @@ public class DBHelper extends SQLiteOpenHelper @Override - public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) - { - if(oldVersion < 2 && newVersion >= 2) - { + public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) { + if (oldVersion < 2 && newVersion >= 2) { db.execSQL("ALTER TABLE " + LoyaltyCardDbIds.TABLE + " ADD COLUMN " + LoyaltyCardDbIds.NOTE + " TEXT not null default ''"); } - if(oldVersion < 3 && newVersion >= 3) - { + if (oldVersion < 3 && newVersion >= 3) { db.execSQL("ALTER TABLE " + LoyaltyCardDbIds.TABLE + " ADD COLUMN " + LoyaltyCardDbIds.HEADER_COLOR + " INTEGER"); db.execSQL("ALTER TABLE " + LoyaltyCardDbIds.TABLE + " ADD COLUMN " + LoyaltyCardDbIds.HEADER_TEXT_COLOR + " INTEGER"); } - if(oldVersion < 4 && newVersion >= 4) - { + if (oldVersion < 4 && newVersion >= 4) { db.execSQL("ALTER TABLE " + LoyaltyCardDbIds.TABLE + " ADD COLUMN " + LoyaltyCardDbIds.STAR_STATUS + " INTEGER DEFAULT '0'"); } - if(oldVersion < 5 && newVersion >= 5) - { + if (oldVersion < 5 && newVersion >= 5) { db.execSQL("CREATE TABLE " + LoyaltyCardDbGroups.TABLE + "(" + LoyaltyCardDbGroups.ID + " TEXT primary key not null)"); db.execSQL("CREATE TABLE " + LoyaltyCardDbIdsGroups.TABLE + "(" + LoyaltyCardDbIdsGroups.cardID + " INTEGER," + LoyaltyCardDbIdsGroups.groupID + " TEXT," + - "primary key (" + LoyaltyCardDbIdsGroups.cardID + "," + LoyaltyCardDbIdsGroups.groupID +"))"); + "primary key (" + LoyaltyCardDbIdsGroups.cardID + "," + LoyaltyCardDbIdsGroups.groupID + "))"); } - if(oldVersion < 6 && newVersion >= 6) - { + if (oldVersion < 6 && newVersion >= 6) { db.execSQL("ALTER TABLE " + LoyaltyCardDbGroups.TABLE + " ADD COLUMN " + LoyaltyCardDbGroups.ORDER + " INTEGER DEFAULT '0'"); } - if(oldVersion < 7 && newVersion >= 7) - { + if (oldVersion < 7 && newVersion >= 7) { db.execSQL("ALTER TABLE " + LoyaltyCardDbIds.TABLE + " ADD COLUMN " + LoyaltyCardDbIds.EXPIRY + " INTEGER"); } - if(oldVersion < 8 && newVersion >= 8) - { + if (oldVersion < 8 && newVersion >= 8) { db.execSQL("ALTER TABLE " + LoyaltyCardDbIds.TABLE + " ADD COLUMN " + LoyaltyCardDbIds.BALANCE + " TEXT not null DEFAULT '0'"); db.execSQL("ALTER TABLE " + LoyaltyCardDbIds.TABLE + " ADD COLUMN " + LoyaltyCardDbIds.BALANCE_TYPE + " TEXT"); } - if(oldVersion < 9 && newVersion >= 9) - { + if (oldVersion < 9 && newVersion >= 9) { db.execSQL("ALTER TABLE " + LoyaltyCardDbIds.TABLE + " ADD COLUMN " + LoyaltyCardDbIds.BARCODE_ID + " TEXT"); } - if(oldVersion < 10 && newVersion >= 10) - { + if (oldVersion < 10 && newVersion >= 10) { // SQLite doesn't support modify column // So we need to create a temp column to make barcode type nullable // Let's drop header text colour too while we're at it @@ -277,14 +260,12 @@ public class DBHelper extends SQLiteOpenHelper db.endTransaction(); } - if(oldVersion < 11 && newVersion >= 11) - { + if (oldVersion < 11 && newVersion >= 11) { db.execSQL("ALTER TABLE " + LoyaltyCardDbIds.TABLE + " ADD COLUMN " + LoyaltyCardDbIds.LAST_USED + " INTEGER DEFAULT '0'"); } - if(oldVersion < 12 && newVersion >= 12) - { + if (oldVersion < 12 && newVersion >= 12) { db.execSQL("CREATE VIRTUAL TABLE " + LoyaltyCardDbFTS.TABLE + " USING fts4(" + LoyaltyCardDbFTS.STORE + ", " + LoyaltyCardDbFTS.NOTE + ", " + "tokenize=unicode61);"); @@ -301,8 +282,7 @@ public class DBHelper extends SQLiteOpenHelper } } - if(oldVersion < 13 && newVersion >= 13) - { + if (oldVersion < 13 && newVersion >= 13) { db.execSQL("DELETE FROM " + LoyaltyCardDbFTS.TABLE + ";"); Cursor cursor = db.rawQuery("SELECT * FROM " + LoyaltyCardDbIds.TABLE + ";", null, null); @@ -323,7 +303,7 @@ public class DBHelper extends SQLiteOpenHelper cursor.close(); } - if(oldVersion < 14 && newVersion >= 14){ + if (oldVersion < 14 && newVersion >= 14) { db.execSQL("ALTER TABLE " + LoyaltyCardDbIds.TABLE + " ADD COLUMN " + LoyaltyCardDbIds.ZOOM_LEVEL + " INTEGER DEFAULT '100' "); } @@ -374,8 +354,7 @@ public class DBHelper extends SQLiteOpenHelper final BigDecimal balance, final Currency balanceType, final String cardId, final String barcodeId, final CatimaBarcode barcodeType, final Integer headerColor, - final int starStatus, final Long lastUsed) - { + final int starStatus, final Long lastUsed) { SQLiteDatabase db = getWritableDatabase(); db.beginTransaction(); @@ -408,8 +387,7 @@ public class DBHelper extends SQLiteOpenHelper final Currency balanceType, final String cardId, final String barcodeId, final CatimaBarcode barcodeType, final Integer headerColor, final int starStatus, - final Long lastUsed) - { + final Long lastUsed) { db.beginTransaction(); // Card @@ -441,8 +419,7 @@ public class DBHelper extends SQLiteOpenHelper final Currency balanceType, final String cardId, final String barcodeId, final CatimaBarcode barcodeType, final Integer headerColor, final int starStatus, - final Long lastUsed) - { + final Long lastUsed) { db.beginTransaction(); // Card @@ -474,8 +451,7 @@ public class DBHelper extends SQLiteOpenHelper final Date expiry, final BigDecimal balance, final Currency balanceType, final String cardId, final String barcodeId, final CatimaBarcode barcodeType, - final Integer headerColor) - { + final Integer headerColor) { SQLiteDatabase db = getWritableDatabase(); db.beginTransaction(); @@ -502,11 +478,10 @@ public class DBHelper extends SQLiteOpenHelper return (rowsUpdated == 1); } - public boolean updateLoyaltyCardStarStatus(final int id, final int starStatus) - { + public boolean updateLoyaltyCardStarStatus(final int id, final int starStatus) { SQLiteDatabase db = getWritableDatabase(); ContentValues contentValues = new ContentValues(); - contentValues.put(LoyaltyCardDbIds.STAR_STATUS,starStatus); + contentValues.put(LoyaltyCardDbIds.STAR_STATUS, starStatus); int rowsUpdated = db.update(LoyaltyCardDbIds.TABLE, contentValues, whereAttrs(LoyaltyCardDbIds.ID), withArgs(id)); @@ -523,27 +498,25 @@ public class DBHelper extends SQLiteOpenHelper return (rowsUpdated == 1); } - public boolean updateLoyaltyCardZoomLevel(int loyaltyCardId, int zoomLevel){ + public boolean updateLoyaltyCardZoomLevel(int loyaltyCardId, int zoomLevel) { SQLiteDatabase db = getWritableDatabase(); ContentValues contentValues = new ContentValues(); - contentValues.put(LoyaltyCardDbIds.ZOOM_LEVEL,zoomLevel); - Log.d("updateLoyaltyCardZLevel","Card Id = "+loyaltyCardId+" Zoom level= "+zoomLevel); - int rowsUpdated = db.update(LoyaltyCardDbIds.TABLE,contentValues, + contentValues.put(LoyaltyCardDbIds.ZOOM_LEVEL, zoomLevel); + Log.d("updateLoyaltyCardZLevel", "Card Id = " + loyaltyCardId + " Zoom level= " + zoomLevel); + int rowsUpdated = db.update(LoyaltyCardDbIds.TABLE, contentValues, whereAttrs(LoyaltyCardDbIds.ID), withArgs(loyaltyCardId)); - Log.d("updateLoyaltyCardZLevel","Rows changed = "+rowsUpdated); + Log.d("updateLoyaltyCardZLevel", "Rows changed = " + rowsUpdated); return (rowsUpdated == 1); } - public LoyaltyCard getLoyaltyCard(final int id) - { + public LoyaltyCard getLoyaltyCard(final int id) { SQLiteDatabase db = getReadableDatabase(); Cursor data = db.query(LoyaltyCardDbIds.TABLE, null, whereAttrs(LoyaltyCardDbIds.ID), withArgs(id), null, null, null); LoyaltyCard card = null; - if(data.getCount() == 1) - { + if (data.getCount() == 1) { data.moveToFirst(); card = LoyaltyCard.toLoyaltyCard(data); } @@ -553,8 +526,7 @@ public class DBHelper extends SQLiteOpenHelper return card; } - public List getLoyaltyCardGroups(final int id) - { + public List getLoyaltyCardGroups(final int id) { SQLiteDatabase db = getReadableDatabase(); Cursor data = db.rawQuery("select * from " + LoyaltyCardDbGroups.TABLE + " g " + " LEFT JOIN " + LoyaltyCardDbIdsGroups.TABLE + " ig ON ig." + LoyaltyCardDbIdsGroups.groupID + " = g." + LoyaltyCardDbGroups.ID + @@ -579,8 +551,7 @@ public class DBHelper extends SQLiteOpenHelper return groups; } - public void setLoyaltyCardGroups(final int id, List groups) - { + public void setLoyaltyCardGroups(final int id, List groups) { SQLiteDatabase db = getWritableDatabase(); // First delete lookup table entries associated with this card @@ -597,8 +568,7 @@ public class DBHelper extends SQLiteOpenHelper } } - public void setLoyaltyCardGroups(final SQLiteDatabase db, final int id, List groups) - { + public void setLoyaltyCardGroups(final SQLiteDatabase db, final int id, List groups) { // First delete lookup table entries associated with this card db.delete(LoyaltyCardDbIdsGroups.TABLE, whereAttrs(LoyaltyCardDbIdsGroups.cardID), @@ -613,8 +583,7 @@ public class DBHelper extends SQLiteOpenHelper } } - public boolean deleteLoyaltyCard(final int id) - { + public boolean deleteLoyaltyCard(final int id) { SQLiteDatabase db = getWritableDatabase(); // Delete card int rowsDeleted = db.delete(LoyaltyCardDbIds.TABLE, @@ -632,18 +601,18 @@ public class DBHelper extends SQLiteOpenHelper withArgs(id)); // Also wipe card images associated with this card - try { - Utils.saveCardImage(mContext, null, id, true); - Utils.saveCardImage(mContext, null, id, false); - } catch (FileNotFoundException e) { - e.printStackTrace(); + for (ImageLocationType imageLocationType : ImageLocationType.values()) { + try { + Utils.saveCardImage(mContext, null, id, imageLocationType); + } catch (FileNotFoundException e) { + e.printStackTrace(); + } } return (rowsDeleted == 1); } - public Cursor getLoyaltyCardCursor() - { + public Cursor getLoyaltyCardCursor() { // An empty string will match everything return getLoyaltyCardCursor(""); } @@ -654,8 +623,7 @@ public class DBHelper extends SQLiteOpenHelper * @param filter * @return Cursor */ - public Cursor getLoyaltyCardCursor(final String filter) - { + public Cursor getLoyaltyCardCursor(final String filter) { return getLoyaltyCardCursor(filter, null); } @@ -666,8 +634,7 @@ public class DBHelper extends SQLiteOpenHelper * @param group * @return Cursor */ - public Cursor getLoyaltyCardCursor(final String filter, Group group) - { + public Cursor getLoyaltyCardCursor(final String filter, Group group) { return getLoyaltyCardCursor(filter, group, LoyaltyCardOrder.Alpha, LoyaltyCardOrderDirection.Ascending); } @@ -715,7 +682,7 @@ public class DBHelper extends SQLiteOpenHelper " (CASE WHEN " + LoyaltyCardDbIds.TABLE + "." + orderField + " IS NULL THEN 1 ELSE 0 END), " + LoyaltyCardDbIds.TABLE + "." + orderField + " COLLATE NOCASE " + getDbDirection(order, direction) + ", " + LoyaltyCardDbIds.TABLE + "." + LoyaltyCardDbIds.STORE + " COLLATE NOCASE ASC " + - limitString, filter.trim().isEmpty() ? null : new String[] { TextUtils.join("* ", filter.split(" ")) + '*' }, null); + limitString, filter.trim().isEmpty() ? null : new String[]{TextUtils.join("* ", filter.split(" ")) + '*'}, null); } /** @@ -723,8 +690,7 @@ public class DBHelper extends SQLiteOpenHelper * * @return Integer */ - public int getLoyaltyCardCount() - { + public int getLoyaltyCardCount() { SQLiteDatabase db = getReadableDatabase(); return (int) DatabaseUtils.queryNumEntries(db, LoyaltyCardDbIds.TABLE); } @@ -734,8 +700,7 @@ public class DBHelper extends SQLiteOpenHelper * * @return Cursor */ - public Cursor getGroupCursor() - { + public Cursor getGroupCursor() { SQLiteDatabase db = getReadableDatabase(); return db.rawQuery("select * from " + LoyaltyCardDbGroups.TABLE + @@ -761,13 +726,11 @@ public class DBHelper extends SQLiteOpenHelper return groups; } - public void reorderGroups(final List groups) - { + public void reorderGroups(final List groups) { Integer order = 0; SQLiteDatabase db = getWritableDatabase(); - for (Group group : groups) - { + for (Group group : groups) { ContentValues contentValues = new ContentValues(); contentValues.put(LoyaltyCardDbGroups.ORDER, order); @@ -779,15 +742,13 @@ public class DBHelper extends SQLiteOpenHelper } } - public Group getGroup(final String groupName) - { + public Group getGroup(final String groupName) { SQLiteDatabase db = getReadableDatabase(); Cursor data = db.query(LoyaltyCardDbGroups.TABLE, null, whereAttrs(LoyaltyCardDbGroups.ID), withArgs(groupName), null, null, null); Group group = null; - if(data.getCount() == 1) - { + if (data.getCount() == 1) { data.moveToFirst(); group = Group.toGroup(data); } @@ -796,14 +757,12 @@ public class DBHelper extends SQLiteOpenHelper return group; } - public int getGroupCount() - { + public int getGroupCount() { SQLiteDatabase db = getReadableDatabase(); return (int) DatabaseUtils.queryNumEntries(db, LoyaltyCardDbGroups.TABLE); } - public List getGroupCardIds(final String groupName) - { + public List getGroupCardIds(final String groupName) { SQLiteDatabase db = getReadableDatabase(); Cursor data = db.query(LoyaltyCardDbIdsGroups.TABLE, withArgs(LoyaltyCardDbIdsGroups.cardID), whereAttrs(LoyaltyCardDbIdsGroups.groupID), withArgs(groupName), null, null, null); @@ -824,8 +783,7 @@ public class DBHelper extends SQLiteOpenHelper return cardIds; } - public long insertGroup(final String name) - { + public long insertGroup(final String name) { if (name.isEmpty()) return -1; SQLiteDatabase db = getWritableDatabase(); @@ -835,8 +793,7 @@ public class DBHelper extends SQLiteOpenHelper return db.insert(LoyaltyCardDbGroups.TABLE, null, contentValues); } - public boolean insertGroup(final SQLiteDatabase db, final String name) - { + public boolean insertGroup(final SQLiteDatabase db, final String name) { ContentValues contentValues = new ContentValues(); contentValues.put(LoyaltyCardDbGroups.ID, name); contentValues.put(LoyaltyCardDbGroups.ORDER, getGroupCount()); @@ -844,8 +801,7 @@ public class DBHelper extends SQLiteOpenHelper return newId != -1; } - public boolean updateGroup(final String groupName, final String newName) - { + public boolean updateGroup(final String groupName, final String newName) { if (newName.isEmpty()) return false; boolean success = false; @@ -881,8 +837,7 @@ public class DBHelper extends SQLiteOpenHelper return success; } - public boolean deleteGroup(final String groupName) - { + public boolean deleteGroup(final String groupName) { boolean success = false; SQLiteDatabase db = getWritableDatabase(); @@ -913,8 +868,7 @@ public class DBHelper extends SQLiteOpenHelper return success; } - public int getGroupCardCount(final String groupName) - { + public int getGroupCardCount(final String groupName) { SQLiteDatabase db = getReadableDatabase(); return (int) DatabaseUtils.queryNumEntries(db, LoyaltyCardDbIdsGroups.TABLE, diff --git a/app/src/main/java/protect/card_locker/FormatException.java b/app/src/main/java/protect/card_locker/FormatException.java index db23ebfd4..a44286a80 100644 --- a/app/src/main/java/protect/card_locker/FormatException.java +++ b/app/src/main/java/protect/card_locker/FormatException.java @@ -5,15 +5,12 @@ package protect.card_locker; * encountered with the format of data being * imported or exported. */ -public class FormatException extends Exception -{ - public FormatException(String message) - { +public class FormatException extends Exception { + public FormatException(String message) { super(message); } - public FormatException(String message, Exception rootCause) - { + public FormatException(String message, Exception rootCause) { super(message, rootCause); } } diff --git a/app/src/main/java/protect/card_locker/Group.java b/app/src/main/java/protect/card_locker/Group.java index 1d5de8603..90d8da69d 100644 --- a/app/src/main/java/protect/card_locker/Group.java +++ b/app/src/main/java/protect/card_locker/Group.java @@ -2,8 +2,9 @@ package protect.card_locker; import android.database.Cursor; -public class Group -{ +import androidx.annotation.Nullable; + +public class Group { public final String _id; public final int order; @@ -12,11 +13,28 @@ public class Group this.order = order; } - public static Group toGroup(Cursor cursor) - { + public static Group toGroup(Cursor cursor) { String _id = cursor.getString(cursor.getColumnIndexOrThrow(DBHelper.LoyaltyCardDbGroups.ID)); int order = cursor.getInt(cursor.getColumnIndexOrThrow(DBHelper.LoyaltyCardDbGroups.ORDER)); return new Group(_id, order); } + + @Override + public boolean equals(@Nullable Object obj) { + if (obj == null) { + return false; + } + if (!(obj instanceof Group)) { + return false; + } + Group anotherGroup = (Group) obj; + return _id.equals(anotherGroup._id) && order == anotherGroup.order; + } + + @Override + public int hashCode() { + String combined = _id + "_" + order; + return combined.hashCode(); + } } diff --git a/app/src/main/java/protect/card_locker/GroupCursorAdapter.java b/app/src/main/java/protect/card_locker/GroupCursorAdapter.java index 1e89bb091..05cc1d9f6 100644 --- a/app/src/main/java/protect/card_locker/GroupCursorAdapter.java +++ b/app/src/main/java/protect/card_locker/GroupCursorAdapter.java @@ -12,8 +12,7 @@ import androidx.appcompat.widget.AppCompatImageButton; import androidx.recyclerview.widget.RecyclerView; import protect.card_locker.preferences.Settings; -class GroupCursorAdapter extends BaseCursorAdapter -{ +class GroupCursorAdapter extends BaseCursorAdapter { Settings mSettings; private Cursor mCursor; private final Context mContext; @@ -39,14 +38,12 @@ class GroupCursorAdapter extends BaseCursorAdapter mListener.onMoveDownButtonClicked(inputHolder.itemView)); inputHolder.mMoveUp.setOnClickListener(view -> mListener.onMoveUpButtonClicked(inputHolder.itemView)); inputHolder.mEdit.setOnClickListener(view -> mListener.onEditButtonClicked(inputHolder.itemView)); inputHolder.mDelete.setOnClickListener(view -> mListener.onDeleteButtonClicked(inputHolder.itemView)); } - public interface GroupAdapterListener - { + public interface GroupAdapterListener { void onMoveDownButtonClicked(View view); + void onMoveUpButtonClicked(View view); + void onEditButtonClicked(View view); + void onDeleteButtonClicked(View view); } - public static class GroupListItemViewHolder extends RecyclerView.ViewHolder - { + public static class GroupListItemViewHolder extends RecyclerView.ViewHolder { public TextView mName, mCardCount; public AppCompatImageButton mMoveUp, mMoveDown, mEdit, mDelete; diff --git a/app/src/main/java/protect/card_locker/ImageLocationType.java b/app/src/main/java/protect/card_locker/ImageLocationType.java new file mode 100644 index 000000000..457a03bdd --- /dev/null +++ b/app/src/main/java/protect/card_locker/ImageLocationType.java @@ -0,0 +1,7 @@ +package protect.card_locker; + +public enum ImageLocationType { + front, + back, + icon +} diff --git a/app/src/main/java/protect/card_locker/ImportExportActivity.java b/app/src/main/java/protect/card_locker/ImportExportActivity.java index 444cc2d9e..b48090c3a 100644 --- a/app/src/main/java/protect/card_locker/ImportExportActivity.java +++ b/app/src/main/java/protect/card_locker/ImportExportActivity.java @@ -10,14 +10,20 @@ import android.os.Bundle; import android.text.InputType; import android.util.Log; import android.view.MenuItem; -import android.view.View; import android.view.ViewGroup; import android.widget.Button; import android.widget.EditText; import android.widget.FrameLayout; import android.widget.Toast; -import java.io.File; +import androidx.activity.result.ActivityResultLauncher; +import androidx.activity.result.contract.ActivityResultContracts; +import androidx.appcompat.app.ActionBar; +import androidx.appcompat.app.AlertDialog; +import androidx.appcompat.widget.Toolbar; +import androidx.core.app.ActivityCompat; +import androidx.core.content.ContextCompat; + import java.io.FileInputStream; import java.io.FileOutputStream; import java.io.IOException; @@ -26,12 +32,6 @@ import java.io.OutputStream; import java.util.ArrayList; import java.util.List; -import androidx.appcompat.app.ActionBar; -import androidx.appcompat.app.AlertDialog; -import androidx.appcompat.widget.Toolbar; -import androidx.core.app.ActivityCompat; -import androidx.core.content.ContextCompat; - import protect.card_locker.async.TaskHandler; import protect.card_locker.importexport.DataFormat; import protect.card_locker.importexport.ImportExportResult; @@ -40,8 +40,6 @@ public class ImportExportActivity extends CatimaAppCompatActivity { private static final String TAG = "Catima"; private static final int PERMISSIONS_EXTERNAL_STORAGE = 1; - private static final int CHOOSE_EXPORT_LOCATION = 2; - private static final int IMPORT = 3; private ImportExportTask importExporter; @@ -50,6 +48,10 @@ public class ImportExportActivity extends CatimaAppCompatActivity { private DataFormat importDataFormat; private String exportPassword; + private ActivityResultLauncher fileCreateLauncher; + private ActivityResultLauncher fileOpenLauncher; + private ActivityResultLauncher filePickerLauncher; + final private TaskHandler mTasks = new TaskHandler(); @Override @@ -77,6 +79,49 @@ public class ImportExportActivity extends CatimaAppCompatActivity { PERMISSIONS_EXTERNAL_STORAGE); } + // would use ActivityResultContracts.CreateDocument() but mime type cannot be set + fileCreateLauncher = registerForActivityResult(new ActivityResultContracts.StartActivityForResult(), result -> { + Intent intent = result.getData(); + if (intent == null) { + Log.e(TAG, "Activity returned NULL data"); + return; + } + Uri uri = intent.getData(); + if (uri == null) { + Log.e(TAG, "Activity returned NULL uri"); + return; + } + 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(ImportExportResult.GenericFailure, uri); + } + + }); + fileOpenLauncher = registerForActivityResult(new ActivityResultContracts.GetContent(), result -> { + if (result == null) { + Log.e(TAG, "Activity returned NULL data"); + return; + } + openFileForImport(result, null); + }); + filePickerLauncher = registerForActivityResult(new ActivityResultContracts.StartActivityForResult(), result -> { + Intent intent = result.getData(); + if (intent == null) { + Log.e(TAG, "Activity returned NULL data"); + return; + } + Uri uri = intent.getData(); + if (uri == null) { + Log.e(TAG, "Activity returned NULL uri"); + return; + } + openFileForImport(intent.getData(), null); + }); + // Check that there is a file manager available final Intent intentCreateDocumentAction = new Intent(Intent.ACTION_CREATE_DOCUMENT); intentCreateDocumentAction.addCategory(Intent.CATEGORY_OPENABLE); @@ -84,60 +129,57 @@ public class ImportExportActivity extends CatimaAppCompatActivity { intentCreateDocumentAction.putExtra(Intent.EXTRA_TITLE, "catima.zip"); Button exportButton = findViewById(R.id.exportButton); - exportButton.setOnClickListener(new View.OnClickListener() { - @Override - public void onClick(View v) { - AlertDialog.Builder builder = new AlertDialog.Builder(ImportExportActivity.this); - builder.setTitle(R.string.exportPassword); + exportButton.setOnClickListener(v -> { + AlertDialog.Builder builder = new AlertDialog.Builder(ImportExportActivity.this); + builder.setTitle(R.string.exportPassword); - FrameLayout container = new FrameLayout(ImportExportActivity.this); - FrameLayout.LayoutParams params = new FrameLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT); - params.leftMargin = 50; - params.rightMargin = 50; + FrameLayout container = new FrameLayout(ImportExportActivity.this); + FrameLayout.LayoutParams params = new FrameLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT); + params.leftMargin = 50; + params.rightMargin = 50; - final EditText input = new EditText(ImportExportActivity.this); - input.setInputType(InputType.TYPE_CLASS_TEXT | InputType.TYPE_TEXT_VARIATION_PASSWORD); - input.setLayoutParams(params); - input.setHint(R.string.exportPasswordHint); + final EditText input = new EditText(ImportExportActivity.this); + input.setInputType(InputType.TYPE_CLASS_TEXT | InputType.TYPE_TEXT_VARIATION_PASSWORD); + input.setLayoutParams(params); + input.setHint(R.string.exportPasswordHint); - container.addView(input); - builder.setView(container); - builder.setPositiveButton(R.string.ok, (dialogInterface, i) -> { - exportPassword = input.getText().toString(); - chooseFileWithIntent(intentCreateDocumentAction, CHOOSE_EXPORT_LOCATION); - }); - builder.setNegativeButton(R.string.cancel, (dialogInterface, i) -> dialogInterface.cancel()); - builder.show(); + container.addView(input); + builder.setView(container); + builder.setPositiveButton(R.string.ok, (dialogInterface, i) -> { + exportPassword = input.getText().toString(); + try { + fileCreateLauncher.launch(intentCreateDocumentAction); + } catch (ActivityNotFoundException e) { + Toast.makeText(getApplicationContext(), R.string.failedOpeningFileManager, Toast.LENGTH_LONG).show(); + Log.e(TAG, "No activity found to handle intent", e); + } + }); + builder.setNegativeButton(R.string.cancel, (dialogInterface, i) -> dialogInterface.cancel()); + builder.show(); - } }); // Check that there is a file manager available - final Intent intentGetContentAction = new Intent(Intent.ACTION_GET_CONTENT); - intentGetContentAction.addCategory(Intent.CATEGORY_OPENABLE); - intentGetContentAction.setType("*/*"); - Button importFilesystem = findViewById(R.id.importOptionFilesystemButton); - importFilesystem.setOnClickListener(new View.OnClickListener() { - @Override - public void onClick(View v) { - chooseImportType(intentGetContentAction); - } - }); + importFilesystem.setOnClickListener(v -> chooseImportType(false)); // Check that there is an app that data can be imported from - final Intent intentPickAction = new Intent(Intent.ACTION_PICK); - Button importApplication = findViewById(R.id.importOptionApplicationButton); - importApplication.setOnClickListener(new View.OnClickListener() { - @Override - public void onClick(View v) { - chooseImportType(intentPickAction); - } - }); + importApplication.setOnClickListener(v -> chooseImportType(true)); } - private void chooseImportType(Intent baseIntent) { + 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(ImportExportResult.GenericFailure, uri, importDataFormat); + } + } + + private void chooseImportType(boolean choosePicker) { List betaImportOptions = new ArrayList<>(); betaImportOptions.add("Fidme"); betaImportOptions.add("Stocard"); @@ -195,7 +237,17 @@ public class ImportExportActivity extends CatimaAppCompatActivity { .setPositiveButton(R.string.ok, new DialogInterface.OnClickListener() { @Override public void onClick(DialogInterface dialog, int which) { - chooseFileWithIntent(baseIntent, IMPORT); + 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) @@ -298,7 +350,7 @@ public class ImportExportActivity extends CatimaAppCompatActivity { builder.setView(input); builder.setPositiveButton(R.string.ok, (dialogInterface, i) -> { - activityResultParser(IMPORT, RESULT_OK, uri, input.getText().toString().toCharArray()); + openFileForImport(uri, input.getText().toString().toCharArray()); }); builder.setNegativeButton(R.string.cancel, (dialogInterface, i) -> dialogInterface.cancel()); @@ -374,69 +426,4 @@ public class ImportExportActivity extends CatimaAppCompatActivity { builder.create().show(); } - - private void chooseFileWithIntent(Intent intent, int requestCode) { - try { - startActivityForResult(intent, requestCode); - } catch (ActivityNotFoundException e) { - Toast.makeText(getApplicationContext(), R.string.failedOpeningFileManager, Toast.LENGTH_LONG).show(); - Log.e(TAG, "No activity found to handle intent", e); - } - } - - private void activityResultParser(int requestCode, int resultCode, Uri uri, char[] password) { - if (resultCode != RESULT_OK) { - Log.w(TAG, "Failed onActivityResult(), result=" + resultCode); - return; - } - - if (uri == null) { - Log.e(TAG, "Activity returned a NULL URI"); - return; - } - - try { - if (requestCode == CHOOSE_EXPORT_LOCATION) { - - OutputStream writer; - if (uri.getScheme() != null) { - writer = getContentResolver().openOutputStream(uri); - } else { - writer = new FileOutputStream(new File(uri.toString())); - } - Log.e(TAG, "Starting file export with: " + uri.toString()); - startExport(writer, uri, exportPassword.toCharArray(), true); - } else { - InputStream reader; - if (uri.getScheme() != null) { - reader = getContentResolver().openInputStream(uri); - } else { - reader = new FileInputStream(new File(uri.toString())); - } - - Log.e(TAG, "Starting file import with: " + uri.toString()); - - startImport(reader, uri, importDataFormat, password, true); - } - } catch (IOException e) { - Log.e(TAG, "Failed to import/export file: " + uri.toString(), e); - if (requestCode == CHOOSE_EXPORT_LOCATION) { - onExportComplete(ImportExportResult.GenericFailure, uri); - } else { - onImportComplete(ImportExportResult.GenericFailure, uri, importDataFormat); - } - } - } - - @Override - protected void onActivityResult(int requestCode, int resultCode, Intent data) { - super.onActivityResult(requestCode, resultCode, data); - - if (data == null) { - Log.e(TAG, "Activity returned NULL data"); - return; - } - - activityResultParser(requestCode, resultCode, data.getData(), null); - } } \ No newline at end of file diff --git a/app/src/main/java/protect/card_locker/ImportURIHelper.java b/app/src/main/java/protect/card_locker/ImportURIHelper.java index 812d5665c..4cb476708 100644 --- a/app/src/main/java/protect/card_locker/ImportURIHelper.java +++ b/app/src/main/java/protect/card_locker/ImportURIHelper.java @@ -4,8 +4,6 @@ import android.content.Context; import android.content.Intent; import android.net.Uri; -import com.google.zxing.BarcodeFormat; - import java.io.InvalidObjectException; import java.io.UnsupportedEncodingException; import java.math.BigDecimal; @@ -57,7 +55,7 @@ public class ImportURIHelper { } public LoyaltyCard parse(Uri uri) throws InvalidObjectException { - if(!isImportUri(uri)) { + if (!isImportUri(uri)) { throw new InvalidObjectException("Not an import URI"); } @@ -92,37 +90,33 @@ public class ImportURIHelper { String note = kv.get(NOTE); String cardId = kv.get(CARD_ID); String barcodeId = kv.get(BARCODE_ID); - if (store == null || note == null || cardId == null) throw new InvalidObjectException("Not a valid import URI: " + uri.toString()); + if (store == null || note == null || cardId == null) + throw new InvalidObjectException("Not a valid import URI: " + uri.toString()); String unparsedBarcodeType = kv.get(BARCODE_TYPE); - if(unparsedBarcodeType != null && !unparsedBarcodeType.equals("")) - { + if (unparsedBarcodeType != null && !unparsedBarcodeType.equals("")) { barcodeType = CatimaBarcode.fromName(unparsedBarcodeType); } String unparsedBalance = kv.get(BALANCE); - if(unparsedBalance != null && !unparsedBalance.equals("")) - { + if (unparsedBalance != null && !unparsedBalance.equals("")) { balance = new BigDecimal(unparsedBalance); } String unparsedBalanceType = kv.get(BALANCE_TYPE); - if (unparsedBalanceType != null && !unparsedBalanceType.equals("")) - { + if (unparsedBalanceType != null && !unparsedBalanceType.equals("")) { balanceType = Currency.getInstance(unparsedBalanceType); } String unparsedExpiry = kv.get(EXPIRY); - if(unparsedExpiry != null && !unparsedExpiry.equals("")) - { + if (unparsedExpiry != null && !unparsedExpiry.equals("")) { expiry = new Date(Long.parseLong(unparsedExpiry)); } String unparsedHeaderColor = kv.get(HEADER_COLOR); - if(unparsedHeaderColor != null) - { + if (unparsedHeaderColor != null) { headerColor = Integer.parseInt(unparsedHeaderColor); } - return new LoyaltyCard(-1, store, note, expiry, balance, balanceType, cardId, barcodeId, barcodeType, headerColor, 0, Utils.getUnixTime(),100); + return new LoyaltyCard(-1, store, note, expiry, balance, balanceType, cardId, barcodeId, barcodeType, headerColor, 0, Utils.getUnixTime(), 100); } catch (NullPointerException | NumberFormatException | UnsupportedEncodingException ex) { throw new InvalidObjectException("Not a valid import URI"); } @@ -159,14 +153,14 @@ public class ImportURIHelper { fragment = appendFragment(fragment, EXPIRY, String.valueOf(loyaltyCard.expiry.getTime())); } fragment = appendFragment(fragment, CARD_ID, loyaltyCard.cardId); - if(loyaltyCard.barcodeId != null) { + if (loyaltyCard.barcodeId != null) { fragment = appendFragment(fragment, BARCODE_ID, loyaltyCard.barcodeId); } - if(loyaltyCard.barcodeType != null) { + if (loyaltyCard.barcodeType != null) { fragment = appendFragment(fragment, BARCODE_TYPE, loyaltyCard.barcodeType.name()); } - if(loyaltyCard.headerColor != null) { + if (loyaltyCard.headerColor != null) { fragment = appendFragment(fragment, HEADER_COLOR, loyaltyCard.headerColor.toString()); } // Star status will not be exported diff --git a/app/src/main/java/protect/card_locker/LetterBitmap.java b/app/src/main/java/protect/card_locker/LetterBitmap.java index 2afc75f2e..94b409168 100644 --- a/app/src/main/java/protect/card_locker/LetterBitmap.java +++ b/app/src/main/java/protect/card_locker/LetterBitmap.java @@ -18,8 +18,7 @@ import android.text.TextPaint; * alphabet or digit, if there is no letter or digit available, a default image * is shown instead. */ -class LetterBitmap -{ +class LetterBitmap { /** * The number of available tile colors @@ -37,39 +36,32 @@ class LetterBitmap /** * Constructor for LetterTileProvider * - * @param context The {@link Context} to use - * @param displayName The name used to create the letter for the tile - * @param key The key used to generate the background color for the tile + * @param context The {@link Context} to use + * @param displayName The name used to create the letter for the tile + * @param key The key used to generate the background color for the tile * @param tileLetterFontSize The font size used to display the letter - * @param width The desired width of the tile - * @param height The desired height of the tile - * @param backgroundColor (optional) color to use for background. - * @param textColor (optional) color to use for text. + * @param width The desired width of the tile + * @param height The desired height of the tile + * @param backgroundColor (optional) color to use for background. + * @param textColor (optional) color to use for text. */ public LetterBitmap(Context context, String displayName, String key, int tileLetterFontSize, - int width, int height, Integer backgroundColor, Integer textColor) - { + int width, int height, Integer backgroundColor, Integer textColor) { TextPaint paint = new TextPaint(); paint.setTypeface(Typeface.create("sans-serif-light", Typeface.BOLD)); - if(textColor != null) - { + if (textColor != null) { paint.setColor(textColor); - } - else - { + } else { paint.setColor(Color.WHITE); } paint.setTextAlign(Paint.Align.CENTER); paint.setAntiAlias(true); - if(backgroundColor == null) - { + if (backgroundColor == null) { mColor = getDefaultColor(context, key); - } - else - { + } else { mColor = backgroundColor; } @@ -80,7 +72,7 @@ class LetterBitmap c.setBitmap(mBitmap); c.drawColor(mColor); - char [] firstCharArray = new char[1]; + char[] firstCharArray = new char[1]; firstCharArray[0] = firstChar.toUpperCase().charAt(0); paint.setTextSize(tileLetterFontSize); @@ -97,16 +89,14 @@ class LetterBitmap * alphabet or digit, if there is no letter or digit available, a * default image is shown instead */ - public Bitmap getLetterTile() - { + public Bitmap getLetterTile() { return mBitmap; } /** * @return background color used for letter title. */ - public int getBackgroundColor() - { + public int getBackgroundColor() { return mColor; } @@ -115,8 +105,7 @@ class LetterBitmap * @return A new or previously chosen color for key used as the * tile background color */ - private static int pickColor(String key, TypedArray colors) - { + private static int pickColor(String key, TypedArray colors) { // String.hashCode() is not supposed to change across java versions, so // this should guarantee the same key always maps to the same color final int color = Math.abs(key.hashCode()) % NUM_OF_TILE_COLORS; @@ -127,8 +116,7 @@ class LetterBitmap * Determine the color which the letter tile will use if no default * color is provided. */ - public static int getDefaultColor(Context context, String key) - { + public static int getDefaultColor(Context context, String key) { final Resources res = context.getResources(); TypedArray colors = res.obtainTypedArray(R.array.letter_tile_colors); diff --git a/app/src/main/java/protect/card_locker/LoyaltyCard.java b/app/src/main/java/protect/card_locker/LoyaltyCard.java index 1ec44fa6d..06a0165a5 100644 --- a/app/src/main/java/protect/card_locker/LoyaltyCard.java +++ b/app/src/main/java/protect/card_locker/LoyaltyCard.java @@ -35,8 +35,7 @@ public class LoyaltyCard implements Parcelable { public LoyaltyCard(final int id, final String store, final String note, final Date expiry, final BigDecimal balance, final Currency balanceType, final String cardId, @Nullable final String barcodeId, @Nullable final CatimaBarcode barcodeType, - @Nullable final Integer headerColor, final int starStatus, final long lastUsed,final int zoomLevel) - { + @Nullable final Integer headerColor, final int starStatus, final long lastUsed, final int zoomLevel) { this.id = id; this.store = store; this.note = note; @@ -88,8 +87,7 @@ public class LoyaltyCard implements Parcelable { parcel.writeInt(zoomLevel); } - public static LoyaltyCard toLoyaltyCard(Cursor cursor) - { + public static LoyaltyCard toLoyaltyCard(Cursor cursor) { int id = cursor.getInt(cursor.getColumnIndexOrThrow(DBHelper.LoyaltyCardDbIds.ID)); String store = cursor.getString(cursor.getColumnIndexOrThrow(DBHelper.LoyaltyCardDbIds.STORE)); String note = cursor.getString(cursor.getColumnIndexOrThrow(DBHelper.LoyaltyCardDbIds.NOTE)); @@ -110,27 +108,23 @@ public class LoyaltyCard implements Parcelable { Date expiry = null; Integer headerColor = null; - if (cursor.isNull(barcodeTypeColumn) == false) - { + if (cursor.isNull(barcodeTypeColumn) == false) { barcodeType = CatimaBarcode.fromName(cursor.getString(barcodeTypeColumn)); } - if (cursor.isNull(balanceTypeColumn) == false) - { + if (cursor.isNull(balanceTypeColumn) == false) { balanceType = Currency.getInstance(cursor.getString(balanceTypeColumn)); } - if(expiryLong > 0) - { + if (expiryLong > 0) { expiry = new Date(expiryLong); } - if(cursor.isNull(headerColorColumn) == false) - { + if (cursor.isNull(headerColorColumn) == false) { headerColor = cursor.getInt(headerColorColumn); } - return new LoyaltyCard(id, store, note, expiry, balance, balanceType, cardId, barcodeId, barcodeType, headerColor, starred, lastUsed,zoomLevel); + return new LoyaltyCard(id, store, note, expiry, balance, balanceType, cardId, barcodeId, barcodeType, headerColor, starred, lastUsed, zoomLevel); } @Override diff --git a/app/src/main/java/protect/card_locker/LoyaltyCardCursorAdapter.java b/app/src/main/java/protect/card_locker/LoyaltyCardCursorAdapter.java index 1e974ae0f..c5a05ff6d 100644 --- a/app/src/main/java/protect/card_locker/LoyaltyCardCursorAdapter.java +++ b/app/src/main/java/protect/card_locker/LoyaltyCardCursorAdapter.java @@ -3,6 +3,7 @@ package protect.card_locker; import android.content.Context; import android.content.res.Resources; import android.database.Cursor; +import android.graphics.Bitmap; import android.graphics.Color; import android.graphics.drawable.Drawable; import android.util.SparseBooleanArray; @@ -12,20 +13,19 @@ import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; import android.widget.ImageView; -import android.widget.LinearLayout; -import android.widget.RelativeLayout; import android.widget.TextView; +import androidx.core.content.ContextCompat; +import androidx.core.graphics.BlendModeColorFilterCompat; +import androidx.core.graphics.BlendModeCompat; +import androidx.recyclerview.widget.RecyclerView; + import com.google.android.material.card.MaterialCardView; import java.math.BigDecimal; import java.text.DateFormat; import java.util.ArrayList; -import androidx.core.graphics.BlendModeColorFilterCompat; -import androidx.core.graphics.BlendModeCompat; -import androidx.recyclerview.widget.RecyclerView; - import protect.card_locker.preferences.Settings; public class LoyaltyCardCursorAdapter extends BaseCursorAdapter { @@ -33,11 +33,12 @@ public class LoyaltyCardCursorAdapter extends BaseCursorAdapter mListener.onRowClicked(inputPosition)); - inputHolder.mInformationContainer.setOnClickListener(inputView -> mListener.onRowClicked(inputPosition)); inputHolder.mRow.setOnLongClickListener(inputView -> { mListener.onRowLongClicked(inputPosition); inputView.performHapticFeedback(HapticFeedbackConstants.LONG_PRESS); return true; }); - - inputHolder.mInformationContainer.setOnLongClickListener(inputView -> { - mListener.onRowLongClicked(inputPosition); - inputView.performHapticFeedback(HapticFeedbackConstants.LONG_PRESS); - return true; - }); } private void applyIconAnimation(LoyaltyCardListItemViewHolder inputHolder, int inputPosition) { if (mSelectedItems.get(inputPosition, false)) { - inputHolder.mThumbnailFrontContainer.setVisibility(View.GONE); - resetIconYAxis(inputHolder.mThumbnailBackContainer); - inputHolder.mThumbnailBackContainer.setVisibility(View.VISIBLE); - inputHolder.mThumbnailBackContainer.setAlpha(1); + inputHolder.mCardIcon.setVisibility(View.GONE); + resetIconYAxis(inputHolder.mTickIcon); + inputHolder.mTickIcon.setVisibility(View.VISIBLE); if (mCurrentSelectedIndex == inputPosition) { - LoyaltyCardAnimator.flipView(mContext, inputHolder.mThumbnailBackContainer, inputHolder.mThumbnailFrontContainer, true); + LoyaltyCardAnimator.flipView(mContext, inputHolder.mTickIcon, inputHolder.mCardIcon, true); resetCurrentIndex(); } } else { - inputHolder.mThumbnailBackContainer.setVisibility(View.GONE); - resetIconYAxis(inputHolder.mThumbnailFrontContainer); - inputHolder.mThumbnailFrontContainer.setVisibility(View.VISIBLE); - inputHolder.mThumbnailFrontContainer.setAlpha(1); + inputHolder.mTickIcon.setVisibility(View.GONE); + resetIconYAxis(inputHolder.mCardIcon); + inputHolder.mCardIcon.setVisibility(View.VISIBLE); if ((mReverseAllAnimations && mAnimationItemsIndex.get(inputPosition, false)) || mCurrentSelectedIndex == inputPosition) { - LoyaltyCardAnimator.flipView(mContext, inputHolder.mThumbnailBackContainer, inputHolder.mThumbnailFrontContainer, false); + LoyaltyCardAnimator.flipView(mContext, inputHolder.mTickIcon, inputHolder.mCardIcon, false); resetCurrentIndex(); } } @@ -264,23 +246,20 @@ public class LoyaltyCardCursorAdapter extends BaseCursorAdapter currencies = new HashMap<>(); - String tempCameraPicturePath; - LoyaltyCard tempLoyaltyCard; + ActivityResultLauncher mPhotoTakerLauncher; + ActivityResultLauncher mPhotoPickerLauncher; + ActivityResultLauncher mCardIdAndBarCodeEditorLauncher; + + ActivityResultLauncher mCropperLauncher; + int mRequestedImage = 0; + int mCropperFinishedType = 0; + UCrop.Options mCropperOptions; + + boolean mFrontImageUnsaved = false; + boolean mBackImageUnsaved = false; + boolean mIconUnsaved = false; + + boolean mFrontImageRemoved = false; + boolean mBackImageRemoved = false; + boolean mIconRemoved = false; + final private TaskHandler mTasks = new TaskHandler(); private static LoyaltyCard updateTempState(LoyaltyCard loyaltyCard, LoyaltyCardField fieldName, Object value) { @@ -156,7 +189,7 @@ public class LoyaltyCardEditActivity extends CatimaAppCompatActivity { (CatimaBarcode) (fieldName == LoyaltyCardField.barcodeType ? value : loyaltyCard.barcodeType), (Integer) (fieldName == LoyaltyCardField.headerColor ? value : loyaltyCard.headerColor), (int) (fieldName == LoyaltyCardField.starStatus ? value : loyaltyCard.starStatus), - Utils.getUnixTime(),100 + Utils.getUnixTime(), 100 ); } @@ -182,12 +215,41 @@ public class LoyaltyCardEditActivity extends CatimaAppCompatActivity { + ", updateLoyaltyCard=" + updateLoyaltyCard); } + @Override public void onSaveInstanceState(@NonNull Bundle savedInstanceState) { super.onSaveInstanceState(savedInstanceState); tabs = findViewById(R.id.tabs); savedInstanceState.putInt(STATE_TAB_INDEX, tabs.getSelectedTabPosition()); savedInstanceState.putParcelable(STATE_TEMP_CARD, tempLoyaltyCard); + savedInstanceState.putInt(STATE_REQUESTED_IMAGE, mRequestedImage); + + Object cardImageFrontObj = cardImageFront.getTag(); + if (mFrontImageUnsaved && (cardImageFrontObj instanceof Bitmap) && Utils.saveTempImage(this, (Bitmap) cardImageFrontObj, TEMP_UNSAVED_FRONT_IMAGE_NAME, TEMP_UNSAVED_IMAGE_FORMAT) != null) { + savedInstanceState.putInt(STATE_FRONT_IMAGE_UNSAVED, 1); + } else { + savedInstanceState.putInt(STATE_FRONT_IMAGE_UNSAVED, 0); + } + + Object cardImageBackObj = cardImageBack.getTag(); + if (mBackImageUnsaved && (cardImageBackObj instanceof Bitmap) && Utils.saveTempImage(this, (Bitmap) cardImageBackObj, TEMP_UNSAVED_BACK_IMAGE_NAME, TEMP_UNSAVED_IMAGE_FORMAT) != null) { + savedInstanceState.putInt(STATE_BACK_IMAGE_UNSAVED, 1); + } else { + savedInstanceState.putInt(STATE_BACK_IMAGE_UNSAVED, 0); + } + + Object thumbnailObj = thumbnail.getTag(); + if (mIconUnsaved && (thumbnailObj instanceof Bitmap) && Utils.saveTempImage(this, (Bitmap) thumbnailObj, TEMP_UNSAVED_ICON_NAME, TEMP_UNSAVED_IMAGE_FORMAT) != null) { + savedInstanceState.putInt(STATE_ICON_UNSAVED, 1); + } else { + savedInstanceState.putInt(STATE_ICON_UNSAVED, 0); + } + + savedInstanceState.putInt(STATE_UPDATE_LOYALTY_CARD, updateLoyaltyCard ? 1 : 0); + savedInstanceState.putInt(STATE_HAS_CHANGED, hasChanged ? 1 : 0); + savedInstanceState.putInt(STATE_FRONT_IMAGE_REMOVED, mFrontImageRemoved ? 1 : 0); + savedInstanceState.putInt(STATE_BACK_IMAGE_REMOVED, mBackImageRemoved ? 1 : 0); + savedInstanceState.putInt(STATE_ICON_REMOVED, mIconRemoved ? 1 : 0); } @Override @@ -196,6 +258,15 @@ public class LoyaltyCardEditActivity extends CatimaAppCompatActivity { super.onRestoreInstanceState(savedInstanceState); tabs = findViewById(R.id.tabs); tabs.selectTab(tabs.getTabAt(savedInstanceState.getInt(STATE_TAB_INDEX))); + mRequestedImage = savedInstanceState.getInt(STATE_REQUESTED_IMAGE); + mFrontImageUnsaved = savedInstanceState.getInt(STATE_FRONT_IMAGE_UNSAVED) == 1; + mBackImageUnsaved = savedInstanceState.getInt(STATE_BACK_IMAGE_UNSAVED) == 1; + mIconUnsaved = savedInstanceState.getInt(STATE_ICON_UNSAVED) == 1; + updateLoyaltyCard = savedInstanceState.getInt(STATE_UPDATE_LOYALTY_CARD) == 1; + hasChanged = savedInstanceState.getInt(STATE_HAS_CHANGED) == 1; + mFrontImageRemoved = savedInstanceState.getInt(STATE_FRONT_IMAGE_REMOVED) == 1; + mBackImageRemoved = savedInstanceState.getInt(STATE_BACK_IMAGE_REMOVED) == 1; + mIconRemoved = savedInstanceState.getInt(STATE_ICON_REMOVED) == 1; } @Override @@ -235,8 +306,6 @@ public class LoyaltyCardEditActivity extends CatimaAppCompatActivity { barcodeCaptureLayout = findViewById(R.id.barcodeCaptureLayout); cardImageFrontHolder = findViewById(R.id.frontImageHolder); cardImageBackHolder = findViewById(R.id.backImageHolder); - cardImageFrontHolder.setId(ID_IMAGE_FRONT); - cardImageBackHolder.setId(ID_IMAGE_BACK); cardImageFront = findViewById(R.id.frontImage); cardImageBack = findViewById(R.id.backImage); @@ -417,7 +486,6 @@ public class LoyaltyCardEditActivity extends CatimaAppCompatActivity { if (!lastValue.toString().equals(getString(R.string.setBarcodeId))) { barcodeIdField.setText(lastValue); } - ; AlertDialog.Builder builder = new AlertDialog.Builder(LoyaltyCardEditActivity.this); builder.setTitle(R.string.setBarcodeId); @@ -509,6 +577,112 @@ public class LoyaltyCardEditActivity extends CatimaAppCompatActivity { }); tabs.selectTab(tabs.getTabAt(0)); + + + mPhotoTakerLauncher = registerForActivityResult(new ActivityResultContracts.TakePicture(), result -> { + if (result) { + startCropper(getCacheDir() + "/" + TEMP_CAMERA_IMAGE_NAME); + } + }); + + // android 11: wanted to swap it to ActivityResultContracts.GetContent but then it shows a file browsers that shows image mime types, offering gallery in the file browser + mPhotoPickerLauncher = registerForActivityResult(new ActivityResultContracts.StartActivityForResult(), result -> { + if (result.getResultCode() == RESULT_OK) { + Intent intent = result.getData(); + if (intent == null) { + Log.d("photo picker", "photo picker returned without an intent"); + return; + } + Uri uri = intent.getData(); + startCropperUri(uri); + } + }); + + mCardIdAndBarCodeEditorLauncher = registerForActivityResult(new ActivityResultContracts.StartActivityForResult(), result -> { + if (result.getResultCode() == RESULT_OK) { + Intent intent = result.getData(); + if (intent == null) { + Log.d("barcode card id editor", "barcode and card id editor picker returned without an intent"); + return; + } + BarcodeValues barcodeValues = Utils.parseSetBarcodeActivityResult(Utils.BARCODE_SCAN, result.getResultCode(), intent, getApplicationContext()); + + cardId = barcodeValues.content(); + barcodeType = barcodeValues.format(); + barcodeId = ""; + } + }); + + mCropperLauncher = registerForActivityResult(new ActivityResultContracts.StartActivityForResult(), result -> { + Intent intent = result.getData(); + if (intent == null) { + Log.d("cropper", "ucrop returned a null intent"); + return; + } + if (result.getResultCode() == Activity.RESULT_OK) { + Uri debugUri = UCrop.getOutput(intent); + if (debugUri == null) { + throw new RuntimeException("ucrop returned success but not destination uri!"); + } + Log.d("cropper", "ucrop produced image at " + debugUri); + Bitmap bitmap = BitmapFactory.decodeFile(getCacheDir() + "/" + TEMP_CROP_IMAGE_NAME); + + if (bitmap != null) { + if (requestedFrontImage()) { + mFrontImageUnsaved = true; + setCardImage(cardImageFront, Utils.resizeBitmap(bitmap, Utils.BITMAP_SIZE_BIG), true); + } else if (requestedBackImage()) { + mBackImageUnsaved = true; + setCardImage(cardImageBack, Utils.resizeBitmap(bitmap, Utils.BITMAP_SIZE_BIG), true); + } else { + mIconUnsaved = true; + setCardImage(thumbnail, Utils.resizeBitmap(bitmap, Utils.BITMAP_SIZE_SMALL), false); + thumbnail.setBackgroundColor(Color.TRANSPARENT); + setColorFromIcon(); + } + Log.d("cropper", "mRequestedImage: " + mRequestedImage); + mCropperFinishedType = mRequestedImage; + hasChanged = true; + } else { + Toast.makeText(LoyaltyCardEditActivity.this, R.string.errorReadingImage, Toast.LENGTH_LONG).show(); + } + } else if (result.getResultCode() == UCrop.RESULT_ERROR) { + Throwable e = UCrop.getError(intent); + if (e == null) { + throw new RuntimeException("ucrop returned error state but not an error!"); + } + Log.e("cropper error", e.toString()); + } + }); + + mCropperOptions = new UCrop.Options(); + setCropperTheme(); + } + + // ucrop 2.2.6 initial aspect ratio is glitched when 0x0 is used as the initial ratio option + // https://github.com/Yalantis/uCrop/blob/281c8e6438d81f464d836fc6b500517144af264a/ucrop/src/main/java/com/yalantis/ucrop/UCropActivity.java#L264 + // so source width height has to be provided for now, depending on whether future versions of ucrop will support 0x0 as the default option + private void setCropperOptions(boolean cardShapeDefault, float sourceWidth, float sourceHeight) { + mCropperOptions.setCompressionFormat(TEMP_CROP_IMAGE_FORMAT); + mCropperOptions.setFreeStyleCropEnabled(true); + mCropperOptions.setHideBottomControls(false); + // default aspect ratio workaround + int selectedByDefault = 1; + if (cardShapeDefault) { + selectedByDefault = 2; + } + mCropperOptions.setAspectRatioOptions(selectedByDefault, + new AspectRatio(null, 1, 1), + new AspectRatio(getResources().getString(R.string.ucrop_label_original).toUpperCase(), sourceWidth, sourceHeight), + new AspectRatio(getResources().getString(R.string.card).toUpperCase(), 85.6f, 53.98f) + ); + } + + private void setCropperTheme() { + mCropperOptions.setToolbarColor(ContextCompat.getColor(this, R.color.colorPrimary)); + mCropperOptions.setStatusBarColor(ContextCompat.getColor(this, R.color.colorPrimaryDark)); + mCropperOptions.setToolbarWidgetColor(Color.WHITE); + mCropperOptions.setActiveControlsWidgetColor(ContextCompat.getColor(this, R.color.colorPrimary)); } @Override @@ -519,6 +693,30 @@ public class LoyaltyCardEditActivity extends CatimaAppCompatActivity { extractIntentFields(intent); } + private boolean requestedFrontImage() { + return mRequestedImage == Utils.CARD_IMAGE_FROM_CAMERA_FRONT || mRequestedImage == Utils.CARD_IMAGE_FROM_FILE_FRONT; + } + + private boolean croppedFrontImage() { + return mCropperFinishedType == Utils.CARD_IMAGE_FROM_CAMERA_FRONT || mCropperFinishedType == Utils.CARD_IMAGE_FROM_FILE_FRONT; + } + + private boolean requestedBackImage() { + return mRequestedImage == Utils.CARD_IMAGE_FROM_CAMERA_BACK || mRequestedImage == Utils.CARD_IMAGE_FROM_FILE_BACK; + } + + private boolean croppedBackImage() { + return mCropperFinishedType == Utils.CARD_IMAGE_FROM_CAMERA_BACK || mCropperFinishedType == Utils.CARD_IMAGE_FROM_FILE_BACK; + } + + private boolean requestedIcon() { + return mRequestedImage == Utils.CARD_IMAGE_FROM_CAMERA_ICON || mRequestedImage == Utils.CARD_IMAGE_FROM_FILE_ICON; + } + + private boolean croppedIcon() { + return mCropperFinishedType == Utils.CARD_IMAGE_FROM_CAMERA_ICON || mCropperFinishedType == Utils.CARD_IMAGE_FROM_FILE_ICON; + } + @SuppressLint("DefaultLocale") @Override public void onResume() { @@ -528,6 +726,7 @@ public class LoyaltyCardEditActivity extends CatimaAppCompatActivity { onResuming = true; + if (tempLoyaltyCard == null) { if (updateLoyaltyCard) { tempLoyaltyCard = db.getLoyaltyCard(loyaltyCardId); @@ -537,9 +736,6 @@ public class LoyaltyCardEditActivity extends CatimaAppCompatActivity { finish(); return; } - setTitle(R.string.editCardTitle); - setCardImage(cardImageFront, Utils.retrieveCardImage(this, tempLoyaltyCard.id, true)); - setCardImage(cardImageBack, Utils.retrieveCardImage(this, tempLoyaltyCard.id, false)); } else if (importLoyaltyCardUri != null) { try { tempLoyaltyCard = importUriHelper.parse(importLoyaltyCardUri); @@ -548,14 +744,45 @@ public class LoyaltyCardEditActivity extends CatimaAppCompatActivity { finish(); return; } - setTitle(R.string.addCardTitle); } else { // New card, use default values - tempLoyaltyCard = new LoyaltyCard(-1, "", "", null, new BigDecimal("0"), null, "", null, null, null, 0, Utils.getUnixTime(),100); - setTitle(R.string.addCardTitle); + tempLoyaltyCard = new LoyaltyCard(-1, "", "", null, new BigDecimal("0"), null, "", null, null, null, 0, Utils.getUnixTime(), 100); + } } + if (!initDone) { + if (updateLoyaltyCard) { + setTitle(R.string.editCardTitle); + + if (!mFrontImageUnsaved && !croppedFrontImage() && !mFrontImageRemoved) { + setCardImage(cardImageFront, Utils.retrieveCardImage(this, tempLoyaltyCard.id, ImageLocationType.front), true); + } + if (!mBackImageUnsaved && !croppedBackImage() && !mBackImageRemoved) { + setCardImage(cardImageBack, Utils.retrieveCardImage(this, tempLoyaltyCard.id, ImageLocationType.back), true); + } + if (!mIconUnsaved && !croppedIcon() && !mIconRemoved) { + setCardImage(thumbnail, Utils.retrieveCardImage(this, tempLoyaltyCard.id, ImageLocationType.icon), false); + } + } else { + setTitle(R.string.addCardTitle); + } + + if (mFrontImageUnsaved && !croppedFrontImage()) { + setCardImage(cardImageFront, Utils.loadTempImage(this, TEMP_UNSAVED_FRONT_IMAGE_NAME), true); + } + if (mBackImageUnsaved && !croppedBackImage()) { + setCardImage(cardImageBack, Utils.loadTempImage(this, TEMP_UNSAVED_BACK_IMAGE_NAME), true); + } + if (mIconUnsaved && !croppedIcon()) { + setCardImage(thumbnail, Utils.loadTempImage(this, TEMP_UNSAVED_ICON_NAME), false); + } + } + + mCropperFinishedType = 0; + + boolean hadChanges = hasChanged; + storeFieldEdit.setText(tempLoyaltyCard.store); noteFieldEdit.setText(tempLoyaltyCard.note); formatExpiryField(this, expiryField, tempLoyaltyCard.expiry); @@ -614,7 +841,7 @@ public class LoyaltyCardEditActivity extends CatimaAppCompatActivity { // It can't be null because we set it in updateTempState but SpotBugs insists it can be // NP_NULL_ON_SOME_PATH: Possible null pointer dereference if (tempLoyaltyCard.headerColor != null) { - thumbnail.setOnClickListener(new ColorSelectListener()); + thumbnail.setOnClickListener(new ChooseCardImage()); } // Update from intent @@ -645,8 +872,8 @@ public class LoyaltyCardEditActivity extends CatimaAppCompatActivity { // Initialization has finished if (!initDone) { - hasChanged = false; initDone = true; + hasChanged = hadChanges; } generateOrHideBarcode(); @@ -666,12 +893,21 @@ public class LoyaltyCardEditActivity extends CatimaAppCompatActivity { onResuming = false; } - protected static void setCardImage(ImageView imageView, Bitmap bitmap) { + protected void setColorFromIcon() { + Object icon = thumbnail.getTag(); + if (icon != null && (icon instanceof Bitmap)) { + updateTempState(LoyaltyCardField.headerColor, new Palette.Builder((Bitmap) icon).generate().getDominantColor(tempLoyaltyCard.headerColor != null ? tempLoyaltyCard.headerColor : ContextCompat.getColor(this, R.color.colorPrimary))); + } else { + Log.d("setColorFromIcon", "attempting header color change from icon but icon does not exist"); + } + } + + protected void setCardImage(ImageView imageView, Bitmap bitmap, boolean applyFallback) { imageView.setTag(bitmap); if (bitmap != null) { imageView.setImageBitmap(bitmap); - } else { + } else if (applyFallback) { imageView.setImageResource(R.drawable.ic_camera_white); } } @@ -707,8 +943,10 @@ public class LoyaltyCardEditActivity extends CatimaAppCompatActivity { try { if (requestCode == PERMISSION_REQUEST_CAMERA_IMAGE_FRONT) { takePhotoForCard(Utils.CARD_IMAGE_FROM_CAMERA_FRONT); - } else { + } else if (requestCode == PERMISSION_REQUEST_CAMERA_IMAGE_BACK) { takePhotoForCard(Utils.CARD_IMAGE_FROM_CAMERA_BACK); + } else if (requestCode == PERMISSION_REQUEST_CAMERA_IMAGE_ICON) { + takePhotoForCard(Utils.CARD_IMAGE_FROM_CAMERA_ICON); } } catch (Exception e) { e.printStackTrace(); @@ -737,9 +975,7 @@ public class LoyaltyCardEditActivity extends CatimaAppCompatActivity { dialog.dismiss(); }) - .setNegativeButton(R.string.no, (dialog, which) -> { - dialog.dismiss(); - }) + .setNegativeButton(R.string.no, (dialog, which) -> dialog.dismiss()) .setOnDismissListener(dialogInterface -> { if (tempStoredOldBarcodeValue != null) { barcodeIdField.setText(tempStoredOldBarcodeValue); @@ -768,40 +1004,22 @@ public class LoyaltyCardEditActivity extends CatimaAppCompatActivity { AlertDialog.Builder builder = new AlertDialog.Builder(this); builder.setTitle(R.string.leaveWithoutSaveTitle); builder.setMessage(R.string.leaveWithoutSaveConfirmation); - builder.setPositiveButton(R.string.confirm, new DialogInterface.OnClickListener() { - @Override - public void onClick(DialogInterface dialog, int which) { - finish(); - dialog.dismiss(); - } - }); - builder.setNegativeButton(R.string.cancel, new DialogInterface.OnClickListener() { - @Override - public void onClick(DialogInterface dialog, int which) { - dialog.dismiss(); - } + builder.setPositiveButton(R.string.confirm, (dialog, which) -> { + finish(); + dialog.dismiss(); }); + builder.setNegativeButton(R.string.cancel, (dialog, which) -> dialog.dismiss()); confirmExitDialog = builder.create(); } confirmExitDialog.show(); } - private void takePhotoForCard(int type) throws IOException { - Intent i = new Intent(MediaStore.ACTION_IMAGE_CAPTURE); - String imageFileName = "CATIMA_" + new Date().getTime(); - File image = File.createTempFile( - imageFileName, - ".jpg", - getExternalFilesDir(Environment.DIRECTORY_PICTURES) - ); + private void takePhotoForCard(int type) { + Uri photoURI = FileProvider.getUriForFile(LoyaltyCardEditActivity.this, BuildConfig.APPLICATION_ID, Utils.createTempFile(this, TEMP_CAMERA_IMAGE_NAME)); + mRequestedImage = type; - tempCameraPicturePath = image.getAbsolutePath(); - - Uri photoURI = FileProvider.getUriForFile(LoyaltyCardEditActivity.this, BuildConfig.APPLICATION_ID, image); - i.putExtra(MediaStore.EXTRA_OUTPUT, photoURI); - - startActivityForResult(i, type); + mPhotoTakerLauncher.launch(photoURI); } class EditCardIdAndBarcode implements View.OnClickListener { @@ -811,41 +1029,140 @@ public class LoyaltyCardEditActivity extends CatimaAppCompatActivity { final Bundle b = new Bundle(); b.putString(LoyaltyCardEditActivity.BUNDLE_CARDID, cardIdFieldView.getText().toString()); i.putExtras(b); - startActivityForResult(i, Utils.BARCODE_SCAN); + mCardIdAndBarCodeEditorLauncher.launch(i); } } class ChooseCardImage implements View.OnClickListener { @Override public void onClick(View v) throws NoSuchElementException { - ImageView targetView = v.getId() == ID_IMAGE_FRONT ? cardImageFront : cardImageBack; + ImageView targetView; + + if (v.getId() == R.id.frontImageHolder) { + targetView = cardImageFront; + } else if (v.getId() == R.id.backImageHolder) { + targetView = cardImageBack; + } else if (v.getId() == R.id.thumbnail) { + targetView = thumbnail; + } else { + throw new IllegalArgumentException("Invalid IMAGE ID " + v.getId()); + } LinkedHashMap> cardOptions = new LinkedHashMap<>(); - if (targetView.getTag() != null) { + if (targetView.getTag() != null && v.getId() != R.id.thumbnail) { cardOptions.put(getString(R.string.removeImage), () -> { - setCardImage(targetView, null); + if (targetView == cardImageFront) { + mFrontImageRemoved = true; + mFrontImageUnsaved = false; + } else { + mBackImageRemoved = true; + mBackImageUnsaved = false; + } + + setCardImage(targetView, null, true); + return null; + }); + } + + if (v.getId() == R.id.thumbnail) { + cardOptions.put(getString(R.string.selectColor), () -> { + ColorPickerDialog.Builder dialogBuilder = ColorPickerDialog.newBuilder(); + + if (tempLoyaltyCard.headerColor != null) { + dialogBuilder.setColor(tempLoyaltyCard.headerColor); + } + + ColorPickerDialog dialog = dialogBuilder.create(); + dialog.setColorPickerDialogListener(new ColorPickerDialogListener() { + @Override + public void onColorSelected(int dialogId, int color) { + updateTempState(LoyaltyCardField.headerColor, color); + + // Unset image if set + thumbnail.setTag(null); + + generateIcon(storeFieldEdit.getText().toString()); + } + + @Override + public void onDialogDismissed(int dialogId) { + // Nothing to do, no change made + } + }); + dialog.show(getSupportFragmentManager(), "color-picker-dialog"); + + setCardImage(targetView, null, false); + mIconRemoved = true; + mIconUnsaved = false; + return null; }); } cardOptions.put(getString(R.string.takePhoto), () -> { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { - requestPermissions(new String[]{Manifest.permission.CAMERA}, v.getId() == ID_IMAGE_FRONT ? PERMISSION_REQUEST_CAMERA_IMAGE_FRONT : PERMISSION_REQUEST_CAMERA_IMAGE_BACK); + int permissionRequestType; + + if (v.getId() == R.id.frontImageHolder) { + permissionRequestType = PERMISSION_REQUEST_CAMERA_IMAGE_FRONT; + } else if (v.getId() == R.id.backImageHolder) { + permissionRequestType = PERMISSION_REQUEST_CAMERA_IMAGE_BACK; + } else if (v.getId() == R.id.thumbnail) { + permissionRequestType = PERMISSION_REQUEST_CAMERA_IMAGE_ICON; + } else { + throw new IllegalArgumentException("Unknown ID type " + v.getId()); + } + + requestPermissions(new String[]{Manifest.permission.CAMERA}, permissionRequestType); } else { - takePhotoForCard(v.getId() == ID_IMAGE_FRONT ? Utils.CARD_IMAGE_FROM_CAMERA_FRONT : Utils.CARD_IMAGE_FROM_CAMERA_BACK); + int cardImageType; + + if (v.getId() == R.id.frontImageHolder) { + cardImageType = Utils.CARD_IMAGE_FROM_CAMERA_FRONT; + } else if (v.getId() == R.id.backImageHolder) { + cardImageType = Utils.CARD_IMAGE_FROM_CAMERA_BACK; + } else if (v.getId() == R.id.thumbnail) { + cardImageType = Utils.CARD_IMAGE_FROM_CAMERA_ICON; + } else { + throw new IllegalArgumentException("Unknown ID type " + v.getId()); + } + + takePhotoForCard(cardImageType); } return null; }); cardOptions.put(getString(R.string.addFromImage), () -> { + if (v.getId() == R.id.frontImageHolder) { + mRequestedImage = Utils.CARD_IMAGE_FROM_FILE_FRONT; + } else if (v.getId() == R.id.backImageHolder) { + mRequestedImage = Utils.CARD_IMAGE_FROM_FILE_BACK; + } else if (v.getId() == R.id.thumbnail) { + mRequestedImage = Utils.CARD_IMAGE_FROM_FILE_ICON; + } else { + throw new IllegalArgumentException("Unknown ID type " + v.getId()); + } + Intent i = new Intent(Intent.ACTION_PICK); i.setType("image/*"); - startActivityForResult(i, v.getId() == ID_IMAGE_FRONT ? Utils.CARD_IMAGE_FROM_FILE_FRONT : Utils.CARD_IMAGE_FROM_FILE_BACK); + mPhotoPickerLauncher.launch(i); return null; }); + int titleResource; + + if (v.getId() == R.id.frontImageHolder) { + titleResource = R.string.setFrontImage; + } else if (v.getId() == R.id.backImageHolder) { + titleResource = R.string.setBackImage; + } else if (v.getId() == R.id.thumbnail) { + titleResource = R.string.setIcon; + } else { + throw new IllegalArgumentException("Unknown ID type " + v.getId()); + } + new AlertDialog.Builder(LoyaltyCardEditActivity.this) - .setTitle(v.getId() == ID_IMAGE_FRONT ? getString(R.string.setFrontImage) : getString(R.string.setBackImage)) + .setTitle(getString(titleResource)) .setItems(cardOptions.keySet().toArray(new CharSequence[cardOptions.size()]), (dialog, which) -> { Iterator> callables = cardOptions.values().iterator(); Callable callable = callables.next(); @@ -864,33 +1181,6 @@ public class LoyaltyCardEditActivity extends CatimaAppCompatActivity { } } - class ColorSelectListener implements View.OnClickListener { - @Override - public void onClick(View v) { - ColorPickerDialog.Builder dialogBuilder = ColorPickerDialog.newBuilder(); - - if (tempLoyaltyCard.headerColor != null) { - dialogBuilder.setColor(tempLoyaltyCard.headerColor); - } - - ColorPickerDialog dialog = dialogBuilder.create(); - dialog.setColorPickerDialogListener(new ColorPickerDialogListener() { - @Override - public void onColorSelected(int dialogId, int color) { - updateTempState(LoyaltyCardField.headerColor, color); - - generateIcon(storeFieldEdit.getText().toString()); - } - - @Override - public void onDialogDismissed(int dialogId) { - // Nothing to do, no change made - } - }); - dialog.show(getSupportFragmentManager(), "color-picker-dialog"); - } - } - public static class DatePickerFragment extends DialogFragment implements DatePickerDialog.OnDateSetListener { @@ -902,6 +1192,7 @@ public class LoyaltyCardEditActivity extends CatimaAppCompatActivity { this.expiryFieldEdit = expiryFieldEdit; } + @NonNull @Override public Dialog onCreateDialog(Bundle savedInstanceState) { // Use the current date as the default date in the picker @@ -969,8 +1260,9 @@ public class LoyaltyCardEditActivity extends CatimaAppCompatActivity { if (updateLoyaltyCard) { //update of "starStatus" not necessary, since it cannot be changed in this activity (only in ViewActivity) db.updateLoyaltyCard(loyaltyCardId, tempLoyaltyCard.store, tempLoyaltyCard.note, tempLoyaltyCard.expiry, tempLoyaltyCard.balance, tempLoyaltyCard.balanceType, tempLoyaltyCard.cardId, tempLoyaltyCard.barcodeId, tempLoyaltyCard.barcodeType, tempLoyaltyCard.headerColor); try { - Utils.saveCardImage(this, (Bitmap) cardImageFront.getTag(), loyaltyCardId, true); - Utils.saveCardImage(this, (Bitmap) cardImageBack.getTag(), loyaltyCardId, false); + Utils.saveCardImage(this, (Bitmap) cardImageFront.getTag(), loyaltyCardId, ImageLocationType.front); + Utils.saveCardImage(this, (Bitmap) cardImageBack.getTag(), loyaltyCardId, ImageLocationType.back); + Utils.saveCardImage(this, (Bitmap) thumbnail.getTag(), loyaltyCardId, ImageLocationType.icon); } catch (FileNotFoundException e) { e.printStackTrace(); } @@ -978,8 +1270,9 @@ public class LoyaltyCardEditActivity extends CatimaAppCompatActivity { } else { loyaltyCardId = (int) db.insertLoyaltyCard(tempLoyaltyCard.store, tempLoyaltyCard.note, tempLoyaltyCard.expiry, tempLoyaltyCard.balance, tempLoyaltyCard.balanceType, tempLoyaltyCard.cardId, tempLoyaltyCard.barcodeId, tempLoyaltyCard.barcodeType, tempLoyaltyCard.headerColor, 0, tempLoyaltyCard.lastUsed); try { - Utils.saveCardImage(this, (Bitmap) cardImageFront.getTag(), loyaltyCardId, true); - Utils.saveCardImage(this, (Bitmap) cardImageBack.getTag(), loyaltyCardId, false); + Utils.saveCardImage(this, (Bitmap) cardImageFront.getTag(), loyaltyCardId, ImageLocationType.front); + Utils.saveCardImage(this, (Bitmap) cardImageBack.getTag(), loyaltyCardId, ImageLocationType.back); + Utils.saveCardImage(this, (Bitmap) thumbnail.getTag(), loyaltyCardId, ImageLocationType.icon); } catch (FileNotFoundException e) { e.printStackTrace(); } @@ -987,6 +1280,8 @@ public class LoyaltyCardEditActivity extends CatimaAppCompatActivity { db.setLoyaltyCardGroups(loyaltyCardId, selectedGroups); + ShortcutHelper.updateShortcuts(this, db.getLoyaltyCard(loyaltyCardId)); + finish(); } @@ -1035,68 +1330,60 @@ public class LoyaltyCardEditActivity extends CatimaAppCompatActivity { return super.onOptionsItemSelected(item); } - @Override - public void onActivityResult(int requestCode, int resultCode, Intent intent) { - super.onActivityResult(requestCode, resultCode, intent); + public void startCropper(String sourceImagePath) { + startCropperUri(Uri.parse("file://" + sourceImagePath)); + } - if (resultCode == RESULT_OK) { - if (requestCode == Utils.CARD_IMAGE_FROM_CAMERA_FRONT || requestCode == Utils.CARD_IMAGE_FROM_CAMERA_BACK) { - Bitmap bitmap = BitmapFactory.decodeFile(tempCameraPicturePath); + public void startCropperUri(Uri sourceUri) { + Log.d("cropper", "launching cropper with image " + sourceUri.getPath()); + File cropOutput = Utils.createTempFile(this, TEMP_CROP_IMAGE_NAME); + Uri destUri = Uri.parse("file://" + cropOutput.getAbsolutePath()); + Log.d("cropper", "asking cropper to output to " + destUri.toString()); - if (bitmap != null) { - bitmap = Utils.resizeBitmap(bitmap); - try { - bitmap = Utils.rotateBitmap(bitmap, new ExifInterface(tempCameraPicturePath)); - } catch (IOException e) { - e.printStackTrace(); - } - - if (requestCode == Utils.CARD_IMAGE_FROM_CAMERA_FRONT) { - setCardImage(cardImageFront, bitmap); - } else { - setCardImage(cardImageBack, bitmap); - } - - hasChanged = true; - } else { - Toast.makeText(this, R.string.errorReadingImage, Toast.LENGTH_LONG).show(); - } - } else if (requestCode == Utils.CARD_IMAGE_FROM_FILE_FRONT || requestCode == Utils.CARD_IMAGE_FROM_FILE_BACK) { - Bitmap bitmap = null; - try { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { - ImageDecoder.Source image_source = ImageDecoder.createSource(getContentResolver(), intent.getData()); - bitmap = ImageDecoder.decodeBitmap(image_source, (decoder, info, source) -> decoder.setMutableRequired(true)); - } else { - bitmap = MediaStore.Images.Media.getBitmap(getContentResolver(), intent.getData()); - } - } catch (IOException e) { - Log.e(TAG, "Error getting data from image file"); - e.printStackTrace(); - } - - if (bitmap != null) { - bitmap = Utils.resizeBitmap(bitmap); - if (requestCode == Utils.CARD_IMAGE_FROM_FILE_FRONT) { - setCardImage(cardImageFront, bitmap); - } else { - setCardImage(cardImageBack, bitmap); - } - - hasChanged = true; - } else { - Toast.makeText(this, R.string.errorReadingImage, Toast.LENGTH_LONG).show(); - } - } else { - BarcodeValues barcodeValues = Utils.parseSetBarcodeActivityResult(requestCode, resultCode, intent, this); - - cardId = barcodeValues.content(); - barcodeType = barcodeValues.format(); - barcodeId = ""; - } + if (requestedFrontImage()) { + mCropperOptions.setToolbarTitle(getResources().getString(R.string.setFrontImage)); + } else if (requestedBackImage()) { + mCropperOptions.setToolbarTitle(getResources().getString(R.string.setBackImage)); + } else if (requestedIcon()) { + mCropperOptions.setToolbarTitle(getResources().getString(R.string.setIcon)); } - onResume(); + if (requestedIcon()) { + setCropperOptions(true, 0f, 0f); + } else { + // sniff the input image for width and height to work around a ucrop bug + Bitmap image = null; + try { + image = BitmapFactory.decodeStream(getContentResolver().openInputStream(sourceUri)); + } catch (FileNotFoundException e) { + e.printStackTrace(); + Log.d("cropper", "failed opening bitmap for initial width and height for ucrop " + sourceUri.toString()); + } + if (image == null) { + Log.d("cropper", "failed loading bitmap for initial width and height for ucrop " + sourceUri.toString()); + setCropperOptions(true, 0f, 0f); + } else { + try { + Bitmap imageRotated = Utils.rotateBitmap(image, new ExifInterface(getContentResolver().openInputStream(sourceUri))); + setCropperOptions(false, imageRotated.getWidth(), imageRotated.getHeight()); + } catch (FileNotFoundException e) { + e.printStackTrace(); + Log.d("cropper", "failed opening image for exif reading before setting initial width and height for ucrop"); + setCropperOptions(false, image.getWidth(), image.getHeight()); + } catch (IOException e) { + e.printStackTrace(); + Log.d("cropper", "exif reading failed before setting initial width and height for ucrop"); + setCropperOptions(false, image.getWidth(), image.getHeight()); + } + } + } + mCropperLauncher.launch( + UCrop.of( + sourceUri, + destUri + ).withOptions(mCropperOptions) + .getIntent(this) + ); } private void showBarcode() { @@ -1120,7 +1407,7 @@ public class LoyaltyCardEditActivity extends CatimaAppCompatActivity { private void generateBarcode(String cardIdString, CatimaBarcode barcodeFormat) { mTasks.flushTaskList(TaskHandler.TYPE.BARCODE, true, false, false); - + if (barcodeImage.getHeight() == 0) { Log.d(TAG, "ImageView size is not known known at start, waiting for load"); // The size of the ImageView is not yet available as it has not @@ -1150,14 +1437,16 @@ public class LoyaltyCardEditActivity extends CatimaAppCompatActivity { return; } - thumbnail.setBackgroundColor(tempLoyaltyCard.headerColor); + if (thumbnail.getTag() == null) { + thumbnail.setBackgroundColor(tempLoyaltyCard.headerColor); - LetterBitmap letterBitmap = Utils.generateIcon(this, store, tempLoyaltyCard.headerColor); + LetterBitmap letterBitmap = Utils.generateIcon(this, store, tempLoyaltyCard.headerColor); - if (letterBitmap != null) { - thumbnail.setImageBitmap(letterBitmap.getLetterTile()); - } else { - thumbnail.setImageBitmap(null); + if (letterBitmap != null) { + thumbnail.setImageBitmap(letterBitmap.getLetterTile()); + } else { + thumbnail.setImageBitmap(null); + } } thumbnail.setMinimumWidth(thumbnail.getHeight()); diff --git a/app/src/main/java/protect/card_locker/LoyaltyCardLockerApplication.java b/app/src/main/java/protect/card_locker/LoyaltyCardLockerApplication.java index c031f7904..4ceb46994 100644 --- a/app/src/main/java/protect/card_locker/LoyaltyCardLockerApplication.java +++ b/app/src/main/java/protect/card_locker/LoyaltyCardLockerApplication.java @@ -3,7 +3,6 @@ package protect.card_locker; import android.app.Application; import androidx.appcompat.app.AppCompatDelegate; - import protect.card_locker.preferences.Settings; public class LoyaltyCardLockerApplication extends Application { diff --git a/app/src/main/java/protect/card_locker/LoyaltyCardViewActivity.java b/app/src/main/java/protect/card_locker/LoyaltyCardViewActivity.java index 1249dfc63..d7b1b7335 100644 --- a/app/src/main/java/protect/card_locker/LoyaltyCardViewActivity.java +++ b/app/src/main/java/protect/card_locker/LoyaltyCardViewActivity.java @@ -5,11 +5,11 @@ import android.content.pm.ActivityInfo; import android.content.res.Configuration; import android.graphics.Bitmap; import android.graphics.Color; +import android.graphics.Outline; import android.graphics.drawable.Drawable; import android.os.Build; import android.os.Bundle; import android.text.TextUtils; -import android.util.DisplayMetrics; import android.util.Log; import android.util.TypedValue; import android.view.GestureDetector; @@ -17,6 +17,8 @@ import android.view.Menu; import android.view.MenuItem; import android.view.MotionEvent; import android.view.View; +import android.view.ViewGroup; +import android.view.ViewOutlineProvider; import android.view.ViewTreeObserver; import android.view.Window; import android.view.WindowManager; @@ -44,8 +46,10 @@ import androidx.appcompat.content.res.AppCompatResources; import androidx.appcompat.widget.AppCompatTextView; import androidx.appcompat.widget.Toolbar; import androidx.constraintlayout.widget.Guideline; +import androidx.coordinatorlayout.widget.CoordinatorLayout; import androidx.core.content.ContextCompat; import androidx.core.graphics.drawable.DrawableCompat; +import androidx.core.widget.NestedScrollView; import androidx.core.widget.TextViewCompat; import protect.card_locker.async.TaskHandler; @@ -56,10 +60,11 @@ public class LoyaltyCardViewActivity extends CatimaAppCompatActivity implements private GestureDetector mGestureDetector; + CoordinatorLayout coordinatorLayout; TextView cardIdFieldView; BottomSheetBehavior behavior; - View bottomSheet; - View bottomSheetContentWrapper; + LinearLayout bottomSheet; + NestedScrollView bottomSheetContentWrapper; ImageView bottomSheetButton; TextView noteView; TextView groupsView; @@ -72,6 +77,9 @@ public class LoyaltyCardViewActivity extends CatimaAppCompatActivity implements ImageButton minimizeButton; View collapsingToolbarLayout; AppBarLayout appBarLayout; + ImageView iconImage; + Toolbar landscapeToolbar; + int loyaltyCardId; LoyaltyCard loyaltyCard; boolean rotationEnabled; @@ -99,8 +107,13 @@ public class LoyaltyCardViewActivity extends CatimaAppCompatActivity implements private ImageView[] dots; boolean isBarcodeSupported = true; + int bottomSheetState; + static final String STATE_IMAGEINDEX = "imageIndex"; static final String STATE_FULLSCREEN = "isFullscreen"; + static final String STATE_BOTTOMSHEET = "bottomSheetState"; + + private final int HEADER_FILTER_ALPHA = 127; final private TaskHandler mTasks = new TaskHandler(); @@ -190,11 +203,15 @@ public class LoyaltyCardViewActivity extends CatimaAppCompatActivity implements Log.d(TAG, "View activity: id=" + loyaltyCardId); } - private Drawable getDotIcon(boolean active) { + private Drawable getDotIcon(boolean active, boolean darkMode) { Drawable unwrappedIcon = AppCompatResources.getDrawable(this, active ? R.drawable.active_dot : R.drawable.inactive_dot); assert unwrappedIcon != null; Drawable wrappedIcon = DrawableCompat.wrap(unwrappedIcon); - DrawableCompat.setTint(wrappedIcon, ContextCompat.getColor(getApplicationContext(), R.color.iconColor)); + if (darkMode) { + DrawableCompat.setTint(wrappedIcon, Color.WHITE); + } else { + DrawableCompat.setTint(wrappedIcon, Color.BLACK); + } return wrappedIcon; } @@ -212,6 +229,17 @@ public class LoyaltyCardViewActivity extends CatimaAppCompatActivity implements return wrappedIcon; } + private void setCenterGuideline(int zoomLevel) { + float scale = zoomLevel / 100f; + + if (format != null && format.isSquare()) { + centerGuideline.setGuidelinePercent(0.75f * scale); + } else { + centerGuideline.setGuidelinePercent(0.5f * scale); + } + + } + @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); @@ -219,6 +247,7 @@ public class LoyaltyCardViewActivity extends CatimaAppCompatActivity implements if (savedInstanceState != null) { mainImageIndex = savedInstanceState.getInt(STATE_IMAGEINDEX); isFullscreen = savedInstanceState.getBoolean(STATE_FULLSCREEN); + bottomSheetState = savedInstanceState.getInt(STATE_BOTTOMSHEET); } settings = new Settings(this); @@ -230,6 +259,7 @@ public class LoyaltyCardViewActivity extends CatimaAppCompatActivity implements db = new DBHelper(this); importURIHelper = new ImportURIHelper(this); + coordinatorLayout = findViewById(R.id.coordinator_layout); cardIdFieldView = findViewById(R.id.cardIdView); bottomSheet = findViewById(R.id.bottom_sheet); bottomSheetContentWrapper = findViewById(R.id.bottomSheetContentWrapper); @@ -245,6 +275,8 @@ public class LoyaltyCardViewActivity extends CatimaAppCompatActivity implements minimizeButton = findViewById(R.id.minimizeButton); collapsingToolbarLayout = findViewById(R.id.collapsingToolbarLayout); appBarLayout = findViewById(R.id.app_bar_layout); + iconImage = findViewById(R.id.icon_image); + landscapeToolbar = findViewById(R.id.toolbar_landscape); centerGuideline = findViewById(R.id.centerGuideline); barcodeScaler = findViewById(R.id.barcodeScaler); @@ -254,21 +286,19 @@ public class LoyaltyCardViewActivity extends CatimaAppCompatActivity implements barcodeScaler.setOnSeekBarChangeListener(new SeekBar.OnSeekBarChangeListener() { @Override public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) { + if (!fromUser) { + Log.d(TAG, "non user triggered onProgressChanged, ignoring, progress is " + progress); + return; + } Log.d(TAG, "Progress is " + progress); Log.d(TAG, "Max is " + barcodeScaler.getMax()); float scale = (float) progress / (float) barcodeScaler.getMax(); Log.d(TAG, "Scaling to " + scale); - if(isFullscreen){ - loyaltyCard.zoomLevel = progress; - db.updateLoyaltyCardZoomLevel(loyaltyCardId, loyaltyCard.zoomLevel); - } + loyaltyCard.zoomLevel = progress; + db.updateLoyaltyCardZoomLevel(loyaltyCardId, loyaltyCard.zoomLevel); - if (format != null && format.isSquare()) { - centerGuideline.setGuidelinePercent(0.75f * scale); - } else { - centerGuideline.setGuidelinePercent(0.5f * scale); - } + setCenterGuideline(loyaltyCard.zoomLevel); drawMainImage(mainImageIndex, true); } @@ -306,20 +336,7 @@ public class LoyaltyCardViewActivity extends CatimaAppCompatActivity implements behavior.addBottomSheetCallback(new BottomSheetBehavior.BottomSheetCallback() { @Override public void onStateChanged(@NonNull View bottomSheet, int newState) { - if (newState == BottomSheetBehavior.STATE_DRAGGING) { - editButton.hide(); - } else if (newState == BottomSheetBehavior.STATE_EXPANDED) { - bottomSheetButton.setImageResource(R.drawable.ic_baseline_arrow_drop_down_24); - editButton.hide(); - } else if (newState == BottomSheetBehavior.STATE_COLLAPSED) { - bottomSheetButton.setImageResource(R.drawable.ic_baseline_arrow_drop_up_24); - if (!isFullscreen) { - editButton.show(); - } - - // Scroll bottomsheet content back to top - bottomSheetContentWrapper.setScrollY(0); - } + changeUiToBottomSheetState(newState); } @Override @@ -335,22 +352,18 @@ public class LoyaltyCardViewActivity extends CatimaAppCompatActivity implements } }); - // Fix bottom sheet content sizing - ViewTreeObserver viewTreeObserver = bottomSheet.getViewTreeObserver(); - viewTreeObserver.addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() { + appBarLayout.addOnLayoutChangeListener(new View.OnLayoutChangeListener() { @Override - public void onGlobalLayout() { - bottomSheet.getViewTreeObserver().removeOnGlobalLayoutListener(this); - DisplayMetrics displayMetrics = getResources().getDisplayMetrics(); - int height = displayMetrics.heightPixels; - int maxHeight = height - appBarLayout.getHeight() - bottomSheetButton.getHeight(); - Log.d(TAG, "Button sheet should be " + maxHeight + " pixels high"); - bottomSheetContentWrapper.setLayoutParams( - new LinearLayout.LayoutParams( - LinearLayout.LayoutParams.MATCH_PARENT, - maxHeight - ) - ); + public void onLayoutChange(View v, int left, int top, int right, int bottom, int oldLeft, int oldTop, int oldRight, int oldBottom) { + adjustLayoutHeights(); + } + }); + + appBarLayout.setOutlineProvider(new ViewOutlineProvider() { + @Override + public void getOutline(View view, Outline outline) { + ViewOutlineProvider.BACKGROUND.getOutline(view, outline); + outline.setAlpha(0f); } }); @@ -359,6 +372,53 @@ public class LoyaltyCardViewActivity extends CatimaAppCompatActivity implements mainImage.setOnTouchListener(gestureTouchListener); } + private void changeUiToBottomSheetState(int newState) { + if (newState == BottomSheetBehavior.STATE_DRAGGING) { + editButton.hide(); + } else if (newState == BottomSheetBehavior.STATE_EXPANDED) { + bottomSheetButton.setImageResource(R.drawable.ic_baseline_arrow_drop_down_24); + editButton.hide(); + } else if (newState == BottomSheetBehavior.STATE_COLLAPSED) { + bottomSheetButton.setImageResource(R.drawable.ic_baseline_arrow_drop_up_24); + if (!isFullscreen) { + editButton.show(); + } + + // Scroll bottomsheet content back to top + bottomSheetContentWrapper.setScrollY(0); + } + bottomSheetState = newState; + } + + private void adjustLayoutHeights() { + // use getLayoutParams instead of getHeight when heights are pre-determined in xml! getHeight could return 0 if a View is not inflated + if (iconImage.getLayoutParams().height != appBarLayout.getHeight()) { + Log.d("adjustLayoutHeights", "setting imageIcon height from: " + iconImage.getLayoutParams().height + " to: " + appBarLayout.getHeight()); + iconImage.setLayoutParams(new CoordinatorLayout.LayoutParams( + CoordinatorLayout.LayoutParams.MATCH_PARENT, appBarLayout.getHeight()) + ); + } + int bottomSheetHeight = getResources().getDisplayMetrics().heightPixels - appBarLayout.getHeight() - bottomSheetButton.getLayoutParams().height; + ViewGroup.LayoutParams params = bottomSheetContentWrapper.getLayoutParams(); + if (params.height != bottomSheetHeight || params.width != LinearLayout.LayoutParams.MATCH_PARENT) { + // XXX android 5 - 9 has so much quirks with setting bottomSheetContent height + // just invalidate the wrapper works on 10 onward + // bottomSheetContentWrapper.invalidate(); + // The below worked on android 5 but not 6, reloading the card then it breaks again on 6, entirely random :( + // for (int i = 0; i < bottomSheetContentWrapper.getChildCount(); i++) { + // bottomSheetContentWrapper.getChildAt(i).invalidate(); + // } + // since it's basically allergic to getting enlarged then shrunk again, and setting it at all when fullscreen makes no sense + if (!isFullscreen) { + Log.d("adjustLayoutHeights", "setting bottomSheet height from: " + params.height + " to: " + bottomSheetHeight); + bottomSheetContentWrapper.setLayoutParams( + new LinearLayout.LayoutParams(LinearLayout.LayoutParams.MATCH_PARENT, bottomSheetHeight) + ); + } + } + } + + @Override public void onNewIntent(Intent intent) { super.onNewIntent(intent); @@ -371,6 +431,7 @@ public class LoyaltyCardViewActivity extends CatimaAppCompatActivity implements public void onSaveInstanceState(Bundle savedInstanceState) { savedInstanceState.putInt(STATE_IMAGEINDEX, mainImageIndex); savedInstanceState.putBoolean(STATE_FULLSCREEN, isFullscreen); + savedInstanceState.putInt(STATE_BOTTOMSHEET, bottomSheetState); super.onSaveInstanceState(savedInstanceState); } @@ -489,9 +550,6 @@ public class LoyaltyCardViewActivity extends CatimaAppCompatActivity implements backgroundHeaderColor = LetterBitmap.getDefaultColor(this, loyaltyCard.store); } - collapsingToolbarLayout.setBackgroundColor(backgroundHeaderColor); - appBarLayout.setBackgroundColor(backgroundHeaderColor); - int textColor; if (Utils.needsDarkForeground(backgroundHeaderColor)) { textColor = Color.BLACK; @@ -499,7 +557,21 @@ public class LoyaltyCardViewActivity extends CatimaAppCompatActivity implements textColor = Color.WHITE; } storeName.setTextColor(textColor); - ((Toolbar) findViewById(R.id.toolbar_landscape)).setTitleTextColor(textColor); + landscapeToolbar.setTitleTextColor(textColor); + + Bitmap icon = Utils.retrieveCardImage(this, loyaltyCard.id, ImageLocationType.icon); + if (icon != null) { + int backgroundAlphaColor = Utils.needsDarkForeground(backgroundHeaderColor) ? Color.WHITE : Color.BLACK; + Log.d("onResume", "setting icon image"); + iconImage.setImageBitmap(icon); + int backgroundWithAlpha = Color.argb(HEADER_FILTER_ALPHA, Color.red(backgroundAlphaColor), Color.green(backgroundAlphaColor), Color.blue(backgroundAlphaColor)); + // for images that has alpha + appBarLayout.setBackgroundColor(backgroundWithAlpha); + } else { + Bitmap plain = Bitmap.createBitmap(new int[]{backgroundHeaderColor}, 1, 1, Bitmap.Config.ARGB_8888); + iconImage.setImageBitmap(plain); + appBarLayout.setBackgroundColor(Color.TRANSPARENT); + } // If the background is very bright, we should use dark icons backgroundNeedsDarkIcons = Utils.needsDarkForeground(backgroundHeaderColor); @@ -531,13 +603,12 @@ public class LoyaltyCardViewActivity extends CatimaAppCompatActivity implements imageTypes.add(ImageType.BARCODE); } - frontImageBitmap = Utils.retrieveCardImage(this, loyaltyCard.id, true); - backImageBitmap = Utils.retrieveCardImage(this, loyaltyCard.id, false); - + frontImageBitmap = Utils.retrieveCardImage(this, loyaltyCard.id, ImageLocationType.front); if (frontImageBitmap != null) { imageTypes.add(ImageType.IMAGE_FRONT); } + backImageBitmap = Utils.retrieveCardImage(this, loyaltyCard.id, ImageLocationType.back); if (backImageBitmap != null) { imageTypes.add(ImageType.IMAGE_BACK); } @@ -545,10 +616,11 @@ public class LoyaltyCardViewActivity extends CatimaAppCompatActivity implements dotIndicator.removeAllViews(); if (imageTypes.size() >= 2) { dots = new ImageView[imageTypes.size()]; + boolean darkMode = Utils.isDarkModeEnabled(getApplicationContext()); for (int i = 0; i < imageTypes.size(); i++) { dots[i] = new ImageView(this); - dots[i].setImageDrawable(getDotIcon(false)); + dots[i].setImageDrawable(getDotIcon(false, darkMode)); LinearLayout.LayoutParams params = new LinearLayout.LayoutParams(LinearLayout.LayoutParams.WRAP_CONTENT, LinearLayout.LayoutParams.WRAP_CONTENT); params.setMargins(8, 0, 8, 0); @@ -561,6 +633,9 @@ public class LoyaltyCardViewActivity extends CatimaAppCompatActivity implements setFullscreen(isFullscreen); + // restore bottomSheet UI states from changing orientation + changeUiToBottomSheetState(bottomSheetState); + db.updateLoyaltyCardLastUsed(loyaltyCard.id); } @@ -647,7 +722,6 @@ public class LoyaltyCardViewActivity extends CatimaAppCompatActivity implements private void setupOrientation() { Toolbar portraitToolbar = findViewById(R.id.toolbar); - Toolbar landscapeToolbar = findViewById(R.id.toolbar_landscape); int orientation = getResources().getConfiguration().orientation; if (orientation == Configuration.ORIENTATION_LANDSCAPE) { @@ -736,8 +810,9 @@ public class LoyaltyCardViewActivity extends CatimaAppCompatActivity implements } if (dots != null) { + boolean darkMode = Utils.isDarkModeEnabled(getApplicationContext()); for (int i = 0; i < dots.length; i++) { - dots[i].setImageDrawable(getDotIcon(i == index)); + dots[i].setImageDrawable(getDotIcon(i == index, darkMode)); } } @@ -781,7 +856,7 @@ public class LoyaltyCardViewActivity extends CatimaAppCompatActivity implements /** * When enabled, hides the status bar and moves the barcode to the top of the screen. - * + *

* The purpose of this function is to make sure the barcode can be scanned from the phone * by machines which offer no space to insert the complete device. */ @@ -795,6 +870,7 @@ public class LoyaltyCardViewActivity extends CatimaAppCompatActivity implements drawMainImage(mainImageIndex, true); barcodeScaler.setProgress(loyaltyCard.zoomLevel); + setCenterGuideline(loyaltyCard.zoomLevel); // Hide maximize and show minimize button and scaler maximizeButton.setVisibility(View.GONE); @@ -813,7 +889,7 @@ public class LoyaltyCardViewActivity extends CatimaAppCompatActivity implements // Don't ask me why... appBarLayout.setVisibility(View.INVISIBLE); collapsingToolbarLayout.setVisibility(View.GONE); - findViewById(R.id.toolbar_landscape).setVisibility(View.GONE); + landscapeToolbar.setVisibility(View.GONE); // Hide other UI elements cardIdFieldView.setVisibility(View.GONE); @@ -821,6 +897,9 @@ public class LoyaltyCardViewActivity extends CatimaAppCompatActivity implements behavior.setState(BottomSheetBehavior.STATE_COLLAPSED); editButton.hide(); + // android 5-9, avoid padding growing on top of bottomSheet + coordinatorLayout.removeView(bottomSheet); + // Set Android to fullscreen mode getWindow().getDecorView().setSystemUiVisibility( getWindow().getDecorView().getSystemUiVisibility() @@ -831,7 +910,7 @@ public class LoyaltyCardViewActivity extends CatimaAppCompatActivity implements Log.d(TAG, "Move out of fullscreen"); // Reset center guideline - barcodeScaler.setProgress(100); + setCenterGuideline(100); drawMainImage(mainImageIndex, true); @@ -849,8 +928,6 @@ public class LoyaltyCardViewActivity extends CatimaAppCompatActivity implements // Show appropriate toolbar // And restore 24dp paddingTop for appBarLayout appBarLayout.setVisibility(View.VISIBLE); - DisplayMetrics metrics = new DisplayMetrics(); - getWindowManager().getDefaultDisplay().getMetrics(metrics); setupOrientation(); // Show other UI elements @@ -864,8 +941,13 @@ public class LoyaltyCardViewActivity extends CatimaAppCompatActivity implements & ~View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY & ~View.SYSTEM_UI_FLAG_FULLSCREEN ); + + // android 5-9, avoid padding growing on top of bottomSheet + if (bottomSheet.getParent() != coordinatorLayout) { + coordinatorLayout.addView(bottomSheet); + } } - Log.d("setFullScreen","Is full screen enabled? "+enabled+" Zoom Level = "+barcodeScaler.getProgress()); + Log.d("setFullScreen", "Is full screen enabled? " + enabled + " Zoom Level = " + barcodeScaler.getProgress()); } } diff --git a/app/src/main/java/protect/card_locker/MainActivity.java b/app/src/main/java/protect/card_locker/MainActivity.java index 7092e9be7..5155041bc 100644 --- a/app/src/main/java/protect/card_locker/MainActivity.java +++ b/app/src/main/java/protect/card_locker/MainActivity.java @@ -6,7 +6,6 @@ import android.content.ClipboardManager; import android.content.Context; import android.content.Intent; import android.content.SharedPreferences; -import android.content.res.Configuration; import android.database.Cursor; import android.database.CursorIndexOutOfBoundsException; import android.os.Bundle; @@ -27,17 +26,18 @@ import java.util.Arrays; import java.util.List; import java.util.concurrent.atomic.AtomicInteger; +import androidx.activity.result.ActivityResultLauncher; +import androidx.activity.result.contract.ActivityResultContracts; import androidx.appcompat.app.AlertDialog; import androidx.appcompat.view.ActionMode; import androidx.appcompat.widget.SearchView; import androidx.appcompat.widget.Toolbar; -import androidx.core.app.ActivityCompat; import androidx.core.splashscreen.SplashScreen; import androidx.recyclerview.widget.RecyclerView; + import protect.card_locker.preferences.SettingsActivity; -public class MainActivity extends CatimaAppCompatActivity implements LoyaltyCardCursorAdapter.CardAdapterListener, GestureDetector.OnGestureListener -{ +public class MainActivity extends CatimaAppCompatActivity implements LoyaltyCardCursorAdapter.CardAdapterListener, GestureDetector.OnGestureListener { private static final String TAG = "Catima"; private final DBHelper mDB = new DBHelper(this); @@ -55,8 +55,10 @@ public class MainActivity extends CatimaAppCompatActivity implements LoyaltyCard private View mNoMatchingCardsText; private View mNoGroupCardsText; - private ActionMode.Callback mCurrentActionModeCallback = new ActionMode.Callback() - { + private ActivityResultLauncher mMainRequestLauncher; + private ActivityResultLauncher mBarcodeScannerLauncher; + + private ActionMode.Callback mCurrentActionModeCallback = new ActionMode.Callback() { @Override public boolean onCreateActionMode(ActionMode inputMode, Menu inputMenu) { inputMode.getMenuInflater().inflate(R.menu.card_longclick_menu, inputMenu); @@ -64,8 +66,7 @@ public class MainActivity extends CatimaAppCompatActivity implements LoyaltyCard } @Override - public boolean onPrepareActionMode(ActionMode inputMode, Menu inputMenu) - { + public boolean onPrepareActionMode(ActionMode inputMode, Menu inputMenu) { return false; } @@ -109,7 +110,7 @@ public class MainActivity extends CatimaAppCompatActivity implements LoyaltyCard } inputMode.finish(); return true; - } else if(inputItem.getItemId() == R.id.action_edit) { + } else if (inputItem.getItemId() == R.id.action_edit) { if (mAdapter.getSelectedItemCount() != 1) { throw new IllegalArgumentException("Cannot edit more than 1 card at a time"); } @@ -122,7 +123,7 @@ public class MainActivity extends CatimaAppCompatActivity implements LoyaltyCard startActivity(intent); inputMode.finish(); return true; - } else if(inputItem.getItemId() == R.id.action_delete) { + } else if (inputItem.getItemId() == R.id.action_delete) { AlertDialog.Builder builder = new AlertDialog.Builder(MainActivity.this); // The following may seem weird, but it is necessary to give translators enough flexibility. // For example, in Russian, Android's plural quantity "one" actually refers to "any number ending on 1 but not ending in 11". @@ -165,15 +166,12 @@ public class MainActivity extends CatimaAppCompatActivity implements LoyaltyCard } @Override - public void onDestroyActionMode(ActionMode inputMode) - { + public void onDestroyActionMode(ActionMode inputMode) { mAdapter.clearSelections(); mCurrentActionMode = null; - mCardList.post(new Runnable() - { + mCardList.post(new Runnable() { @Override - public void run() - { + public void run() { mAdapter.resetAnimationIndex(); } }); @@ -181,8 +179,7 @@ public class MainActivity extends CatimaAppCompatActivity implements LoyaltyCard }; @Override - protected void onCreate(Bundle inputSavedInstanceState) - { + protected void onCreate(Bundle inputSavedInstanceState) { super.onCreate(inputSavedInstanceState); SplashScreen.installSplashScreen(this); setTitle(R.string.app_name); @@ -195,7 +192,7 @@ public class MainActivity extends CatimaAppCompatActivity implements LoyaltyCard @Override public void onTabSelected(TabLayout.Tab tab) { selectedTab = tab.getPosition(); - Log.d("onTabSelected","Tab Position "+tab.getPosition()); + Log.d("onTabSelected", "Tab Position " + tab.getPosition()); mGroup = tab.getTag(); updateLoyaltyCardList(); // Store active tab in Shared Preference to restore next app launch @@ -269,25 +266,49 @@ public class MainActivity extends CatimaAppCompatActivity implements LoyaltyCard .show(); } */ + + mMainRequestLauncher = registerForActivityResult(new ActivityResultContracts.StartActivityForResult(), result -> { + // We're coming back from another view so clear the search + // We only do this now to prevent a flash of all entries right after picking one + mFilter = ""; + if (mMenu != null) { + MenuItem searchItem = mMenu.findItem(R.id.action_search); + searchItem.collapseActionView(); + } + }); + + mBarcodeScannerLauncher = registerForActivityResult(new ActivityResultContracts.StartActivityForResult(), result -> { + Intent intent = result.getData(); + BarcodeValues barcodeValues = Utils.parseSetBarcodeActivityResult(Utils.BARCODE_SCAN, result.getResultCode(), intent, this); + + if (!barcodeValues.isEmpty()) { + Intent newIntent = new Intent(getApplicationContext(), LoyaltyCardEditActivity.class); + Bundle newBundle = new Bundle(); + newBundle.putString(LoyaltyCardEditActivity.BUNDLE_BARCODETYPE, barcodeValues.format()); + newBundle.putString(LoyaltyCardEditActivity.BUNDLE_CARDID, barcodeValues.content()); + Bundle inputBundle = intent.getExtras(); + if (inputBundle != null && inputBundle.getString(LoyaltyCardEditActivity.BUNDLE_ADDGROUP) != null) { + newBundle.putString(LoyaltyCardEditActivity.BUNDLE_ADDGROUP, inputBundle.getString(LoyaltyCardEditActivity.BUNDLE_ADDGROUP)); + } + newIntent.putExtras(newBundle); + startActivity(newIntent); + } + }); } @Override - protected void onResume() - { + protected void onResume() { super.onResume(); - if(mCurrentActionMode != null) - { + if (mCurrentActionMode != null) { mAdapter.clearSelections(); mCurrentActionMode.finish(); } - if (mMenu != null) - { + if (mMenu != null) { SearchView searchView = (SearchView) mMenu.findItem(R.id.action_search).getActionView(); - if (!searchView.isIconified()) - { + if (!searchView.isIconified()) { mFilter = searchView.getQuery().toString(); } } @@ -307,7 +328,8 @@ public class MainActivity extends CatimaAppCompatActivity implements LoyaltyCard try { mOrder = DBHelper.LoyaltyCardOrder.valueOf(sortPref.getString(getString(R.string.sharedpreference_sort_order), null)); mOrderDirection = DBHelper.LoyaltyCardOrderDirection.valueOf(sortPref.getString(getString(R.string.sharedpreference_sort_direction), null)); - } catch (IllegalArgumentException | NullPointerException ignored) {} + } catch (IllegalArgumentException | NullPointerException ignored) { + } mGroup = null; @@ -332,48 +354,13 @@ public class MainActivity extends CatimaAppCompatActivity implements LoyaltyCard bundle.putString(LoyaltyCardEditActivity.BUNDLE_ADDGROUP, groupsTabLayout.getTabAt(selectedTab).getText().toString()); } intent.putExtras(bundle); - startActivityForResult(intent, Utils.BARCODE_SCAN); + mBarcodeScannerLauncher.launch(intent); }); addButton.bringToFront(); } @Override - protected void onActivityResult(int requestCode, int resultCode, Intent intent) { - super.onActivityResult(requestCode, resultCode, intent); - - if (requestCode == Utils.MAIN_REQUEST) { - // We're coming back from another view so clear the search - // We only do this now to prevent a flash of all entries right after picking one - mFilter = ""; - if (mMenu != null) - { - MenuItem searchItem = mMenu.findItem(R.id.action_search); - searchItem.collapseActionView(); - } - ActivityCompat.recreate(this); - - return; - } - - BarcodeValues barcodeValues = Utils.parseSetBarcodeActivityResult(requestCode, resultCode, intent, this); - - if(!barcodeValues.isEmpty()) { - Intent newIntent = new Intent(getApplicationContext(), LoyaltyCardEditActivity.class); - Bundle newBundle = new Bundle(); - newBundle.putString(LoyaltyCardEditActivity.BUNDLE_BARCODETYPE, barcodeValues.format()); - newBundle.putString(LoyaltyCardEditActivity.BUNDLE_CARDID, barcodeValues.content()); - Bundle inputBundle = intent.getExtras(); - if (inputBundle != null && inputBundle.getString(LoyaltyCardEditActivity.BUNDLE_ADDGROUP) != null) { - newBundle.putString(LoyaltyCardEditActivity.BUNDLE_ADDGROUP, inputBundle.getString(LoyaltyCardEditActivity.BUNDLE_ADDGROUP)); - } - newIntent.putExtras(newBundle); - startActivity(newIntent); - } - } - - @Override - public void onBackPressed() - { + public void onBackPressed() { if (mMenu != null) { SearchView searchView = (SearchView) mMenu.findItem(R.id.action_search).getActionView(); @@ -394,20 +381,16 @@ public class MainActivity extends CatimaAppCompatActivity implements LoyaltyCard mAdapter.swapCursor(mDB.getLoyaltyCardCursor(mFilter, group, mOrder, mOrderDirection)); - if(mDB.getLoyaltyCardCount() > 0) - { + if (mDB.getLoyaltyCardCount() > 0) { // We want the cardList to be visible regardless of the filtered match count // to ensure that the noMatchingCardsText doesn't end up being shown below // the keyboard mHelpText.setVisibility(View.GONE); mNoGroupCardsText.setVisibility(View.GONE); - if(mAdapter.getItemCount() > 0) - { + if (mAdapter.getItemCount() > 0) { mCardList.setVisibility(View.VISIBLE); mNoMatchingCardsText.setVisibility(View.GONE); - } - else - { + } else { mCardList.setVisibility(View.GONE); if (!mFilter.isEmpty()) { // Actual Empty Search Result @@ -419,9 +402,7 @@ public class MainActivity extends CatimaAppCompatActivity implements LoyaltyCard mNoGroupCardsText.setVisibility(View.VISIBLE); } } - } - else - { + } else { mCardList.setVisibility(View.GONE); mHelpText.setVisibility(View.VISIBLE); mNoMatchingCardsText.setVisibility(View.GONE); @@ -433,8 +414,7 @@ public class MainActivity extends CatimaAppCompatActivity implements LoyaltyCard } } - public void updateTabGroups(TabLayout groupsTabLayout) - { + public void updateTabGroups(TabLayout groupsTabLayout) { final DBHelper db = new DBHelper(this); List newGroups = db.getGroups(); @@ -463,15 +443,13 @@ public class MainActivity extends CatimaAppCompatActivity implements LoyaltyCard } @Override - public boolean onCreateOptionsMenu(Menu inputMenu) - { + public boolean onCreateOptionsMenu(Menu inputMenu) { this.mMenu = inputMenu; getMenuInflater().inflate(R.menu.main_menu, inputMenu); SearchManager searchManager = (SearchManager) getSystemService(Context.SEARCH_SERVICE); - if (searchManager != null) - { + if (searchManager != null) { SearchView searchView = (SearchView) inputMenu.findItem(R.id.action_search).getActionView(); searchView.setSearchableInfo(searchManager.getSearchableInfo(getComponentName())); searchView.setSubmitButtonEnabled(false); @@ -481,17 +459,14 @@ public class MainActivity extends CatimaAppCompatActivity implements LoyaltyCard return false; }); - searchView.setOnQueryTextListener(new SearchView.OnQueryTextListener() - { + searchView.setOnQueryTextListener(new SearchView.OnQueryTextListener() { @Override - public boolean onQueryTextSubmit(String query) - { + public boolean onQueryTextSubmit(String query) { return false; } @Override - public boolean onQueryTextChange(String newText) - { + public boolean onQueryTextChange(String newText) { mFilter = newText; TabLayout groupsTabLayout = findViewById(R.id.groups); @@ -508,12 +483,26 @@ public class MainActivity extends CatimaAppCompatActivity implements LoyaltyCard } @Override - public boolean onOptionsItemSelected(MenuItem inputItem) - { + public boolean onOptionsItemSelected(MenuItem inputItem) { int id = inputItem.getItemId(); - if (id == R.id.action_sort) - { + if (id == R.id.action_unfold) { + boolean shouldShow = !mAdapter.showingDetails(); + + if (shouldShow) { + inputItem.setIcon(R.drawable.ic_baseline_unfold_less_24); + inputItem.setTitle(R.string.action_hide_details); + } else { + inputItem.setIcon(R.drawable.ic_baseline_unfold_more_24); + inputItem.setTitle(R.string.action_show_details); + } + + mAdapter.showDetails(shouldShow); + + return true; + } + + if (id == R.id.action_sort) { TabLayout.Tab tab = ((TabLayout) findViewById(R.id.groups)).getTabAt(selectedTab); AtomicInteger currentIndex = new AtomicInteger(); List loyaltyCardOrders = Arrays.asList(DBHelper.LoyaltyCardOrder.values()); @@ -536,10 +525,9 @@ public class MainActivity extends CatimaAppCompatActivity implements LoyaltyCard builder.setSingleChoiceItems(R.array.sort_types_array, currentIndex.get(), (dialog, which) -> currentIndex.set(which)); builder.setPositiveButton(R.string.sort, (dialog, which) -> { - if(ch.isChecked()) { + if (ch.isChecked()) { setSort(loyaltyCardOrders.get(currentIndex.get()), DBHelper.LoyaltyCardOrderDirection.Descending); - } - else { + } else { setSort(loyaltyCardOrders.get(currentIndex.get()), DBHelper.LoyaltyCardOrderDirection.Ascending); } dialog.dismiss(); @@ -553,31 +541,27 @@ public class MainActivity extends CatimaAppCompatActivity implements LoyaltyCard return true; } - if (id == R.id.action_manage_groups) - { + if (id == R.id.action_manage_groups) { Intent i = new Intent(getApplicationContext(), ManageGroupsActivity.class); - startActivityForResult(i, Utils.MAIN_REQUEST); + mMainRequestLauncher.launch(i); return true; } - if (id == R.id.action_import_export) - { + if (id == R.id.action_import_export) { Intent i = new Intent(getApplicationContext(), ImportExportActivity.class); - startActivityForResult(i, Utils.MAIN_REQUEST); + mMainRequestLauncher.launch(i); return true; } - if (id == R.id.action_settings) - { + if (id == R.id.action_settings) { Intent i = new Intent(getApplicationContext(), SettingsActivity.class); - startActivityForResult(i, Utils.MAIN_REQUEST); + mMainRequestLauncher.launch(i); return true; } - if (id == R.id.action_about) - { + if (id == R.id.action_about) { Intent i = new Intent(getApplicationContext(), AboutActivity.class); - startActivityForResult(i, Utils.MAIN_REQUEST); + mMainRequestLauncher.launch(i); return true; } @@ -602,13 +586,6 @@ public class MainActivity extends CatimaAppCompatActivity implements LoyaltyCard updateLoyaltyCardList(); } - protected static boolean isDarkModeEnabled(Context inputContext) - { - Configuration config = inputContext.getResources().getConfiguration(); - int currentNightMode = config.uiMode & Configuration.UI_MODE_NIGHT_MASK; - return (currentNightMode == Configuration.UI_MODE_NIGHT_YES); - } - @Override public boolean onDown(MotionEvent e) { return false; @@ -655,10 +632,10 @@ public class MainActivity extends CatimaAppCompatActivity implements LoyaltyCard } Integer currentTab = groupsTabLayout.getSelectedTabPosition(); - Log.d("onFling","Current Tab "+currentTab); + Log.d("onFling", "Current Tab " + currentTab); // Swipe right if (velocityX < -150) { - Log.d("onFling","Right Swipe detected "+velocityX); + Log.d("onFling", "Right Swipe detected " + velocityX); Integer nextTab = currentTab + 1; if (nextTab == groupsTabLayout.getTabCount()) { @@ -672,7 +649,7 @@ public class MainActivity extends CatimaAppCompatActivity implements LoyaltyCard // Swipe left if (velocityX > 150) { - Log.d("onFling","Left Swipe detected "+velocityX); + Log.d("onFling", "Left Swipe detected " + velocityX); Integer nextTab = currentTab - 1; if (nextTab < 0) { @@ -688,22 +665,18 @@ public class MainActivity extends CatimaAppCompatActivity implements LoyaltyCard } @Override - public void onRowLongClicked(int inputPosition) - { + public void onRowLongClicked(int inputPosition) { enableActionMode(inputPosition); } - private void enableActionMode(int inputPosition) - { - if (mCurrentActionMode == null) - { + private void enableActionMode(int inputPosition) { + if (mCurrentActionMode == null) { mCurrentActionMode = startSupportActionMode(mCurrentActionModeCallback); } toggleSelection(inputPosition); } - private void toggleSelection(int inputPosition) - { + private void toggleSelection(int inputPosition) { mAdapter.toggleSelection(inputPosition); int count = mAdapter.getSelectedItemCount(); @@ -725,16 +698,11 @@ public class MainActivity extends CatimaAppCompatActivity implements LoyaltyCard } } - @Override - public void onRowClicked(int inputPosition) - { - if (mAdapter.getSelectedItemCount() > 0) - { + public void onRowClicked(int inputPosition) { + if (mAdapter.getSelectedItemCount() > 0) { enableActionMode(inputPosition); - } - else - { + } else { Cursor selected = mAdapter.getCursor(); selected.moveToPosition(inputPosition); // FIXME @@ -761,7 +729,7 @@ public class MainActivity extends CatimaAppCompatActivity implements LoyaltyCard ShortcutHelper.updateShortcuts(MainActivity.this, loyaltyCard); - startActivityForResult(i, Utils.MAIN_REQUEST); + mMainRequestLauncher.launch(i); } } } diff --git a/app/src/main/java/protect/card_locker/ManageGroupActivity.java b/app/src/main/java/protect/card_locker/ManageGroupActivity.java new file mode 100644 index 000000000..8e0df0883 --- /dev/null +++ b/app/src/main/java/protect/card_locker/ManageGroupActivity.java @@ -0,0 +1,216 @@ +package protect.card_locker; + + +import android.content.Intent; +import android.os.Bundle; +import android.text.Editable; +import android.text.TextWatcher; +import android.util.Log; +import android.view.View; +import android.widget.EditText; +import android.widget.TextView; +import android.widget.Toast; + +import com.google.android.material.floatingactionbutton.FloatingActionButton; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.Map; + +import androidx.annotation.NonNull; +import androidx.appcompat.app.ActionBar; +import androidx.appcompat.app.AlertDialog; +import androidx.appcompat.widget.Toolbar; +import androidx.recyclerview.widget.RecyclerView; + +public class ManageGroupActivity extends CatimaAppCompatActivity implements ManageGroupCursorAdapter.CardAdapterListener { + + private final DBHelper mDB = new DBHelper(this); + private ManageGroupCursorAdapter mAdapter; + + private final String SAVE_INSTANCE_ADAPTER_STATE = "adapterState"; + private final String SAVE_INSTANCE_CURRENT_GROUP_NAME = "currentGroupName"; + + protected Group mGroup = null; + private RecyclerView mCardList; + private TextView mHelpText; + private EditText mGroupNameText; + + private boolean mGroupNameNotInUse; + + @Override + protected void onCreate(Bundle inputSavedInstanceState) { + super.onCreate(inputSavedInstanceState); + setContentView(R.layout.activity_manage_group); + Toolbar toolbar = findViewById(R.id.toolbar); + setSupportActionBar(toolbar); + + mHelpText = findViewById(R.id.helpText); + mCardList = findViewById(R.id.list); + FloatingActionButton saveButton = findViewById(R.id.fabSave); + + mGroupNameText = findViewById(R.id.editTextGroupName); + + mGroupNameText.addTextChangedListener(new TextWatcher() { + @Override + public void beforeTextChanged(CharSequence s, int start, int count, int after) { + } + + @Override + public void onTextChanged(CharSequence s, int start, int before, int count) { + } + + @Override + public void afterTextChanged(Editable s) { + mGroupNameNotInUse = true; + mGroupNameText.setError(null); + String currentGroupName = mGroupNameText.getText().toString().trim(); + if (currentGroupName.length() == 0) { + mGroupNameText.setError(getResources().getText(R.string.group_name_is_empty)); + return; + } + if (!mGroup._id.equals(currentGroupName)) { + if (mDB.getGroup(currentGroupName) != null) { + mGroupNameNotInUse = false; + mGroupNameText.setError(getResources().getText(R.string.group_name_already_in_use)); + } else { + mGroupNameNotInUse = true; + } + } + } + }); + + Intent intent = getIntent(); + String groupId = intent.getStringExtra("group"); + if (groupId == null) { + throw (new IllegalArgumentException("this activity expects a group loaded into it's intent")); + } + Log.d("groupId", "groupId: " + groupId); + mGroup = mDB.getGroup(groupId); + if (mGroup == null) { + throw (new IllegalArgumentException("cannot load group " + groupId + " from database")); + } + mGroupNameText.setText(mGroup._id); + setTitle(getString(R.string.editGroup, mGroup._id)); + mAdapter = new ManageGroupCursorAdapter(this, null, this, mGroup); + mCardList.setAdapter(mAdapter); + registerForContextMenu(mCardList); + + if (inputSavedInstanceState != null) { + mAdapter.importInGroupState(integerArrayToAdapterState(inputSavedInstanceState.getIntegerArrayList(SAVE_INSTANCE_ADAPTER_STATE))); + mGroupNameText.setText(inputSavedInstanceState.getString(SAVE_INSTANCE_CURRENT_GROUP_NAME)); + } + + ActionBar actionBar = getSupportActionBar(); + if (actionBar == null) { + throw (new RuntimeException("mActionBar is not expected to be null here")); + } + actionBar.setDisplayHomeAsUpEnabled(true); + actionBar.setDisplayShowHomeEnabled(true); + + saveButton.setOnClickListener(v -> { + String currentGroupName = mGroupNameText.getText().toString().trim(); + if (!currentGroupName.equals(mGroup._id)) { + if (currentGroupName.length() == 0) { + Toast.makeText(getApplicationContext(), R.string.group_name_is_empty, Toast.LENGTH_SHORT).show(); + return; + } + if (!mGroupNameNotInUse) { + Toast.makeText(getApplicationContext(), R.string.group_name_already_in_use, Toast.LENGTH_SHORT).show(); + return; + } + } + + mAdapter.commitToDatabase(); + if (!currentGroupName.equals(mGroup._id)) { + mDB.updateGroup(mGroup._id, currentGroupName); + } + Toast.makeText(getApplicationContext(), R.string.group_updated, Toast.LENGTH_SHORT).show(); + finish(); + }); + // this setText is here because content_main.xml is reused from main activity + mHelpText.setText(getResources().getText(R.string.noGiftCardsGroup)); + updateLoyaltyCardList(); + } + + private ArrayList adapterStateToIntegerArray(HashMap adapterState) { + ArrayList ret = new ArrayList<>(adapterState.size() * 2); + for (Map.Entry entry : adapterState.entrySet()) { + ret.add(entry.getKey()); + ret.add(entry.getValue() ? 1 : 0); + } + return ret; + } + + private HashMap integerArrayToAdapterState(ArrayList in) { + HashMap ret = new HashMap<>(); + if (in.size() % 2 != 0) { + throw (new RuntimeException("failed restoring adapterState from integer array list")); + } + for (int i = 0; i < in.size(); i += 2) { + ret.put(in.get(i), in.get(i + 1) == 1); + } + return ret; + } + + + @Override + protected void onSaveInstanceState(@NonNull Bundle outState) { + super.onSaveInstanceState(outState); + + outState.putIntegerArrayList(SAVE_INSTANCE_ADAPTER_STATE, adapterStateToIntegerArray(mAdapter.exportInGroupState())); + outState.putString(SAVE_INSTANCE_CURRENT_GROUP_NAME, mGroupNameText.getText().toString()); + } + + private void updateLoyaltyCardList() { + mAdapter.swapCursor(mDB.getLoyaltyCardCursor()); + + if (mAdapter.getCountFromCursor() == 0) { + mCardList.setVisibility(View.GONE); + mHelpText.setVisibility(View.VISIBLE); + } else { + mCardList.setVisibility(View.VISIBLE); + mHelpText.setVisibility(View.GONE); + } + } + + private void leaveWithoutSaving() { + if (hasChanged()) { + AlertDialog.Builder builder = new AlertDialog.Builder(ManageGroupActivity.this); + builder.setTitle(R.string.leaveWithoutSaveTitle); + builder.setMessage(R.string.leaveWithoutSaveConfirmation); + builder.setPositiveButton(R.string.confirm, (dialog, which) -> finish()); + builder.setNegativeButton(R.string.cancel, (dialog, which) -> dialog.dismiss()); + AlertDialog dialog = builder.create(); + dialog.show(); + } else { + finish(); + } + } + + @Override + public void onBackPressed() { + leaveWithoutSaving(); + } + + @Override + public boolean onSupportNavigateUp() { + onBackPressed(); + return true; + } + + private boolean hasChanged() { + return mAdapter.hasChanged() || !mGroup._id.equals(mGroupNameText.getText().toString().trim()); + } + + @Override + public void onRowLongClicked(int inputPosition) { + // do nothing for now + } + + @Override + public void onRowClicked(int inputPosition) { + mAdapter.toggleSelection(inputPosition); + + } +} diff --git a/app/src/main/java/protect/card_locker/ManageGroupCursorAdapter.java b/app/src/main/java/protect/card_locker/ManageGroupCursorAdapter.java new file mode 100644 index 000000000..cef4fb8d1 --- /dev/null +++ b/app/src/main/java/protect/card_locker/ManageGroupCursorAdapter.java @@ -0,0 +1,112 @@ +package protect.card_locker; + +import android.content.Context; +import android.database.Cursor; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +public class ManageGroupCursorAdapter extends LoyaltyCardCursorAdapter { + private HashMap mIndexCardMap; + private HashMap mInGroupOverlay; + private HashMap mIsLoyaltyCardInGroupCache; + private HashMap> mGetGroupCache; + final private Group mGroup; + final private DBHelper mDb; + + public ManageGroupCursorAdapter(Context inputContext, Cursor inputCursor, CardAdapterListener inputListener, Group group) { + super(inputContext, inputCursor, inputListener); + mGroup = new Group(group._id, group.order); + mInGroupOverlay = new HashMap<>(); + mDb = new DBHelper(inputContext); + } + + @Override + public void swapCursor(Cursor inputCursor) { + super.swapCursor(inputCursor); + mIndexCardMap = new HashMap<>(); + mIsLoyaltyCardInGroupCache = new HashMap<>(); + mGetGroupCache = new HashMap<>(); + } + + @Override + public void onBindViewHolder(LoyaltyCardListItemViewHolder inputHolder, Cursor inputCursor) { + LoyaltyCard loyaltyCard = LoyaltyCard.toLoyaltyCard(inputCursor); + Boolean overlayValue = mInGroupOverlay.get(loyaltyCard.id); + if ((overlayValue != null ? overlayValue : isLoyaltyCardInGroup(loyaltyCard.id))) { + mAnimationItemsIndex.put(inputCursor.getPosition(), true); + mSelectedItems.put(inputCursor.getPosition(), true); + } + mIndexCardMap.put(inputCursor.getPosition(), loyaltyCard.id); + super.onBindViewHolder(inputHolder, inputCursor); + } + + private List getGroups(int cardId) { + List cache = mGetGroupCache.get(cardId); + if (cache != null) { + return cache; + } + List groups = mDb.getLoyaltyCardGroups(cardId); + mGetGroupCache.put(cardId, groups); + return groups; + } + + private boolean isLoyaltyCardInGroup(int cardId) { + Boolean cache = mIsLoyaltyCardInGroupCache.get(cardId); + if (cache != null) { + return cache; + } + List groups = getGroups(cardId); + if (groups.contains(mGroup)) { + mIsLoyaltyCardInGroupCache.put(cardId, true); + return true; + } + mIsLoyaltyCardInGroupCache.put(cardId, false); + return false; + } + + @Override + public void toggleSelection(int inputPosition) { + super.toggleSelection(inputPosition); + Integer cardId = mIndexCardMap.get(inputPosition); + if (cardId == null) { + throw (new RuntimeException("cardId should not be null here")); + } + Boolean overlayValue = mInGroupOverlay.get(cardId); + if (overlayValue == null) { + mInGroupOverlay.put(cardId, !isLoyaltyCardInGroup(cardId)); + } else { + mInGroupOverlay.remove(cardId); + } + } + + public boolean hasChanged() { + return mInGroupOverlay.size() > 0; + } + + public void commitToDatabase() { + for (Map.Entry entry : mInGroupOverlay.entrySet()) { + int cardId = entry.getKey(); + List groups = getGroups(cardId); + if (entry.getValue()) { + groups.add(mGroup); + } else { + groups.remove(mGroup); + } + mDb.setLoyaltyCardGroups(cardId, groups); + } + } + + public void importInGroupState(HashMap cardIdInGroupMap) { + mInGroupOverlay = new HashMap<>(cardIdInGroupMap); + } + + public HashMap exportInGroupState() { + return new HashMap<>(mInGroupOverlay); + } + + public int getCountFromCursor() { + return super.getCursor().getCount(); + } +} diff --git a/app/src/main/java/protect/card_locker/ManageGroupsActivity.java b/app/src/main/java/protect/card_locker/ManageGroupsActivity.java index 60e6dd35f..2573a9440 100644 --- a/app/src/main/java/protect/card_locker/ManageGroupsActivity.java +++ b/app/src/main/java/protect/card_locker/ManageGroupsActivity.java @@ -1,6 +1,7 @@ package protect.card_locker; import android.content.Context; +import android.content.Intent; import android.content.SharedPreferences; import android.os.Bundle; import android.text.InputType; @@ -9,6 +10,7 @@ import android.view.View; import android.view.WindowManager; import android.widget.EditText; import android.widget.TextView; +import android.widget.Toast; import com.google.android.material.floatingactionbutton.FloatingActionButton; @@ -21,8 +23,7 @@ import androidx.recyclerview.widget.DefaultItemAnimator; import androidx.recyclerview.widget.LinearLayoutManager; import androidx.recyclerview.widget.RecyclerView; -public class ManageGroupsActivity extends CatimaAppCompatActivity implements GroupCursorAdapter.GroupAdapterListener -{ +public class ManageGroupsActivity extends CatimaAppCompatActivity implements GroupCursorAdapter.GroupAdapterListener { private static final String TAG = "Catima"; private final DBHelper mDb = new DBHelper(this); @@ -31,16 +32,14 @@ public class ManageGroupsActivity extends CatimaAppCompatActivity implements Gro GroupCursorAdapter mAdapter; @Override - protected void onCreate(Bundle savedInstanceState) - { + protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setTitle(R.string.groups); setContentView(R.layout.manage_groups_activity); Toolbar toolbar = findViewById(R.id.toolbar); setSupportActionBar(toolbar); ActionBar actionBar = getSupportActionBar(); - if(actionBar != null) - { + if (actionBar != null) { actionBar.setDisplayHomeAsUpEnabled(true); } } @@ -72,8 +71,7 @@ public class ManageGroupsActivity extends CatimaAppCompatActivity implements Gro super.onBackPressed(); } - private void updateGroupList() - { + private void updateGroupList() { mAdapter.swapCursor(mDb.getGroupCursor()); if (mDb.getGroupCount() == 0) { @@ -87,8 +85,7 @@ public class ManageGroupsActivity extends CatimaAppCompatActivity implements Gro mHelpText.setVisibility(View.GONE); } - private void invalidateHomescreenActiveTab() - { + private void invalidateHomescreenActiveTab() { SharedPreferences activeTabPref = getApplicationContext().getSharedPreferences( getString(R.string.sharedpreference_active_tab), Context.MODE_PRIVATE); @@ -98,8 +95,7 @@ public class ManageGroupsActivity extends CatimaAppCompatActivity implements Gro } @Override - public boolean onOptionsItemSelected(MenuItem item) - { + public boolean onOptionsItemSelected(MenuItem item) { int id = item.getItemId(); if (id == android.R.id.home) { @@ -110,14 +106,23 @@ public class ManageGroupsActivity extends CatimaAppCompatActivity implements Gro } private void createGroup() { - AlertDialog.Builder builder = new AlertDialog.Builder(this,R.style.AlertDialogTheme); + AlertDialog.Builder builder = new AlertDialog.Builder(this, R.style.AlertDialogTheme); builder.setTitle(R.string.enter_group_name); final EditText input = new EditText(this); input.setInputType(InputType.TYPE_CLASS_TEXT); builder.setView(input); builder.setPositiveButton(getString(R.string.ok), (dialog, which) -> { - mDb.insertGroup(input.getText().toString()); + String inputString = input.getText().toString().trim(); + if (inputString.length() == 0) { + Toast.makeText(getApplicationContext(), R.string.group_name_is_empty, Toast.LENGTH_SHORT).show(); + return; + } + if (mDb.getGroup(inputString) != null) { + Toast.makeText(getApplicationContext(), R.string.group_name_already_in_use, Toast.LENGTH_SHORT).show(); + return; + } + mDb.insertGroup(inputString); updateGroupList(); }); builder.setNegativeButton(getString(R.string.cancel), (dialog, which) -> dialog.cancel()); @@ -176,25 +181,9 @@ public class ManageGroupsActivity extends CatimaAppCompatActivity implements Gro @Override public void onEditButtonClicked(View view) { - final String groupName = getGroupName(view); - - AlertDialog.Builder builder = new AlertDialog.Builder(this); - builder.setTitle(R.string.enter_group_name); - final EditText input = new EditText(this); - input.setInputType(InputType.TYPE_CLASS_TEXT); - input.setText(groupName); - builder.setView(input); - - builder.setPositiveButton(getString(R.string.ok), (dialog, which) -> { - String newGroupName = input.getText().toString(); - mDb.updateGroup(groupName, newGroupName); - updateGroupList(); - }); - builder.setNegativeButton(getString(R.string.cancel), (dialog, which) -> dialog.cancel()); - AlertDialog dialog = builder.create(); - dialog.show(); - dialog.getWindow().setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_STATE_ALWAYS_VISIBLE); - input.requestFocus(); + Intent intent = new Intent(this, ManageGroupActivity.class); + intent.putExtra("group", getGroupName(view)); + startActivity(intent); } @Override diff --git a/app/src/main/java/protect/card_locker/ScanActivity.java b/app/src/main/java/protect/card_locker/ScanActivity.java index 826263c86..da2d7c763 100644 --- a/app/src/main/java/protect/card_locker/ScanActivity.java +++ b/app/src/main/java/protect/card_locker/ScanActivity.java @@ -20,12 +20,14 @@ import com.journeyapps.barcodescanner.DecoratedBarcodeView; import java.util.List; +import androidx.activity.result.ActivityResultLauncher; +import androidx.activity.result.contract.ActivityResultContracts; import androidx.appcompat.app.ActionBar; import androidx.appcompat.widget.Toolbar; /** * Custom Scannner Activity extending from Activity to display a custom layout form scanner view. - * + *

* Based on https://github.com/journeyapps/zxing-android-embedded/blob/0fdfbce9fb3285e985bad9971c5f7c0a7a334e7b/sample/src/main/java/example/zxing/CustomScannerActivity.java * originally licensed under Apache 2.0 */ @@ -39,6 +41,10 @@ public class ScanActivity extends CatimaAppCompatActivity { private String addGroup; private boolean torch = false; + private ActivityResultLauncher manualAddLauncher; + // can't use the pre-made contract because that launches the file manager for image type instead of gallery + private ActivityResultLauncher photoPickerLauncher; + private void extractIntentFields(Intent intent) { final Bundle b = intent.getExtras(); cardId = b != null ? b.getString(LoyaltyCardEditActivity.BUNDLE_CARDID) : null; @@ -54,13 +60,14 @@ public class ScanActivity extends CatimaAppCompatActivity { Toolbar toolbar = findViewById(R.id.toolbar); setSupportActionBar(toolbar); ActionBar actionBar = getSupportActionBar(); - if(actionBar != null) - { + if (actionBar != null) { actionBar.setDisplayHomeAsUpEnabled(true); } extractIntentFields(getIntent()); + manualAddLauncher = registerForActivityResult(new ActivityResultContracts.StartActivityForResult(), result -> handleActivityResult(Utils.SELECT_BARCODE_REQUEST, result.getResultCode(), result.getData())); + photoPickerLauncher = registerForActivityResult(new ActivityResultContracts.StartActivityForResult(), result -> handleActivityResult(Utils.BARCODE_IMPORT_FROM_IMAGE_FILE, result.getResultCode(), result.getData())); findViewById(R.id.add_from_image).setOnClickListener(this::addFromImage); findViewById(R.id.add_manually).setOnClickListener(this::addManually); @@ -127,8 +134,7 @@ public class ScanActivity extends CatimaAppCompatActivity { } @Override - public boolean onCreateOptionsMenu(Menu menu) - { + public boolean onCreateOptionsMenu(Menu menu) { if (getPackageManager().hasSystemFeature(PackageManager.FEATURE_CAMERA_FLASH)) { getMenuInflater().inflate(R.menu.scan_menu, menu); } @@ -139,10 +145,8 @@ public class ScanActivity extends CatimaAppCompatActivity { } @Override - public boolean onOptionsItemSelected(MenuItem item) - { - if (item.getItemId() == android.R.id.home) - { + public boolean onOptionsItemSelected(MenuItem item) { + if (item.getItemId() == android.R.id.home) { setResult(Activity.RESULT_CANCELED); finish(); return true; @@ -163,9 +167,7 @@ public class ScanActivity extends CatimaAppCompatActivity { return super.onOptionsItemSelected(item); } - @Override - public void onActivityResult(int requestCode, int resultCode, Intent intent) - { + private void handleActivityResult(int requestCode, int resultCode, Intent intent) { super.onActivityResult(requestCode, resultCode, intent); BarcodeValues barcodeValues; @@ -198,12 +200,12 @@ public class ScanActivity extends CatimaAppCompatActivity { b.putString("initialCardId", cardId); i.putExtras(b); } - startActivityForResult(i, Utils.SELECT_BARCODE_REQUEST); + manualAddLauncher.launch(i); } public void addFromImage(View view) { Intent photoPickerIntent = new Intent(Intent.ACTION_PICK); photoPickerIntent.setType("image/*"); - startActivityForResult(photoPickerIntent, Utils.BARCODE_IMPORT_FROM_IMAGE_FILE); + photoPickerLauncher.launch(photoPickerIntent); } } diff --git a/app/src/main/java/protect/card_locker/ShortcutHelper.java b/app/src/main/java/protect/card_locker/ShortcutHelper.java index 29bbff461..426983cf1 100644 --- a/app/src/main/java/protect/card_locker/ShortcutHelper.java +++ b/app/src/main/java/protect/card_locker/ShortcutHelper.java @@ -3,8 +3,12 @@ package protect.card_locker; import android.content.Context; import android.content.Intent; import android.graphics.Bitmap; +import android.graphics.Canvas; +import android.graphics.Color; import android.os.Bundle; +import org.jetbrains.annotations.NotNull; + import java.util.Collections; import java.util.Comparator; import java.util.LinkedList; @@ -12,10 +16,10 @@ import java.util.List; import androidx.core.content.pm.ShortcutInfoCompat; import androidx.core.content.pm.ShortcutManagerCompat; +import androidx.core.graphics.ColorUtils; import androidx.core.graphics.drawable.IconCompat; -class ShortcutHelper -{ +class ShortcutHelper { // Android documentation says that no more than 5 shortcuts // are supported. However, that may be too many, as not all // launcher will show all 5. Instead, the number is limited @@ -23,6 +27,14 @@ class ShortcutHelper // chance of being shown. private static final int MAX_SHORTCUTS = 3; + // https://developer.android.com/reference/android/graphics/drawable/AdaptiveIconDrawable.html + private static final int ADAPTIVE_BITMAP_SCALE = 1; + private static final int ADAPTIVE_BITMAP_SIZE = 108 * ADAPTIVE_BITMAP_SCALE; + private static final int ADAPTIVE_BITMAP_VISIBLE_SIZE = 72 * ADAPTIVE_BITMAP_SCALE; + private static final int ADAPTIVE_BITMAP_IMAGE_SIZE = ADAPTIVE_BITMAP_VISIBLE_SIZE + 5 * ADAPTIVE_BITMAP_SCALE; + private static final int PADDING_COLOR = Color.argb(255, 255, 255, 255); + private static final int PADDING_COLOR_OVERLAY = Color.argb(127, 0, 0, 0); + /** * Add a card to the app shortcuts, and maintain a list of the most * recently used cards. If there is already a shortcut for the card, @@ -30,8 +42,7 @@ class ShortcutHelper * card exceeds the max number of shortcuts, then the least recently * used card shortcut is discarded. */ - static void updateShortcuts(Context context, LoyaltyCard card) - { + static void updateShortcuts(Context context, LoyaltyCard card) { LinkedList list = new LinkedList<>(ShortcutManagerCompat.getDynamicShortcuts(context)); DBHelper dbHelper = new DBHelper(context); @@ -44,31 +55,25 @@ class ShortcutHelper Integer foundIndex = null; - for(int index = 0; index < list.size(); index++) - { - if(list.get(index).getId().equals(shortcutId)) - { + for (int index = 0; index < list.size(); index++) { + if (list.get(index).getId().equals(shortcutId)) { // Found the item already foundIndex = index; break; } } - if(foundIndex != null) - { + if (foundIndex != null) { // If the item is already found, then the list needs to be // reordered, so that the selected item now has the lowest // rank, thus letting it survive longer. ShortcutInfoCompat found = list.remove(foundIndex.intValue()); list.addFirst(found); - } - else - { + } else { // The item is new to the list. First, we need to trim the list // until it is able to accept a new item, then the item is // inserted. - while(list.size() >= MAX_SHORTCUTS) - { + while (list.size() >= MAX_SHORTCUTS) { list.pollLast(); } @@ -80,15 +85,14 @@ class ShortcutHelper LinkedList finalList = new LinkedList<>(); // The ranks are now updated; the order in the list is the rank. - for(int index = 0; index < list.size(); index++) - { + for (int index = 0; index < list.size(); index++) { ShortcutInfoCompat prevShortcut = list.get(index); LoyaltyCard loyaltyCard = dbHelper.getLoyaltyCard(Integer.parseInt(prevShortcut.getId())); ShortcutInfoCompat updatedShortcut = createShortcutBuilder(context, loyaltyCard) - .setRank(index) - .build(); + .setRank(index) + .build(); finalList.addLast(updatedShortcut); } @@ -100,16 +104,13 @@ class ShortcutHelper * Remove the given card id from the app shortcuts, if such a * shortcut exists. */ - static void removeShortcut(Context context, int cardId) - { + static void removeShortcut(Context context, int cardId) { List list = ShortcutManagerCompat.getDynamicShortcuts(context); String shortcutId = Integer.toString(cardId); - for(int index = 0; index < list.size(); index++) - { - if(list.get(index).getId().equals(shortcutId)) - { + for (int index = 0; index < list.size(); index++) { + if (list.get(index).getId().equals(shortcutId)) { list.remove(index); break; } @@ -118,6 +119,16 @@ class ShortcutHelper ShortcutManagerCompat.setDynamicShortcuts(context, list); } + static @NotNull + Bitmap createAdaptiveBitmap(@NotNull Bitmap in, int paddingColor) { + Bitmap ret = Bitmap.createBitmap(ADAPTIVE_BITMAP_SIZE, ADAPTIVE_BITMAP_SIZE, Bitmap.Config.ARGB_8888); + Canvas output = new Canvas(ret); + output.drawColor(ColorUtils.compositeColors(PADDING_COLOR_OVERLAY, paddingColor)); + Bitmap resized = Utils.resizeBitmap(in, ADAPTIVE_BITMAP_IMAGE_SIZE); + output.drawBitmap(resized, (ADAPTIVE_BITMAP_SIZE - resized.getWidth()) / 2f, (ADAPTIVE_BITMAP_SIZE - resized.getHeight()) / 2f, null); + return ret; + } + static ShortcutInfoCompat.Builder createShortcutBuilder(Context context, LoyaltyCard loyaltyCard) { Intent intent = new Intent(context, LoyaltyCardViewActivity.class); intent.setAction(Intent.ACTION_MAIN); @@ -129,7 +140,12 @@ class ShortcutHelper bundle.putBoolean("view", true); intent.putExtras(bundle); - Bitmap iconBitmap = Utils.generateIcon(context, loyaltyCard, true).getLetterTile(); + Bitmap iconBitmap = Utils.retrieveCardImage(context, loyaltyCard.id, ImageLocationType.icon); + if (iconBitmap == null) { + iconBitmap = Utils.generateIcon(context, loyaltyCard, true).getLetterTile(); + } else { + iconBitmap = createAdaptiveBitmap(iconBitmap, loyaltyCard.headerColor == null ? PADDING_COLOR : loyaltyCard.headerColor); + } IconCompat icon = IconCompat.createWithAdaptiveBitmap(iconBitmap); diff --git a/app/src/main/java/protect/card_locker/SimpleTextWatcher.java b/app/src/main/java/protect/card_locker/SimpleTextWatcher.java index d6483b4a8..05c9762e3 100644 --- a/app/src/main/java/protect/card_locker/SimpleTextWatcher.java +++ b/app/src/main/java/protect/card_locker/SimpleTextWatcher.java @@ -5,11 +5,14 @@ import android.text.TextWatcher; public class SimpleTextWatcher implements TextWatcher { @Override - public void beforeTextChanged(CharSequence s, int start, int count, int after) { } + public void beforeTextChanged(CharSequence s, int start, int count, int after) { + } @Override - public void onTextChanged(CharSequence s, int start, int before, int count) { } + public void onTextChanged(CharSequence s, int start, int before, int count) { + } @Override - public void afterTextChanged(Editable s) { } + public void afterTextChanged(Editable s) { + } } diff --git a/app/src/main/java/protect/card_locker/Utils.java b/app/src/main/java/protect/card_locker/Utils.java index ff1bad0d7..34e697bd4 100644 --- a/app/src/main/java/protect/card_locker/Utils.java +++ b/app/src/main/java/protect/card_locker/Utils.java @@ -25,6 +25,7 @@ import com.google.zxing.Result; import com.google.zxing.common.HybridBinarizer; import java.io.ByteArrayOutputStream; +import java.io.File; import java.io.FileInputStream; import java.io.FileNotFoundException; import java.io.FileOutputStream; @@ -38,6 +39,7 @@ import java.util.GregorianCalendar; import java.util.Locale; import java.util.Map; +import androidx.appcompat.app.AppCompatDelegate; import androidx.core.graphics.ColorUtils; import androidx.exifinterface.media.ExifInterface; import protect.card_locker.preferences.Settings; @@ -52,12 +54,15 @@ public class Utils { public static final int BARCODE_IMPORT_FROM_IMAGE_FILE = 4; public static final int CARD_IMAGE_FROM_CAMERA_FRONT = 5; public static final int CARD_IMAGE_FROM_CAMERA_BACK = 6; - public static final int CARD_IMAGE_FROM_FILE_FRONT = 7; - public static final int CARD_IMAGE_FROM_FILE_BACK = 8; + public static final int CARD_IMAGE_FROM_CAMERA_ICON = 7; + public static final int CARD_IMAGE_FROM_FILE_FRONT = 8; + public static final int CARD_IMAGE_FROM_FILE_BACK = 9; + public static final int CARD_IMAGE_FROM_FILE_ICON = 10; static final double LUMINANCE_MIDPOINT = 0.5; - static final int BITMAP_SIZE_BIG = 512; + static final int BITMAP_SIZE_SMALL = 512; + static final int BITMAP_SIZE_BIG = 2048; static public LetterBitmap generateIcon(Context context, LoyaltyCard loyaltyCard, boolean forShortcut) { return generateIcon(context, loyaltyCard.store, loyaltyCard.headerColor, forShortcut); @@ -264,13 +269,11 @@ public class Utils { return bos.toByteArray(); } - static public Bitmap resizeBitmap(Bitmap bitmap) { + static public Bitmap resizeBitmap(Bitmap bitmap, double maxSize) { if (bitmap == null) { return null; } - double maxSize = BITMAP_SIZE_BIG; - double width = bitmap.getWidth(); double height = bitmap.getHeight(); @@ -313,16 +316,20 @@ public class Utils { return Bitmap.createBitmap(bitmap, 0, 0, bitmap.getWidth(), bitmap.getHeight(), matrix, true); } - static public String getCardImageFileName(int loyaltyCardId, boolean front) { + static public String getCardImageFileName(int loyaltyCardId, ImageLocationType type) { StringBuilder cardImageFileNameBuilder = new StringBuilder(); cardImageFileNameBuilder.append("card_"); cardImageFileNameBuilder.append(loyaltyCardId); cardImageFileNameBuilder.append("_"); - if (front) { + if (type == ImageLocationType.front) { cardImageFileNameBuilder.append("front"); - } else { + } else if (type == ImageLocationType.back) { cardImageFileNameBuilder.append("back"); + } else if (type == ImageLocationType.icon) { + cardImageFileNameBuilder.append("icon"); + } else { + throw new IllegalArgumentException("Unknown image type"); } cardImageFileNameBuilder.append(".png"); @@ -340,8 +347,8 @@ public class Utils { bitmap.compress(Bitmap.CompressFormat.PNG, 100, out); } - static public void saveCardImage(Context context, Bitmap bitmap, int loyaltyCardId, boolean front) throws FileNotFoundException { - saveCardImage(context, bitmap, getCardImageFileName(loyaltyCardId, front)); + static public void saveCardImage(Context context, Bitmap bitmap, int loyaltyCardId, ImageLocationType type) throws FileNotFoundException { + saveCardImage(context, bitmap, getCardImageFileName(loyaltyCardId, type)); } static public Bitmap retrieveCardImage(Context context, String fileName) { @@ -355,11 +362,11 @@ public class Utils { return BitmapFactory.decodeStream(in); } - static public Bitmap retrieveCardImage(Context context, int loyaltyCardId, boolean front) { - return retrieveCardImage(context, getCardImageFileName(loyaltyCardId, front)); + static public Bitmap retrieveCardImage(Context context, int loyaltyCardId, ImageLocationType type) { + return retrieveCardImage(context, getCardImageFileName(loyaltyCardId, type)); } - static public U mapGetOrDefault(Map map, T key, U defaultValue) { + static public U mapGetOrDefault(Map map, T key, U defaultValue) { U value = map.get(key); if (value == null) { return defaultValue; @@ -401,4 +408,44 @@ public class Utils { static public long getUnixTime() { return System.currentTimeMillis() / 1000; } + + static public boolean isDarkModeEnabled(Context inputContext) { + int nightModeSetting = new Settings(inputContext).getTheme(); + if (nightModeSetting == AppCompatDelegate.MODE_NIGHT_FOLLOW_SYSTEM) { + Configuration config = inputContext.getResources().getConfiguration(); + int currentNightMode = config.uiMode & Configuration.UI_MODE_NIGHT_MASK; + return (currentNightMode == Configuration.UI_MODE_NIGHT_YES); + } else { + return nightModeSetting == AppCompatDelegate.MODE_NIGHT_YES; + } + } + + public static File createTempFile(Context context, String name) { + return new File(context.getCacheDir() + "/" + name); + } + + public static String saveTempImage(Context context, Bitmap in, String name, Bitmap.CompressFormat format) { + File image = createTempFile(context, name); + try (FileOutputStream out = new FileOutputStream(image)) { + in.compress(format, 100, out); + return image.getAbsolutePath(); + } catch (IOException e) { + Log.d("store temp image", "failed writing temp file for temporary image, name: " + name); + return null; + } + } + + public static Bitmap loadImage(String path) { + try { + return BitmapFactory.decodeStream(new FileInputStream(path)); + } catch (IOException e) { + Log.d("load image", "failed loading image from " + path); + return null; + } + } + + public static Bitmap loadTempImage(Context context, String name) { + return loadImage(context.getCacheDir() + "/" + name); + } + } diff --git a/app/src/main/java/protect/card_locker/async/CompatCallable.java b/app/src/main/java/protect/card_locker/async/CompatCallable.java index e58d15c2c..352e0aeac 100644 --- a/app/src/main/java/protect/card_locker/async/CompatCallable.java +++ b/app/src/main/java/protect/card_locker/async/CompatCallable.java @@ -4,5 +4,6 @@ import java.util.concurrent.Callable; public interface CompatCallable extends Callable { void onPostExecute(Object result); + void onPreExecute(); } diff --git a/app/src/main/java/protect/card_locker/async/TaskHandler.java b/app/src/main/java/protect/card_locker/async/TaskHandler.java index c25b13e33..c417704be 100644 --- a/app/src/main/java/protect/card_locker/async/TaskHandler.java +++ b/app/src/main/java/protect/card_locker/async/TaskHandler.java @@ -13,11 +13,11 @@ import java.util.concurrent.TimeUnit; /** * AsyncTask has been deprecated so this provides very rudimentary compatibility without * needing to redo too many Parts. - * + *

* However this is a much, much more cooperative Behaviour than before so * the callers need to ensure we do NOT rely on forced cancellation and feed less into the * ThreadPools so we don't OOM/Overload the Users device - * + *

* This assumes single-threaded callers. */ public class TaskHandler { @@ -44,9 +44,10 @@ public class TaskHandler { /** * Replaces (or initializes) an Executor with a clean (new) one + * * @param executors Map Reference - * @param type Which Queue - * @param flushOld attempt shutdown + * @param type Which Queue + * @param flushOld attempt shutdown * @param waitOnOld wait for Termination */ private void replaceExecutor(HashMap executors, TYPE type, Boolean flushOld, Boolean waitOnOld) { diff --git a/app/src/main/java/protect/card_locker/importexport/CSVHelpers.java b/app/src/main/java/protect/card_locker/importexport/CSVHelpers.java index 74dd8f94e..d21b0f11a 100644 --- a/app/src/main/java/protect/card_locker/importexport/CSVHelpers.java +++ b/app/src/main/java/protect/card_locker/importexport/CSVHelpers.java @@ -12,18 +12,13 @@ public class CSVHelpers { * if it is not null. Otherwise, a FormatException is thrown. */ static String extractString(String key, CSVRecord record, String defaultValue) - throws FormatException - { + throws FormatException { String toReturn = defaultValue; - if(record.isMapped(key)) - { + if (record.isMapped(key)) { toReturn = record.get(key); - } - else - { - if(defaultValue == null) - { + } else { + if (defaultValue == null) { throw new FormatException("Field not used but expected: " + key); } } @@ -38,25 +33,19 @@ public class CSVHelpers { * int, a FormatException is thrown. */ static Integer extractInt(String key, CSVRecord record, boolean nullIsOk) - throws FormatException - { - if(record.isMapped(key) == false) - { + throws FormatException { + if (record.isMapped(key) == false) { throw new FormatException("Field not used but expected: " + key); } String value = record.get(key); - if(value.isEmpty() && nullIsOk) - { + if (value.isEmpty() && nullIsOk) { return null; } - try - { + try { return Integer.parseInt(record.get(key)); - } - catch(NumberFormatException e) - { + } catch (NumberFormatException e) { throw new FormatException("Failed to parse field: " + key, e); } } @@ -68,25 +57,19 @@ public class CSVHelpers { * int, a FormatException is thrown. */ static Long extractLong(String key, CSVRecord record, boolean nullIsOk) - throws FormatException - { - if(record.isMapped(key) == false) - { + throws FormatException { + if (record.isMapped(key) == false) { throw new FormatException("Field not used but expected: " + key); } String value = record.get(key); - if(value.isEmpty() && nullIsOk) - { + if (value.isEmpty() && nullIsOk) { return null; } - try - { + try { return Long.parseLong(record.get(key)); - } - catch(NumberFormatException e) - { + } catch (NumberFormatException e) { throw new FormatException("Failed to parse field: " + key, e); } } diff --git a/app/src/main/java/protect/card_locker/importexport/CatimaExporter.java b/app/src/main/java/protect/card_locker/importexport/CatimaExporter.java index 119443bba..b55337ad1 100644 --- a/app/src/main/java/protect/card_locker/importexport/CatimaExporter.java +++ b/app/src/main/java/protect/card_locker/importexport/CatimaExporter.java @@ -22,6 +22,7 @@ import java.nio.charset.StandardCharsets; import protect.card_locker.DBHelper; import protect.card_locker.Group; +import protect.card_locker.ImageLocationType; import protect.card_locker.LoyaltyCard; import protect.card_locker.Utils; @@ -29,10 +30,8 @@ import protect.card_locker.Utils; * Class for exporting the database into CSV (Comma Separate Values) * format. */ -public class CatimaExporter implements Exporter -{ - public void exportData(Context context, DBHelper db, OutputStream output,char[] password) throws IOException, InterruptedException - { +public class CatimaExporter implements Exporter { + public void exportData(Context context, DBHelper db, OutputStream output, char[] password) throws IOException, InterruptedException { // Necessary vars int readLen; byte[] readBuffer = new byte[InternalZipConstants.BUFF_SIZE]; @@ -40,10 +39,9 @@ public class CatimaExporter implements Exporter // Create zip output stream ZipOutputStream zipOutputStream; - if(password!=null && password.length>0){ - zipOutputStream = new ZipOutputStream(output,password); - } - else{ + if (password != null && password.length > 0) { + zipOutputStream = new ZipOutputStream(output, password); + } else { zipOutputStream = new ZipOutputStream(output); } @@ -53,7 +51,7 @@ public class CatimaExporter implements Exporter writeCSV(db, catimaOutputStreamWriter); // Add CSV to zip file - ZipParameters csvZipParameters = createZipParameters("catima.csv",password); + ZipParameters csvZipParameters = createZipParameters("catima.csv", password); zipOutputStream.putNextEntry(csvZipParameters); InputStream csvInputStream = new ByteArrayInputStream(catimaOutputStream.toByteArray()); while ((readLen = csvInputStream.read(readBuffer)) != -1) { @@ -63,22 +61,16 @@ public class CatimaExporter implements Exporter // Loop over all cards again Cursor cardCursor = db.getLoyaltyCardCursor(); - while(cardCursor.moveToNext()) - { + while (cardCursor.moveToNext()) { // For each card LoyaltyCard card = LoyaltyCard.toLoyaltyCard(cardCursor); - // Prepare looping over both front and back image - boolean[] frontValues = new boolean[2]; - frontValues[0] = true; - frontValues[1] = false; - // For each image - for (boolean front : frontValues) { + for (ImageLocationType imageLocationType : ImageLocationType.values()) { // If it exists, add to the .zip file - Bitmap image = Utils.retrieveCardImage(context, card.id, front); + Bitmap image = Utils.retrieveCardImage(context, card.id, imageLocationType); if (image != null) { - ZipParameters imageZipParameters = createZipParameters(Utils.getCardImageFileName(card.id, front),password); + ZipParameters imageZipParameters = createZipParameters(Utils.getCardImageFileName(card.id, imageLocationType), password); zipOutputStream.putNextEntry(imageZipParameters); InputStream imageInputStream = new ByteArrayInputStream(Utils.bitmapToByteArray(image)); while ((readLen = imageInputStream.read(readBuffer)) != -1) { @@ -92,10 +84,10 @@ public class CatimaExporter implements Exporter zipOutputStream.close(); } - private ZipParameters createZipParameters(String fileName, char[] password){ + private ZipParameters createZipParameters(String fileName, char[] password) { ZipParameters zipParameters = new ZipParameters(); zipParameters.setFileNameInZip(fileName); - if(password!=null && password.length>0){ + if (password != null && password.length > 0) { zipParameters.setEncryptFiles(true); zipParameters.setEncryptionMethod(EncryptionMethod.AES); } @@ -115,14 +107,12 @@ public class CatimaExporter implements Exporter Cursor groupCursor = db.getGroupCursor(); - while(groupCursor.moveToNext()) - { + while (groupCursor.moveToNext()) { Group group = Group.toGroup(groupCursor); printer.printRecord(group._id); - if(Thread.currentThread().isInterrupted()) - { + if (Thread.currentThread().isInterrupted()) { throw new InterruptedException(); } } @@ -148,8 +138,7 @@ public class CatimaExporter implements Exporter Cursor cardCursor = db.getLoyaltyCardCursor(); - while(cardCursor.moveToNext()) - { + while (cardCursor.moveToNext()) { LoyaltyCard card = LoyaltyCard.toLoyaltyCard(cardCursor); printer.printRecord(card.id, @@ -165,8 +154,7 @@ public class CatimaExporter implements Exporter card.starStatus, card.lastUsed); - if(Thread.currentThread().isInterrupted()) - { + if (Thread.currentThread().isInterrupted()) { throw new InterruptedException(); } } @@ -182,16 +170,14 @@ public class CatimaExporter implements Exporter Cursor cardCursor2 = db.getLoyaltyCardCursor(); - while(cardCursor2.moveToNext()) - { + while (cardCursor2.moveToNext()) { LoyaltyCard card = LoyaltyCard.toLoyaltyCard(cardCursor2); for (Group group : db.getLoyaltyCardGroups(card.id)) { printer.printRecord(card.id, group._id); } - if(Thread.currentThread().isInterrupted()) - { + if (Thread.currentThread().isInterrupted()) { throw new InterruptedException(); } } diff --git a/app/src/main/java/protect/card_locker/importexport/CatimaImporter.java b/app/src/main/java/protect/card_locker/importexport/CatimaImporter.java index 2ea858e1f..6b4fc05a5 100644 --- a/app/src/main/java/protect/card_locker/importexport/CatimaImporter.java +++ b/app/src/main/java/protect/card_locker/importexport/CatimaImporter.java @@ -4,8 +4,6 @@ import android.content.Context; import android.database.sqlite.SQLiteDatabase; import android.net.Uri; -import com.google.zxing.BarcodeFormat; - import net.lingala.zip4j.io.inputstream.ZipInputStream; import net.lingala.zip4j.model.LocalFileHeader; @@ -37,18 +35,17 @@ import protect.card_locker.ZipUtils; /** * Class for importing a database from CSV (Comma Separate Values) * formatted data. - * + *

* The database's loyalty cards are expected to appear in the CSV data. * A header is expected for the each table showing the names of the columns. */ -public class CatimaImporter implements Importer -{ +public class CatimaImporter implements Importer { public void importData(Context context, DBHelper db, InputStream input, char[] password) throws IOException, FormatException, InterruptedException { InputStream bufferedInputStream = new BufferedInputStream(input); bufferedInputStream.mark(100); // First, check if this is a zip file - ZipInputStream zipInputStream = new ZipInputStream(bufferedInputStream,password); + ZipInputStream zipInputStream = new ZipInputStream(bufferedInputStream, password); boolean isZipFile = false; @@ -102,41 +99,32 @@ public class CatimaImporter implements Importer bufferedReader.close(); } - public void parseV1(Context context, DBHelper db, BufferedReader input) throws IOException, FormatException, InterruptedException - { + public void parseV1(Context context, DBHelper db, BufferedReader input) throws IOException, FormatException, InterruptedException { final CSVParser parser = new CSVParser(input, CSVFormat.RFC4180.builder().setHeader().build()); SQLiteDatabase database = db.getWritableDatabase(); database.beginTransaction(); - try - { - for (CSVRecord record : parser) - { + try { + for (CSVRecord record : parser) { importLoyaltyCard(context, database, db, record); - if(Thread.currentThread().isInterrupted()) - { + if (Thread.currentThread().isInterrupted()) { throw new InterruptedException(); } } parser.close(); database.setTransactionSuccessful(); - } - catch(IllegalArgumentException|IllegalStateException e) - { + } catch (IllegalArgumentException | IllegalStateException e) { throw new FormatException("Issue parsing CSV data", e); - } - finally - { + } finally { database.endTransaction(); database.close(); } } - public void parseV2(Context context, DBHelper db, BufferedReader input) throws IOException, FormatException, InterruptedException - { + public void parseV2(Context context, DBHelper db, BufferedReader input) throws IOException, FormatException, InterruptedException { SQLiteDatabase database = db.getWritableDatabase(); database.beginTransaction(); @@ -206,8 +194,7 @@ public class CatimaImporter implements Importer } } - public void parseV2Groups(DBHelper db, SQLiteDatabase database, String data) throws IOException, FormatException, InterruptedException - { + public void parseV2Groups(DBHelper db, SQLiteDatabase database, String data) throws IOException, FormatException, InterruptedException { // Parse groups final CSVParser groupParser = new CSVParser(new StringReader(data), CSVFormat.RFC4180.builder().setHeader().build()); @@ -232,8 +219,7 @@ public class CatimaImporter implements Importer } } - public void parseV2Cards(Context context, DBHelper db, SQLiteDatabase database, String data) throws IOException, FormatException, InterruptedException - { + public void parseV2Cards(Context context, DBHelper db, SQLiteDatabase database, String data) throws IOException, FormatException, InterruptedException { // Parse cards final CSVParser cardParser = new CSVParser(new StringReader(data), CSVFormat.RFC4180.builder().setHeader().build()); @@ -258,8 +244,7 @@ public class CatimaImporter implements Importer } } - public void parseV2CardGroups(DBHelper db, SQLiteDatabase database, String data) throws IOException, FormatException, InterruptedException - { + public void parseV2CardGroups(DBHelper db, SQLiteDatabase database, String data) throws IOException, FormatException, InterruptedException { // Parse card group mappings final CSVParser cardGroupParser = new CSVParser(new StringReader(data), CSVFormat.RFC4180.builder().setHeader().build()); @@ -289,13 +274,11 @@ public class CatimaImporter implements Importer * session. */ private void importLoyaltyCard(Context context, SQLiteDatabase database, DBHelper helper, CSVRecord record) - throws IOException, FormatException - { + throws IOException, FormatException { int id = CSVHelpers.extractInt(DBHelper.LoyaltyCardDbIds.ID, record, false); String store = CSVHelpers.extractString(DBHelper.LoyaltyCardDbIds.STORE, record, ""); - if(store.isEmpty()) - { + if (store.isEmpty()) { throw new FormatException("No store listed, but is required"); } @@ -303,12 +286,13 @@ public class CatimaImporter implements Importer Date expiry = null; try { expiry = new Date(CSVHelpers.extractLong(DBHelper.LoyaltyCardDbIds.EXPIRY, record, true)); - } catch (NullPointerException | FormatException e) { } + } catch (NullPointerException | FormatException e) { + } BigDecimal balance; try { balance = new BigDecimal(CSVHelpers.extractString(DBHelper.LoyaltyCardDbIds.BALANCE, record, null)); - } catch (FormatException _e ) { + } catch (FormatException _e) { // These fields did not exist in versions 1.8.1 and before // We catch this exception so we can still import old backups balance = new BigDecimal("0"); @@ -316,33 +300,29 @@ public class CatimaImporter implements Importer Currency balanceType = null; String unparsedBalanceType = CSVHelpers.extractString(DBHelper.LoyaltyCardDbIds.BALANCE_TYPE, record, ""); - if(!unparsedBalanceType.isEmpty()) { + if (!unparsedBalanceType.isEmpty()) { balanceType = Currency.getInstance(unparsedBalanceType); } String cardId = CSVHelpers.extractString(DBHelper.LoyaltyCardDbIds.CARD_ID, record, ""); - if(cardId.isEmpty()) - { + if (cardId.isEmpty()) { throw new FormatException("No card ID listed, but is required"); } String barcodeId = CSVHelpers.extractString(DBHelper.LoyaltyCardDbIds.BARCODE_ID, record, ""); - if(barcodeId.isEmpty()) - { + if (barcodeId.isEmpty()) { barcodeId = null; } CatimaBarcode barcodeType = null; String unparsedBarcodeType = CSVHelpers.extractString(DBHelper.LoyaltyCardDbIds.BARCODE_TYPE, record, ""); - if(!unparsedBarcodeType.isEmpty()) - { + if (!unparsedBarcodeType.isEmpty()) { barcodeType = CatimaBarcode.fromName(unparsedBarcodeType); } Integer headerColor = null; - if(record.isMapped(DBHelper.LoyaltyCardDbIds.HEADER_COLOR)) - { + if (record.isMapped(DBHelper.LoyaltyCardDbIds.HEADER_COLOR)) { headerColor = CSVHelpers.extractInt(DBHelper.LoyaltyCardDbIds.HEADER_COLOR, record, true); } @@ -371,8 +351,7 @@ public class CatimaImporter implements Importer * session. */ private void importGroup(SQLiteDatabase database, DBHelper helper, CSVRecord record) - throws IOException, FormatException - { + throws IOException, FormatException { String id = CSVHelpers.extractString(DBHelper.LoyaltyCardDbGroups.ID, record, null); helper.insertGroup(database, id); @@ -383,8 +362,7 @@ public class CatimaImporter implements Importer * session. */ private void importCardGroupMapping(SQLiteDatabase database, DBHelper helper, CSVRecord record) - throws IOException, FormatException - { + throws IOException, FormatException { Integer cardId = CSVHelpers.extractInt(DBHelper.LoyaltyCardDbIdsGroups.cardID, record, false); String groupId = CSVHelpers.extractString(DBHelper.LoyaltyCardDbIdsGroups.groupID, record, null); diff --git a/app/src/main/java/protect/card_locker/importexport/DataFormat.java b/app/src/main/java/protect/card_locker/importexport/DataFormat.java index 26e97e745..fc4fdc531 100644 --- a/app/src/main/java/protect/card_locker/importexport/DataFormat.java +++ b/app/src/main/java/protect/card_locker/importexport/DataFormat.java @@ -1,10 +1,8 @@ package protect.card_locker.importexport; -public enum DataFormat -{ +public enum DataFormat { Catima, Fidme, Stocard, - VoucherVault - ; + VoucherVault; } diff --git a/app/src/main/java/protect/card_locker/importexport/Exporter.java b/app/src/main/java/protect/card_locker/importexport/Exporter.java index ad4928030..584572360 100644 --- a/app/src/main/java/protect/card_locker/importexport/Exporter.java +++ b/app/src/main/java/protect/card_locker/importexport/Exporter.java @@ -11,11 +11,11 @@ import protect.card_locker.DBHelper; * Interface for a class which can export the contents of the database * in a given format. */ -public interface Exporter -{ +public interface Exporter { /** * Export the database to the output stream in a given format. + * * @throws IOException */ - void exportData(Context context, DBHelper db, OutputStream output,char[] password) throws IOException, InterruptedException; + void exportData(Context context, DBHelper db, OutputStream output, char[] password) throws IOException, InterruptedException; } diff --git a/app/src/main/java/protect/card_locker/importexport/FidmeImporter.java b/app/src/main/java/protect/card_locker/importexport/FidmeImporter.java index f5453db67..23c927b8b 100644 --- a/app/src/main/java/protect/card_locker/importexport/FidmeImporter.java +++ b/app/src/main/java/protect/card_locker/importexport/FidmeImporter.java @@ -3,8 +3,6 @@ package protect.card_locker.importexport; import android.content.Context; import android.database.sqlite.SQLiteDatabase; -import com.google.zxing.BarcodeFormat; - import net.lingala.zip4j.io.inputstream.ZipInputStream; import net.lingala.zip4j.model.LocalFileHeader; @@ -23,12 +21,11 @@ import java.text.ParseException; import protect.card_locker.CatimaBarcode; import protect.card_locker.DBHelper; import protect.card_locker.FormatException; -import protect.card_locker.Utils; /** * Class for importing a database from CSV (Comma Separate Values) * formatted data. - * + *

* The database's loyalty cards are expected to appear in the CSV data. * A header is expected for the each table showing the names of the columns. */ diff --git a/app/src/main/java/protect/card_locker/importexport/ImportExportResult.java b/app/src/main/java/protect/card_locker/importexport/ImportExportResult.java index 9a8d71c08..8419d3ebe 100644 --- a/app/src/main/java/protect/card_locker/importexport/ImportExportResult.java +++ b/app/src/main/java/protect/card_locker/importexport/ImportExportResult.java @@ -1,9 +1,7 @@ package protect.card_locker.importexport; -public enum ImportExportResult -{ +public enum ImportExportResult { Success, GenericFailure, - BadPassword - ; + BadPassword; } diff --git a/app/src/main/java/protect/card_locker/importexport/Importer.java b/app/src/main/java/protect/card_locker/importexport/Importer.java index fe47d05fe..a394e455b 100644 --- a/app/src/main/java/protect/card_locker/importexport/Importer.java +++ b/app/src/main/java/protect/card_locker/importexport/Importer.java @@ -15,11 +15,11 @@ import protect.card_locker.FormatException; * Interface for a class which can import the contents of a stream * into the database. */ -public interface Importer -{ +public interface Importer { /** * Import data from the input stream in a given format into * the database. + * * @throws IOException * @throws FormatException */ diff --git a/app/src/main/java/protect/card_locker/importexport/MultiFormatExporter.java b/app/src/main/java/protect/card_locker/importexport/MultiFormatExporter.java index b45cf1eda..c4a3e2dd2 100644 --- a/app/src/main/java/protect/card_locker/importexport/MultiFormatExporter.java +++ b/app/src/main/java/protect/card_locker/importexport/MultiFormatExporter.java @@ -8,26 +8,23 @@ import java.io.OutputStream; import protect.card_locker.DBHelper; -public class MultiFormatExporter -{ +public class MultiFormatExporter { private static final String TAG = "Catima"; /** * Attempts to export data to the output stream in the * given format, if possible. - * + *

* The output stream is closed on success. * * @return ImportExportResult.Success if the database was successfully exported, * another ImportExportResult otherwise. If not Success, partial data may have been * written to the output stream, and it should be discarded. */ - public static ImportExportResult exportData(Context context, DBHelper db, OutputStream output, DataFormat format,char[] password) - { + public static ImportExportResult exportData(Context context, DBHelper db, OutputStream output, DataFormat format, char[] password) { Exporter exporter = null; - switch(format) - { + switch (format) { case Catima: exporter = new CatimaExporter(); break; @@ -36,26 +33,18 @@ public class MultiFormatExporter break; } - if(exporter != null) - { - try - { - exporter.exportData(context, db, output,password); + if (exporter != null) { + try { + exporter.exportData(context, db, output, password); return ImportExportResult.Success; - } - catch(IOException e) - { + } catch (IOException e) { Log.e(TAG, "Failed to export data", e); - } - catch(InterruptedException e) - { + } catch (InterruptedException e) { Log.e(TAG, "Failed to export data", e); } return ImportExportResult.GenericFailure; - } - else - { + } else { Log.e(TAG, "Unsupported data format exported: " + format.name()); return ImportExportResult.GenericFailure; } diff --git a/app/src/main/java/protect/card_locker/importexport/MultiFormatImporter.java b/app/src/main/java/protect/card_locker/importexport/MultiFormatImporter.java index 3463f1073..21a520f41 100644 --- a/app/src/main/java/protect/card_locker/importexport/MultiFormatImporter.java +++ b/app/src/main/java/protect/card_locker/importexport/MultiFormatImporter.java @@ -14,14 +14,13 @@ import java.text.ParseException; import protect.card_locker.DBHelper; import protect.card_locker.FormatException; -public class MultiFormatImporter -{ +public class MultiFormatImporter { private static final String TAG = "Catima"; /** * Attempts to import data from the input stream of the * given format into the database. - * + *

* The input stream is not closed, and doing so is the * responsibility of the caller. * @@ -29,12 +28,10 @@ public class MultiFormatImporter * or another result otherwise. If no Success, no data was written to * the database. */ - public static ImportExportResult importData(Context context, DBHelper db, InputStream input, DataFormat format, char[] password) - { + public static ImportExportResult importData(Context context, DBHelper db, InputStream input, DataFormat format, char[] password) { Importer importer = null; - switch(format) - { + switch (format) { case Catima: importer = new CatimaImporter(); break; @@ -49,25 +46,17 @@ public class MultiFormatImporter break; } - if (importer != null) - { - try - { + if (importer != null) { + try { importer.importData(context, db, input, password); return ImportExportResult.Success; - } - catch(ZipException e) - { + } catch (ZipException e) { return ImportExportResult.BadPassword; - } - catch(IOException | FormatException | InterruptedException | JSONException | ParseException | NullPointerException e) - { + } catch (IOException | FormatException | InterruptedException | JSONException | ParseException | NullPointerException e) { Log.e(TAG, "Failed to import data", e); } - } - else - { + } else { Log.e(TAG, "Unsupported data format imported: " + format.name()); } diff --git a/app/src/main/java/protect/card_locker/importexport/StocardImporter.java b/app/src/main/java/protect/card_locker/importexport/StocardImporter.java index 76761b83a..2f8280285 100644 --- a/app/src/main/java/protect/card_locker/importexport/StocardImporter.java +++ b/app/src/main/java/protect/card_locker/importexport/StocardImporter.java @@ -26,6 +26,7 @@ import java.util.HashMap; import protect.card_locker.CatimaBarcode; import protect.card_locker.DBHelper; import protect.card_locker.FormatException; +import protect.card_locker.ImageLocationType; import protect.card_locker.R; import protect.card_locker.Utils; import protect.card_locker.ZipUtils; @@ -33,22 +34,19 @@ import protect.card_locker.ZipUtils; /** * Class for importing a database from CSV (Comma Separate Values) * formatted data. - * + *

* The database's loyalty cards are expected to appear in the CSV data. * A header is expected for the each table showing the names of the columns. */ -public class StocardImporter implements Importer -{ +public class StocardImporter implements Importer { public void importData(Context context, DBHelper db, InputStream input, char[] password) throws IOException, FormatException, JSONException, ParseException { HashMap> loyaltyCardHashMap = new HashMap<>(); HashMap> providers = new HashMap<>(); final CSVParser parser = new CSVParser(new InputStreamReader(context.getResources().openRawResource(R.raw.stocard_stores), StandardCharsets.UTF_8), CSVFormat.RFC4180.builder().setHeader().build()); - try - { - for (CSVRecord record : parser) - { + try { + for (CSVRecord record : parser) { HashMap recordData = new HashMap<>(); recordData.put("name", record.get("name")); recordData.put("barcodeFormat", record.get("barcodeFormat")); @@ -57,7 +55,7 @@ public class StocardImporter implements Importer } parser.close(); - } catch(IllegalArgumentException|IllegalStateException e) { + } catch (IllegalArgumentException | IllegalStateException e) { throw new FormatException("Issue parsing CSV data", e); } @@ -72,7 +70,7 @@ public class StocardImporter implements Importer String[] nameParts = fileName.split("/"); if (providersFileName == null) { - providersFileName = new String[] { + providersFileName = new String[]{ nameParts[0], "sync", "data", @@ -80,7 +78,7 @@ public class StocardImporter implements Importer nameParts[0], "analytics-properties.json" }; - cardBaseName = new String[] { + cardBaseName = new String[]{ nameParts[0], "sync", "data", @@ -111,9 +109,9 @@ public class StocardImporter implements Importer cardName, "_providerId", jsonObject - .getJSONObject("input_provider_reference") - .getString("identifier") - .substring("/loyalty-card-providers/".length()) + .getJSONObject("input_provider_reference") + .getString("identifier") + .substring("/loyalty-card-providers/".length()) ); if (jsonObject.has("input_barcode_format")) { @@ -131,7 +129,7 @@ public class StocardImporter implements Importer cardName, "note", ZipUtils.readJSON(zipInputStream) - .getString("content") + .getString("content") ); } else if (fileName.endsWith("/images/front.png")) { loyaltyCardHashMap = appendToLoyaltyCardHashMap( @@ -178,10 +176,10 @@ public class StocardImporter implements Importer long loyaltyCardInternalId = db.insertLoyaltyCard(database, store, note, null, BigDecimal.valueOf(0), null, cardId, null, barcodeType, null, 0, null); if (loyaltyCardData.containsKey("frontImage")) { - Utils.saveCardImage(context, (Bitmap) loyaltyCardData.get("frontImage"), (int) loyaltyCardInternalId, true); + Utils.saveCardImage(context, (Bitmap) loyaltyCardData.get("frontImage"), (int) loyaltyCardInternalId, ImageLocationType.front); } if (loyaltyCardData.containsKey("backImage")) { - Utils.saveCardImage(context, (Bitmap) loyaltyCardData.get("backImage"), (int) loyaltyCardInternalId, false); + Utils.saveCardImage(context, (Bitmap) loyaltyCardData.get("backImage"), (int) loyaltyCardInternalId, ImageLocationType.back); } } diff --git a/app/src/main/java/protect/card_locker/importexport/VoucherVaultImporter.java b/app/src/main/java/protect/card_locker/importexport/VoucherVaultImporter.java index ad38b1b62..0aa038e1b 100644 --- a/app/src/main/java/protect/card_locker/importexport/VoucherVaultImporter.java +++ b/app/src/main/java/protect/card_locker/importexport/VoucherVaultImporter.java @@ -31,12 +31,11 @@ import protect.card_locker.Utils; /** * Class for importing a database from CSV (Comma Separate Values) * formatted data. - * + *

* The database's loyalty cards are expected to appear in the CSV data. * A header is expected for the each table showing the names of the columns. */ -public class VoucherVaultImporter implements Importer -{ +public class VoucherVaultImporter implements Importer { public void importData(Context context, DBHelper db, InputStream input, char[] password) throws IOException, FormatException, JSONException, ParseException { BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(input, StandardCharsets.UTF_8)); diff --git a/app/src/main/java/protect/card_locker/preferences/Settings.java b/app/src/main/java/protect/card_locker/preferences/Settings.java index a28ac5f8a..977271ffc 100644 --- a/app/src/main/java/protect/card_locker/preferences/Settings.java +++ b/app/src/main/java/protect/card_locker/preferences/Settings.java @@ -12,44 +12,36 @@ import androidx.preference.PreferenceManager; import protect.card_locker.R; import protect.card_locker.Utils; -public class Settings -{ +public class Settings { private Context context; private SharedPreferences settings; - public Settings(Context context) - { + public Settings(Context context) { this.context = context; this.settings = PreferenceManager.getDefaultSharedPreferences(context); } - private String getResString(@StringRes int resId) - { + private String getResString(@StringRes int resId) { return context.getString(resId); } - private int getResInt(@IntegerRes int resId) - { + private int getResInt(@IntegerRes int resId) { return context.getResources().getInteger(resId); } - private String getString(@StringRes int keyId, String defaultValue) - { + private String getString(@StringRes int keyId, String defaultValue) { return settings.getString(getResString(keyId), defaultValue); } - private int getInt(@StringRes int keyId, @IntegerRes int defaultId) - { + private int getInt(@StringRes int keyId, @IntegerRes int defaultId) { return settings.getInt(getResString(keyId), getResInt(defaultId)); } - private boolean getBoolean(@StringRes int keyId, boolean defaultValue) - { + private boolean getBoolean(@StringRes int keyId, boolean defaultValue) { return settings.getBoolean(getResString(keyId), defaultValue); } - public Locale getLocale() - { + public Locale getLocale() { String value = getString(R.string.settings_key_locale, ""); if (value.length() == 0) { @@ -59,69 +51,55 @@ public class Settings return Utils.stringToLocale(value); } - public int getTheme() - { + public int getTheme() { String value = getString(R.string.settings_key_theme, getResString(R.string.settings_key_system_theme)); - if(value.equals(getResString(R.string.settings_key_light_theme))) - { + if (value.equals(getResString(R.string.settings_key_light_theme))) { return AppCompatDelegate.MODE_NIGHT_NO; - } - else if(value.equals(getResString(R.string.settings_key_dark_theme))) - { + } else if (value.equals(getResString(R.string.settings_key_dark_theme))) { return AppCompatDelegate.MODE_NIGHT_YES; } return AppCompatDelegate.MODE_NIGHT_FOLLOW_SYSTEM; } - public double getFontSizeScale() - { + public double getFontSizeScale() { return getInt(R.string.settings_key_max_font_size_scale, R.integer.settings_max_font_size_scale_pct) / 100.0; } - public int getSmallFont() - { + public int getSmallFont() { return 14; } - public int getMediumFont() - { + public int getMediumFont() { return 28; } - public int getLargeFont() - { + public int getLargeFont() { return 40; } - public int getFontSizeMin(int fontSize) - { + public int getFontSizeMin(int fontSize) { return (int) (Math.round(fontSize / 2.0) - 1); } - public int getFontSizeMax(int fontSize) - { + public int getFontSizeMax(int fontSize) { return (int) Math.round(fontSize * getFontSizeScale()); } - public boolean useMaxBrightnessDisplayingBarcode() - { + public boolean useMaxBrightnessDisplayingBarcode() { return getBoolean(R.string.settings_key_display_barcode_max_brightness, true); } - public boolean getLockBarcodeScreenOrientation() - { + public boolean getLockBarcodeScreenOrientation() { return getBoolean(R.string.settings_key_lock_barcode_orientation, false); } - public boolean getKeepScreenOn() - { + public boolean getKeepScreenOn() { return getBoolean(R.string.settings_key_keep_screen_on, true); } - public boolean getDisableLockscreenWhileViewingCard() - { + public boolean getDisableLockscreenWhileViewingCard() { return getBoolean(R.string.settings_key_disable_lockscreen_while_viewing_card, true); } } diff --git a/app/src/main/java/protect/card_locker/preferences/SettingsActivity.java b/app/src/main/java/protect/card_locker/preferences/SettingsActivity.java index 3e88fb87e..7bd73d668 100644 --- a/app/src/main/java/protect/card_locker/preferences/SettingsActivity.java +++ b/app/src/main/java/protect/card_locker/preferences/SettingsActivity.java @@ -24,19 +24,16 @@ import protect.card_locker.CatimaAppCompatActivity; import protect.card_locker.R; import protect.card_locker.Utils; -public class SettingsActivity extends CatimaAppCompatActivity -{ +public class SettingsActivity extends CatimaAppCompatActivity { @Override - protected void onCreate(Bundle savedInstanceState) - { + protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setTitle(R.string.settings); setContentView(R.layout.settings_activity); Toolbar toolbar = findViewById(R.id.toolbar); setSupportActionBar(toolbar); ActionBar actionBar = getSupportActionBar(); - if(actionBar != null) - { + if (actionBar != null) { actionBar.setDisplayHomeAsUpEnabled(true); } @@ -48,12 +45,10 @@ public class SettingsActivity extends CatimaAppCompatActivity } @Override - public boolean onOptionsItemSelected(MenuItem item) - { + public boolean onOptionsItemSelected(MenuItem item) { int id = item.getItemId(); - if(id == android.R.id.home) - { + if (id == android.R.id.home) { finish(); return true; } @@ -61,8 +56,7 @@ public class SettingsActivity extends CatimaAppCompatActivity return super.onOptionsItemSelected(item); } - public static class SettingsFragment extends PreferenceFragmentCompat - { + public static class SettingsFragment extends PreferenceFragmentCompat { private static final String DIALOG_FRAGMENT_TAG = "SettingsFragment"; @Override diff --git a/app/src/main/res/drawable/ic_baseline_unfold_less_24.xml b/app/src/main/res/drawable/ic_baseline_unfold_less_24.xml new file mode 100644 index 000000000..621f087b7 --- /dev/null +++ b/app/src/main/res/drawable/ic_baseline_unfold_less_24.xml @@ -0,0 +1,5 @@ + + + diff --git a/app/src/main/res/drawable/ic_baseline_unfold_more_24.xml b/app/src/main/res/drawable/ic_baseline_unfold_more_24.xml new file mode 100644 index 000000000..249979c04 --- /dev/null +++ b/app/src/main/res/drawable/ic_baseline_unfold_more_24.xml @@ -0,0 +1,5 @@ + + + diff --git a/app/src/main/res/layout/activity_manage_group.xml b/app/src/main/res/layout/activity_manage_group.xml new file mode 100644 index 000000000..4a1855f48 --- /dev/null +++ b/app/src/main/res/layout/activity_manage_group.xml @@ -0,0 +1,65 @@ + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/layout/barcode_layout.xml b/app/src/main/res/layout/barcode_layout.xml new file mode 100644 index 000000000..0e94e54ad --- /dev/null +++ b/app/src/main/res/layout/barcode_layout.xml @@ -0,0 +1,22 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/barcode_selector_activity.xml b/app/src/main/res/layout/barcode_selector_activity.xml index 83d280398..60dfb6be9 100644 --- a/app/src/main/res/layout/barcode_selector_activity.xml +++ b/app/src/main/res/layout/barcode_selector_activity.xml @@ -1,318 +1,88 @@ - + - - - + android:orientation="vertical"> + + + - - - - - - - - - -