diff --git a/android/build.gradle b/android/build.gradle index f54a3b9ff8..d072805205 100644 --- a/android/build.gradle +++ b/android/build.gradle @@ -144,7 +144,7 @@ configurations.all { force "com.google.firebase:firebase-measurement-connector:18.0.0" force "com.google.firebase:firebase-iid-interop:17.0.0" force "com.google.firebase:firebase-common:19.3.0" - force "com.google.android.material:material:1.1.0" + force "com.google.android.material:material:1.2.0" force "androidx.constraintlayout:constraintlayout:1.1.3" force "androidx.vectordrawable:vectordrawable:1.1.0" force "androidx.coordinatorlayout:coordinatorlayout:1.1.0" diff --git a/android/src/com/mapswithme/maps/search/FilterUtils.java b/android/src/com/mapswithme/maps/search/FilterUtils.java index 28a343f650..a9e18f0150 100644 --- a/android/src/com/mapswithme/maps/search/FilterUtils.java +++ b/android/src/com/mapswithme/maps/search/FilterUtils.java @@ -1,18 +1,36 @@ package com.mapswithme.maps.search; +import android.content.Context; + import androidx.annotation.IntDef; import androidx.annotation.NonNull; import androidx.annotation.Nullable; +import com.google.android.material.datepicker.CalendarConstraints; +import com.google.android.material.datepicker.CompositeDateValidator; +import com.google.android.material.datepicker.DateValidatorPointBackward; +import com.google.android.material.datepicker.DateValidatorPointForward; +import com.google.android.material.datepicker.MaterialDatePicker; +import com.mapswithme.maps.R; +import com.mapswithme.util.Utils; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; +import java.text.SimpleDateFormat; import java.util.ArrayList; import java.util.Arrays; +import java.util.Calendar; +import java.util.Date; import java.util.Iterator; import java.util.List; +import java.util.Locale; +import java.util.concurrent.TimeUnit; public class FilterUtils { + private static final int MAX_STAYING_DAYS = 30; + private static final int MAX_CHECKIN_WINDOW_IN_DAYS = 365; + private static final String DAY_OF_MONTH_PATTERN = "MMM d"; + @Retention(RetentionPolicy.SOURCE) @IntDef({ RATING_ANY, RATING_GOOD, RATING_VERYGOOD, RATING_EXCELLENT }) public @interface RatingDef @@ -180,4 +198,66 @@ public class FilterUtils List hotelTypes = new ArrayList<>(Arrays.asList(types)); return makeOneOf(hotelTypes.iterator()); } + + public static long getMaxCheckoutInMillis(long checkinMillis) + { + long difference = checkinMillis - MaterialDatePicker.todayInUtcMilliseconds(); + int daysToCheckin = (int) TimeUnit.MILLISECONDS.toDays(difference); + int leftDays = MAX_CHECKIN_WINDOW_IN_DAYS - daysToCheckin; + if (leftDays <= 0) + throw new AssertionError("No available dates for checkout!"); + Calendar date = Utils.getCalendarInstance(); + date.setTimeInMillis(checkinMillis); + date.add(Calendar.DAY_OF_YEAR, Math.min(leftDays, MAX_STAYING_DAYS)); + return date.getTimeInMillis(); + } + + private static long getMaxCheckinInMillis() + { + final long today = MaterialDatePicker.todayInUtcMilliseconds(); + Calendar calendar = Utils.getCalendarInstance(); + calendar.setTimeInMillis(today); + calendar.add(Calendar.DAY_OF_YEAR, MAX_CHECKIN_WINDOW_IN_DAYS); + return calendar.getTimeInMillis(); + } + + @NonNull + public static CalendarConstraints.Builder createDateConstraintsBuilder() + { + final long today = MaterialDatePicker.todayInUtcMilliseconds(); + CalendarConstraints.Builder constraintsBuilder = new CalendarConstraints.Builder(); + constraintsBuilder.setStart(today); + constraintsBuilder.setEnd(getMaxCheckinInMillis()); + List validators = new ArrayList<>(); + validators.add(DateValidatorPointForward.now()); + validators.add(DateValidatorPointBackward.before(getMaxCheckinInMillis())); + constraintsBuilder.setValidator(CompositeDateValidator.allOf(validators)); + return constraintsBuilder; + } + + @NonNull + public static String makeDateRangeHeader(@NonNull Context context, long checkinMillis, + long checkoutMillis) + { + final SimpleDateFormat dateFormater = new SimpleDateFormat(DAY_OF_MONTH_PATTERN, + Locale.getDefault()); + String checkin = dateFormater.format(new Date(checkinMillis)); + String checkout = dateFormater.format(new Date(checkoutMillis)); + return context.getString(R.string.booking_filter_date_range, checkin, checkout); + } + + public static boolean isWithinMaxStayingDays(long checkinMillis, long checkoutMillis) + { + long difference = checkoutMillis - checkinMillis; + int days = (int) TimeUnit.MILLISECONDS.toDays(difference); + return days <= MAX_STAYING_DAYS; + } + + public static long getDayAfter(long date) + { + Calendar dayAfter = Utils.getCalendarInstance(); + dayAfter.setTimeInMillis(date); + dayAfter.add(Calendar.DAY_OF_YEAR, 1); + return dayAfter.getTimeInMillis(); + } } diff --git a/android/src/com/mapswithme/maps/widget/SearchToolbarController.java b/android/src/com/mapswithme/maps/widget/SearchToolbarController.java index 97c051aa3b..b6ba841660 100644 --- a/android/src/com/mapswithme/maps/widget/SearchToolbarController.java +++ b/android/src/com/mapswithme/maps/widget/SearchToolbarController.java @@ -9,6 +9,7 @@ import android.view.KeyEvent; import android.view.View; import android.view.inputmethod.EditorInfo; import android.widget.EditText; +import android.widget.Toast; import androidx.annotation.NonNull; import androidx.annotation.Nullable; @@ -16,27 +17,25 @@ import androidx.annotation.StringRes; import androidx.appcompat.app.AppCompatActivity; import androidx.core.util.Pair; import com.google.android.material.chip.Chip; +import com.google.android.material.datepicker.CalendarConstraints; import com.google.android.material.datepicker.MaterialDatePicker; import com.google.android.material.datepicker.MaterialPickerOnPositiveButtonClickListener; import com.mapswithme.maps.R; import com.mapswithme.maps.search.BookingFilterParams; +import com.mapswithme.maps.search.FilterUtils; import com.mapswithme.util.InputUtils; import com.mapswithme.util.StringUtils; import com.mapswithme.util.UiUtils; import com.mapswithme.util.statistics.AlohaHelper; -import java.text.SimpleDateFormat; import java.util.ArrayList; -import java.util.Date; import java.util.List; -import java.util.Locale; +import java.util.Objects; public class SearchToolbarController extends ToolbarController implements View.OnClickListener { private static final int REQUEST_VOICE_RECOGNITION = 0xCA11; - public static final String DAY_OF_MONTH_PATTERN = "MMM d"; - @NonNull private final View mSearchContainer; @NonNull @@ -66,27 +65,20 @@ public class SearchToolbarController extends ToolbarController }; @Nullable private Pair mChosenDates; + @NonNull private final View.OnClickListener mChooseDatesClickListener = v -> { MaterialDatePicker.Builder> builder = MaterialDatePicker.Builder.dateRangePicker(); + CalendarConstraints.Builder constraintsBuilder = FilterUtils.createDateConstraintsBuilder(); + builder.setCalendarConstraints(constraintsBuilder.build()); if (mChosenDates != null) builder.setSelection(mChosenDates); - final MaterialDatePicker picker = builder.build(); - picker.addOnPositiveButtonClickListener(new MaterialPickerOnPositiveButtonClickListener() - { - @Override - public void onPositiveButtonClick(Object selection) - { - //noinspection unchecked - mChosenDates = (Pair) selection; - mChooseDatesChip.setText(picker.getHeaderText()); - for (FilterParamsChangedListener listener : mFilterParamsChangedListeners) - listener.onBookingParamsChanged(); - } - }); + final MaterialDatePicker> picker = builder.build(); + picker.addOnPositiveButtonClickListener(new DatePickerPositiveClickListener(picker)); picker.show(((AppCompatActivity) getActivity()).getSupportFragmentManager(), picker.toString()); }; + @NonNull private List mFilterParamsChangedListeners = new ArrayList<>(); @@ -133,19 +125,18 @@ public class SearchToolbarController extends ToolbarController } public void setFilterParams(@NonNull BookingFilterParams params) + { + formatAndSetChosenDates(params.getCheckinMillisec(), params.getCheckoutMillisec()); + } + + private void formatAndSetChosenDates(long checkinMillis, long checkoutMillis) { if (mChooseDatesChip == null) return; - mChosenDates = new Pair<>(params.getCheckinMillisec(), params.getCheckoutMillisec()); - SimpleDateFormat dateFormater = new SimpleDateFormat(DAY_OF_MONTH_PATTERN, - Locale.getDefault()); - @SuppressWarnings("ConstantConditions") - String start = dateFormater.format(new Date(mChosenDates.first)); - @SuppressWarnings("ConstantConditions") - String end = dateFormater.format(new Date(mChosenDates.second)); - mChooseDatesChip.setText(getActivity().getString(R.string.booking_filter_date_range, - start, end)); + mChooseDatesChip.setText(FilterUtils.makeDateRangeHeader(getActivity(), checkinMillis, + checkoutMillis)); + mChosenDates = new Pair<>(checkinMillis, checkoutMillis); } public void resetFilterParams() @@ -320,4 +311,50 @@ public class SearchToolbarController extends ToolbarController { void onBookingParamsChanged(); } + + private class DatePickerPositiveClickListener + implements MaterialPickerOnPositiveButtonClickListener> + { + @NonNull + private final MaterialDatePicker> mPicker; + + private DatePickerPositiveClickListener(@NonNull MaterialDatePicker> picker) + { + mPicker = picker; + } + + @Override + public void onPositiveButtonClick(Pair selection) + { + if (selection == null) + return; + + mChosenDates = selection; + if (mChosenDates.first == null || mChosenDates.second == null) + return; + + validateAndSetupDates(mChosenDates.first, mChosenDates.second); + + for (FilterParamsChangedListener listener : mFilterParamsChangedListeners) + listener.onBookingParamsChanged(); + } + + private void validateAndSetupDates(long checkinMillis, long checkoutMillis) + { + if (checkoutMillis <= checkinMillis) + { + formatAndSetChosenDates(checkinMillis, FilterUtils.getDayAfter(checkinMillis)); + } + else if (!FilterUtils.isWithinMaxStayingDays(checkinMillis, checkoutMillis)) + { + Toast.makeText(getActivity(), R.string.thirty_days_limit_dialog, Toast.LENGTH_LONG).show(); + formatAndSetChosenDates(checkinMillis, FilterUtils.getMaxCheckoutInMillis(checkinMillis)); + } + else + { + Objects.requireNonNull(mChooseDatesChip); + mChooseDatesChip.setText(mPicker.getHeaderText()); + } + } + } } diff --git a/android/src/com/mapswithme/util/Utils.java b/android/src/com/mapswithme/util/Utils.java index faf3ad06ff..f6bc589929 100644 --- a/android/src/com/mapswithme/util/Utils.java +++ b/android/src/com/mapswithme/util/Utils.java @@ -34,8 +34,8 @@ import androidx.fragment.app.Fragment; import com.mapswithme.maps.BuildConfig; import com.mapswithme.maps.MwmApplication; import com.mapswithme.maps.R; -import com.mapswithme.maps.base.CustomNavigateUpListener; import com.mapswithme.maps.analytics.ExternalLibrariesMediator; +import com.mapswithme.maps.base.CustomNavigateUpListener; import com.mapswithme.util.concurrency.UiThread; import com.mapswithme.util.log.Logger; import com.mapswithme.util.log.LoggerFactory; @@ -48,11 +48,13 @@ import java.net.NetworkInterface; import java.security.MessageDigest; import java.text.NumberFormat; import java.util.Arrays; +import java.util.Calendar; import java.util.Collections; import java.util.Currency; import java.util.List; import java.util.Locale; import java.util.Map; +import java.util.TimeZone; public class Utils { @@ -795,6 +797,12 @@ public class Utils return getLocalizedFeatureByKey(context, key); } + @NonNull + public static Calendar getCalendarInstance() + { + return Calendar.getInstance(TimeZone.getTimeZone("UTC")); + } + private static class SupportInfoWithLogsCallback implements LoggerFactory.OnZipCompletedListener { @NonNull