From 4d6c08fc73cc77a774f7e19701653ed453a046c0 Mon Sep 17 00:00:00 2001 From: Aayush Gupta Date: Mon, 9 Oct 2023 00:16:12 +0530 Subject: [PATCH 1/3] LoyaltCardEditActivity: Migrate to materialdatepicker dialog Signed-off-by: Aayush Gupta --- .../card_locker/LoyaltyCardEditActivity.java | 183 +++++++----------- 1 file changed, 69 insertions(+), 114 deletions(-) diff --git a/app/src/main/java/protect/card_locker/LoyaltyCardEditActivity.java b/app/src/main/java/protect/card_locker/LoyaltyCardEditActivity.java index 3a55f8954..2e1d701c8 100644 --- a/app/src/main/java/protect/card_locker/LoyaltyCardEditActivity.java +++ b/app/src/main/java/protect/card_locker/LoyaltyCardEditActivity.java @@ -2,8 +2,6 @@ package protect.card_locker; import android.annotation.SuppressLint; import android.app.Activity; -import android.app.DatePickerDialog; -import android.app.Dialog; import android.content.ActivityNotFoundException; import android.content.Context; import android.content.Intent; @@ -29,7 +27,6 @@ import android.view.WindowManager; import android.widget.ArrayAdapter; import android.widget.AutoCompleteTextView; import android.widget.Button; -import android.widget.DatePicker; import android.widget.EditText; import android.widget.FrameLayout; import android.widget.ImageView; @@ -43,20 +40,22 @@ import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.annotation.StringRes; import androidx.appcompat.app.AlertDialog; -import androidx.appcompat.app.AppCompatActivity; import androidx.appcompat.widget.AppCompatTextView; import androidx.appcompat.widget.Toolbar; import androidx.core.content.ContextCompat; import androidx.core.content.FileProvider; import androidx.exifinterface.media.ExifInterface; -import androidx.fragment.app.DialogFragment; import com.google.android.material.chip.Chip; import com.google.android.material.chip.ChipGroup; import com.google.android.material.color.MaterialColors; +import com.google.android.material.datepicker.CalendarConstraints; +import com.google.android.material.datepicker.DateValidatorPointBackward; +import com.google.android.material.datepicker.DateValidatorPointForward; +import com.google.android.material.datepicker.MaterialDatePicker; +import com.google.android.material.datepicker.MaterialPickerOnPositiveButtonClickListener; import com.google.android.material.dialog.MaterialAlertDialogBuilder; import com.google.android.material.floatingactionbutton.FloatingActionButton; -import com.google.android.material.snackbar.Snackbar; import com.google.android.material.tabs.TabLayout; import com.jaredrummler.android.colorpicker.ColorPickerDialog; import com.jaredrummler.android.colorpicker.ColorPickerDialogListener; @@ -75,14 +74,12 @@ import java.util.Calendar; import java.util.Collections; import java.util.Currency; import java.util.Date; -import java.util.GregorianCalendar; import java.util.HashMap; import java.util.Iterator; import java.util.LinkedHashMap; import java.util.List; import java.util.Locale; import java.util.NoSuchElementException; -import java.util.Objects; import java.util.concurrent.Callable; import protect.card_locker.async.TaskHandler; @@ -388,21 +385,6 @@ public class LoyaltyCardEditActivity extends CatimaAppCompatActivity implements addDateFieldTextChangedListener(expiryField, R.string.never, R.string.chooseExpiryDate, LoyaltyCardField.expiry); - DatePickerFragment.registerDatePickListener(this, (textFieldToEdit, newDate) -> { - switch (textFieldToEdit) { - case validFrom: - formatDateField(this, validFromField, newDate); - updateTempState(LoyaltyCardField.validFrom, newDate); - break; - case expiry: - formatDateField(this, expiryField, newDate); - updateTempState(LoyaltyCardField.expiry, newDate); - break; - default: - throw new AssertionError("Unexpected field: " + textFieldToEdit); - } - }); - balanceField.setOnFocusChangeListener((v, hasFocus) -> { if (!hasFocus && !onResuming && !onRestoring) { if (balanceField.getText().toString().isEmpty()) { @@ -1024,14 +1006,30 @@ public class LoyaltyCardEditActivity extends CatimaAppCompatActivity implements if (!lastValue.toString().equals(getString(chooseDateOptionStringId))) { dateField.setText(lastValue); } - DialogFragment datePickerFragment = DatePickerFragment.newInstance( + MaterialPickerOnPositiveButtonClickListener materialPickerOnPositiveButtonClickListener = selection -> { + Date newDate = new Date(selection); + switch (loyaltyCardField) { + case validFrom: + formatDateField(LoyaltyCardEditActivity.this, validFromField, newDate); + updateTempState(LoyaltyCardField.validFrom, newDate); + break; + case expiry: + formatDateField(LoyaltyCardEditActivity.this, expiryField, newDate); + updateTempState(LoyaltyCardField.expiry, newDate); + break; + default: + throw new AssertionError("Unexpected field: " + loyaltyCardField); + } + }; + showDatePicker( loyaltyCardField, (Date) dateField.getTag(), // if the expiry date is being set, set date picker's minDate to the 'valid from' date loyaltyCardField == LoyaltyCardField.expiry ? (Date) validFromField.getTag() : null, // if the 'valid from' date is being set, set date picker's maxDate to the expiry date - loyaltyCardField == LoyaltyCardField.validFrom ? (Date) expiryField.getTag() : null); - datePickerFragment.show(getSupportFragmentManager(), "datePicker"); + loyaltyCardField == LoyaltyCardField.validFrom ? (Date) expiryField.getTag() : null, + materialPickerOnPositiveButtonClickListener + ); } } @@ -1382,103 +1380,60 @@ public class LoyaltyCardEditActivity extends CatimaAppCompatActivity implements // Nothing to do, no change made } - public static class DatePickerFragment extends DialogFragment - implements DatePickerDialog.OnDateSetListener { + private void showDatePicker( + LoyaltyCardField loyaltyCardField, + @Nullable Date selectedDate, + @Nullable Date minDate, + @Nullable Date maxDate, + MaterialPickerOnPositiveButtonClickListener listener + ) { + // Create a new instance of MaterialDatePicker and return it + long startDate = minDate != null ? minDate.getTime() : getDefaultMinDateOfDatePicker(); + long endDate = maxDate != null ? maxDate.getTime() : getDefaultMaxDateOfDatePicker(); - public interface OnDatePickListener { - void onDatePicked(@NonNull LoyaltyCardField textFieldToEdit, @NonNull Date newDate); + CalendarConstraints.DateValidator dateValidator; + switch (loyaltyCardField) { + case validFrom: + dateValidator = DateValidatorPointBackward.before(endDate); + break; + case expiry: + dateValidator = DateValidatorPointForward.from(startDate); + break; + default: + throw new AssertionError("Unexpected field: " + loyaltyCardField); } - private static final String TEXT_FIELD_TO_EDIT_ARGUMENT_KEY = "text_field_to_edit"; - private static final String CURRENT_DATE_ARGUMENT_KEY = "current_date"; - private static final String MIN_DATE_ARGUMENT_KEY = "min_date"; - private static final String MAX_DATE_ARGUMENT_KEY = "max_date"; - private static final String PICK_DATE_REQUEST_KEY = "pick_date_request"; - private static final String NEWLY_PICKED_DATE_ARGUMENT_KEY = "newly_picked_date"; + CalendarConstraints calendarConstraints = new CalendarConstraints.Builder() + .setValidator(dateValidator) + .setStart(startDate) + .setEnd(endDate) + .build(); - LoyaltyCardField textFieldEdit; - @Nullable - Date minDate; - @Nullable - Date maxDate; - - public static DatePickerFragment newInstance(@NonNull LoyaltyCardField textField, @Nullable Date currentDate, @Nullable Date minDate, @Nullable Date maxDate) { - Bundle args = new Bundle(); - args.putSerializable(TEXT_FIELD_TO_EDIT_ARGUMENT_KEY, textField); - args.putSerializable(CURRENT_DATE_ARGUMENT_KEY, currentDate); - args.putSerializable(MIN_DATE_ARGUMENT_KEY, minDate); - args.putSerializable(MAX_DATE_ARGUMENT_KEY, maxDate); - DatePickerFragment fragment = new DatePickerFragment(); - fragment.setArguments(args); - return fragment; + // Use the selected date as the default date in the picker + final Calendar calendar = Calendar.getInstance(); + if (selectedDate != null) { + calendar.setTime(selectedDate); } - public static void registerDatePickListener(@NonNull AppCompatActivity activity, @NonNull OnDatePickListener listener) { - activity.getSupportFragmentManager().setFragmentResultListener( - PICK_DATE_REQUEST_KEY, - activity, - (requestKey, result) -> listener.onDatePicked( - (LoyaltyCardField) Objects.requireNonNull(result.getSerializable(TEXT_FIELD_TO_EDIT_ARGUMENT_KEY)), - (Date) Objects.requireNonNull(result.getSerializable(NEWLY_PICKED_DATE_ARGUMENT_KEY)))); - } + MaterialDatePicker materialDatePicker = MaterialDatePicker.Builder.datePicker() + .setSelection(calendar.getTimeInMillis()) + .setCalendarConstraints(calendarConstraints) + .build(); - @NonNull - @Override - public Dialog onCreateDialog(Bundle savedInstanceState) { - Bundle args = requireArguments(); - textFieldEdit = (LoyaltyCardField) args.getSerializable(TEXT_FIELD_TO_EDIT_ARGUMENT_KEY); - minDate = (Date) args.getSerializable(MIN_DATE_ARGUMENT_KEY); - maxDate = (Date) args.getSerializable(MAX_DATE_ARGUMENT_KEY); - // Use the current date as the default date in the picker - final Calendar c = Calendar.getInstance(); + materialDatePicker.addOnPositiveButtonClickListener(listener); + materialDatePicker.show(getSupportFragmentManager(), TAG); + } - Date date = (Date) args.getSerializable(CURRENT_DATE_ARGUMENT_KEY); - if (date != null) { - c.setTime(date); - } + private long getDefaultMinDateOfDatePicker() { + Calendar minDateCalendar = Calendar.getInstance(); + minDateCalendar.set(1970, 0, 1); + return minDateCalendar.getTimeInMillis(); + } - int year = c.get(Calendar.YEAR); - int month = c.get(Calendar.MONTH); - int day = c.get(Calendar.DAY_OF_MONTH); - - // Create a new instance of DatePickerDialog and return it - DatePickerDialog datePickerDialog = new DatePickerDialog(getActivity(), this, year, month, day); - datePickerDialog.getDatePicker().setMinDate(minDate != null ? minDate.getTime() : getDefaultMinDateOfDatePicker()); - datePickerDialog.getDatePicker().setMaxDate(maxDate != null ? maxDate.getTime() : getDefaultMaxDateOfDatePicker()); - return datePickerDialog; - } - - private long getDefaultMinDateOfDatePicker() { - Calendar minDateCalendar = Calendar.getInstance(); - minDateCalendar.set(1970, 0, 1); - return minDateCalendar.getTimeInMillis(); - } - - private long getDefaultMaxDateOfDatePicker() { - Calendar maxDateCalendar = Calendar.getInstance(); - maxDateCalendar.set(2100, 11, 31); - return maxDateCalendar.getTimeInMillis(); - } - - public void onDateSet(DatePicker view, int year, int month, int day) { - Calendar c = new GregorianCalendar(); - c.set(Calendar.YEAR, year); - c.set(Calendar.MONTH, month); - c.set(Calendar.DAY_OF_MONTH, day); - c.set(Calendar.HOUR_OF_DAY, 0); - c.set(Calendar.MINUTE, 0); - c.set(Calendar.SECOND, 0); - c.set(Calendar.MILLISECOND, 0); - - long unixTime = c.getTimeInMillis(); - - Date date = new Date(unixTime); - - Bundle result = new Bundle(); - result.putSerializable(TEXT_FIELD_TO_EDIT_ARGUMENT_KEY, textFieldEdit); - result.putSerializable(NEWLY_PICKED_DATE_ARGUMENT_KEY, date); - getParentFragmentManager().setFragmentResult(PICK_DATE_REQUEST_KEY, result); - } + private long getDefaultMaxDateOfDatePicker() { + Calendar maxDateCalendar = Calendar.getInstance(); + maxDateCalendar.set(2100, 11, 31); + return maxDateCalendar.getTimeInMillis(); } private void doSave() { From 5cab0e3932f58fc2a896818101396d1fbfce2fb5 Mon Sep 17 00:00:00 2001 From: Aayush Gupta Date: Tue, 24 Oct 2023 21:35:19 +0530 Subject: [PATCH 2/3] LoyaltyCardViewActivityTest: Update test to handle MaterialDatePicker migration Signed-off-by: Aayush Gupta --- .../protect/card_locker/LoyaltyCardViewActivityTest.java | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/app/src/test/java/protect/card_locker/LoyaltyCardViewActivityTest.java b/app/src/test/java/protect/card_locker/LoyaltyCardViewActivityTest.java index e9fd035fe..bf5548f09 100644 --- a/app/src/test/java/protect/card_locker/LoyaltyCardViewActivityTest.java +++ b/app/src/test/java/protect/card_locker/LoyaltyCardViewActivityTest.java @@ -2,6 +2,7 @@ package protect.card_locker; import android.app.Activity; import android.app.DatePickerDialog; +import android.app.Dialog; import android.content.Context; import android.content.Intent; import android.content.SharedPreferences; @@ -725,9 +726,9 @@ public class LoyaltyCardViewActivityTest { shadowOf(getMainLooper()).idle(); - DatePickerDialog datePickerDialog = (DatePickerDialog) (ShadowDialog.getLatestDialog()); + Dialog datePickerDialog = ShadowDialog.getLatestDialog(); assertNotNull(datePickerDialog); - datePickerDialog.getButton(DatePickerDialog.BUTTON_POSITIVE).performClick(); + datePickerDialog.findViewById(com.google.android.material.R.id.confirm_button).performClick(); shadowOf(getMainLooper()).idle(); From 55595159bef79843fd6abf83c0e0829a7ce49fb2 Mon Sep 17 00:00:00 2001 From: Aayush Gupta Date: Thu, 2 Nov 2023 14:20:18 +0530 Subject: [PATCH 3/3] LoyaltyCardEditActivity: Handle configuration changes for MaterialDatePicker MaterialDatePicker is final and thus cannot be extended to handle loss of callback on configuration changes. We aren't using ViewModel as well that would help us to persist changes till lifecycle. Fallback to how DatePicker was handling this situation with a couple of more hacks. Signed-off-by: Aayush Gupta --- .../card_locker/LoyaltyCardEditActivity.java | 83 ++++++++++++++----- 1 file changed, 61 insertions(+), 22 deletions(-) diff --git a/app/src/main/java/protect/card_locker/LoyaltyCardEditActivity.java b/app/src/main/java/protect/card_locker/LoyaltyCardEditActivity.java index 2e1d701c8..e74acebdf 100644 --- a/app/src/main/java/protect/card_locker/LoyaltyCardEditActivity.java +++ b/app/src/main/java/protect/card_locker/LoyaltyCardEditActivity.java @@ -53,7 +53,6 @@ import com.google.android.material.datepicker.CalendarConstraints; import com.google.android.material.datepicker.DateValidatorPointBackward; import com.google.android.material.datepicker.DateValidatorPointForward; import com.google.android.material.datepicker.MaterialDatePicker; -import com.google.android.material.datepicker.MaterialPickerOnPositiveButtonClickListener; import com.google.android.material.dialog.MaterialAlertDialogBuilder; import com.google.android.material.floatingactionbutton.FloatingActionButton; import com.google.android.material.tabs.TabLayout; @@ -80,6 +79,7 @@ import java.util.LinkedHashMap; import java.util.List; import java.util.Locale; import java.util.NoSuchElementException; +import java.util.Objects; import java.util.concurrent.Callable; import protect.card_locker.async.TaskHandler; @@ -92,6 +92,7 @@ public class LoyaltyCardEditActivity extends CatimaAppCompatActivity implements private final String STATE_TAB_INDEX = "savedTab"; private final String STATE_TEMP_CARD = "tempLoyaltyCard"; + private final String STATE_TEMP_CARD_FIELD = "tempLoyaltyCardField"; private final String STATE_REQUESTED_IMAGE = "requestedImage"; private final String STATE_FRONT_IMAGE_UNSAVED = "frontImageUnsaved"; private final String STATE_BACK_IMAGE_UNSAVED = "backImageUnsaved"; @@ -103,6 +104,9 @@ public class LoyaltyCardEditActivity extends CatimaAppCompatActivity implements private final String STATE_ICON_REMOVED = "iconRemoved"; private final String STATE_OPEN_SET_ICON_MENU = "openSetIconMenu"; + private static final String PICK_DATE_REQUEST_KEY = "pick_date_request"; + private static final String NEWLY_PICKED_DATE_ARGUMENT_KEY = "newly_picked_date"; + private final String TEMP_CAMERA_IMAGE_NAME = LoyaltyCardEditActivity.class.getSimpleName() + "_camera_image.jpg"; private final String TEMP_CROP_IMAGE_NAME = LoyaltyCardEditActivity.class.getSimpleName() + "_crop_image.png"; private final Bitmap.CompressFormat TEMP_CROP_IMAGE_FORMAT = Bitmap.CompressFormat.PNG; @@ -180,6 +184,7 @@ public class LoyaltyCardEditActivity extends CatimaAppCompatActivity implements HashMap currencySymbols = new HashMap<>(); LoyaltyCard tempLoyaltyCard; + LoyaltyCardField tempLoyaltyCardField; ActivityResultLauncher mPhotoTakerLauncher; ActivityResultLauncher mPhotoPickerLauncher; @@ -265,6 +270,7 @@ public class LoyaltyCardEditActivity extends CatimaAppCompatActivity implements tabs = binding.tabs; savedInstanceState.putInt(STATE_TAB_INDEX, tabs.getSelectedTabPosition()); savedInstanceState.putParcelable(STATE_TEMP_CARD, tempLoyaltyCard); + savedInstanceState.putSerializable(STATE_TEMP_CARD_FIELD, tempLoyaltyCardField); savedInstanceState.putInt(STATE_REQUESTED_IMAGE, mRequestedImage); Object cardImageFrontObj = cardImageFront.getTag(); @@ -300,6 +306,7 @@ public class LoyaltyCardEditActivity extends CatimaAppCompatActivity implements public void onRestoreInstanceState(@NonNull Bundle savedInstanceState) { onRestoring = true; tempLoyaltyCard = savedInstanceState.getParcelable(STATE_TEMP_CARD); + tempLoyaltyCardField = (LoyaltyCardField) savedInstanceState.getSerializable(STATE_TEMP_CARD_FIELD); super.onRestoreInstanceState(savedInstanceState); tabs = binding.tabs; tabs.selectTab(tabs.getTabAt(savedInstanceState.getInt(STATE_TAB_INDEX))); @@ -385,6 +392,8 @@ public class LoyaltyCardEditActivity extends CatimaAppCompatActivity implements addDateFieldTextChangedListener(expiryField, R.string.never, R.string.chooseExpiryDate, LoyaltyCardField.expiry); + setMaterialDatePickerResultListener(); + balanceField.setOnFocusChangeListener((v, hasFocus) -> { if (!hasFocus && !onResuming && !onRestoring) { if (balanceField.getText().toString().isEmpty()) { @@ -1006,29 +1015,13 @@ public class LoyaltyCardEditActivity extends CatimaAppCompatActivity implements if (!lastValue.toString().equals(getString(chooseDateOptionStringId))) { dateField.setText(lastValue); } - MaterialPickerOnPositiveButtonClickListener materialPickerOnPositiveButtonClickListener = selection -> { - Date newDate = new Date(selection); - switch (loyaltyCardField) { - case validFrom: - formatDateField(LoyaltyCardEditActivity.this, validFromField, newDate); - updateTempState(LoyaltyCardField.validFrom, newDate); - break; - case expiry: - formatDateField(LoyaltyCardEditActivity.this, expiryField, newDate); - updateTempState(LoyaltyCardField.expiry, newDate); - break; - default: - throw new AssertionError("Unexpected field: " + loyaltyCardField); - } - }; showDatePicker( loyaltyCardField, (Date) dateField.getTag(), // if the expiry date is being set, set date picker's minDate to the 'valid from' date loyaltyCardField == LoyaltyCardField.expiry ? (Date) validFromField.getTag() : null, // if the 'valid from' date is being set, set date picker's maxDate to the expiry date - loyaltyCardField == LoyaltyCardField.validFrom ? (Date) expiryField.getTag() : null, - materialPickerOnPositiveButtonClickListener + loyaltyCardField == LoyaltyCardField.validFrom ? (Date) expiryField.getTag() : null ); } } @@ -1384,8 +1377,7 @@ public class LoyaltyCardEditActivity extends CatimaAppCompatActivity implements LoyaltyCardField loyaltyCardField, @Nullable Date selectedDate, @Nullable Date minDate, - @Nullable Date maxDate, - MaterialPickerOnPositiveButtonClickListener listener + @Nullable Date maxDate ) { // Create a new instance of MaterialDatePicker and return it long startDate = minDate != null ? minDate.getTime() : getDefaultMinDateOfDatePicker(); @@ -1420,8 +1412,55 @@ public class LoyaltyCardEditActivity extends CatimaAppCompatActivity implements .setCalendarConstraints(calendarConstraints) .build(); - materialDatePicker.addOnPositiveButtonClickListener(listener); - materialDatePicker.show(getSupportFragmentManager(), TAG); + // Required to handle configuration changes + // See https://github.com/material-components/material-components-android/issues/1688 + tempLoyaltyCardField = loyaltyCardField; + getSupportFragmentManager().addFragmentOnAttachListener((fragmentManager, fragment) -> { + if (fragment instanceof MaterialDatePicker && Objects.equals(fragment.getTag(), PICK_DATE_REQUEST_KEY)) { + ((MaterialDatePicker) fragment).addOnPositiveButtonClickListener(selection -> { + Bundle args = new Bundle(); + args.putLong(NEWLY_PICKED_DATE_ARGUMENT_KEY, selection); + getSupportFragmentManager().setFragmentResult(PICK_DATE_REQUEST_KEY, args); + }); + } + }); + + materialDatePicker.show(getSupportFragmentManager(), PICK_DATE_REQUEST_KEY); + } + + // Required to handle configuration changes + // See https://github.com/material-components/material-components-android/issues/1688 + private void setMaterialDatePickerResultListener() { + MaterialDatePicker fragment = (MaterialDatePicker) getSupportFragmentManager().findFragmentByTag(PICK_DATE_REQUEST_KEY); + if (fragment != null) { + fragment.addOnPositiveButtonClickListener(selection -> { + Bundle args = new Bundle(); + args.putLong(NEWLY_PICKED_DATE_ARGUMENT_KEY, selection); + getSupportFragmentManager().setFragmentResult(PICK_DATE_REQUEST_KEY, args); + }); + } + + getSupportFragmentManager().setFragmentResultListener( + PICK_DATE_REQUEST_KEY, + this, + (requestKey, result) -> { + long selection = result.getLong(NEWLY_PICKED_DATE_ARGUMENT_KEY); + + Date newDate = new Date(selection); + switch (tempLoyaltyCardField) { + case validFrom: + formatDateField(LoyaltyCardEditActivity.this, validFromField, newDate); + updateTempState(LoyaltyCardField.validFrom, newDate); + break; + case expiry: + formatDateField(LoyaltyCardEditActivity.this, expiryField, newDate); + updateTempState(LoyaltyCardField.expiry, newDate); + break; + default: + throw new AssertionError("Unexpected field: " + tempLoyaltyCardField); + } + } + ); } private long getDefaultMinDateOfDatePicker() {