diff --git a/android/app/src/main/java/app/organicmaps/widget/placepage/PlacePageView.java b/android/app/src/main/java/app/organicmaps/widget/placepage/PlacePageView.java index 46cf8439f..3b2017f24 100644 --- a/android/app/src/main/java/app/organicmaps/widget/placepage/PlacePageView.java +++ b/android/app/src/main/java/app/organicmaps/widget/placepage/PlacePageView.java @@ -85,9 +85,11 @@ import com.google.android.material.textview.MaterialTextView; import java.time.Instant; import java.time.ZoneId; import java.time.ZonedDateTime; +import java.time.format.TextStyle; import java.util.ArrayList; import java.util.Arrays; import java.util.List; +import java.util.Locale; public class PlacePageView extends Fragment implements View.OnClickListener, View.OnLongClickListener, LocationListener, SensorListener, Observer, @@ -797,57 +799,92 @@ public class PlacePageView extends Fragment final String ohStr = mMapObject.getMetadata(Metadata.MetadataType.FMD_OPEN_HOURS); final Timetable[] timetables = OpeningHours.nativeTimetablesFromString(ohStr); - if (timetables != null && timetables.length != 0) + // No valid timetable + if (timetables == null || timetables.length == 0) { - final Context context = requireContext(); - final OhState poiState = OpeningHours.nativeCurrentState(timetables); - - // Ignore unknown rule state - if (poiState.state == OhState.State.Unknown) - { - UiUtils.hide(mTvOpenState); - return; - } - - // Get colours - final ForegroundColorSpan colorGreen = - new ForegroundColorSpan(ContextCompat.getColor(context, R.color.base_green)); - final ForegroundColorSpan colorYellow = - new ForegroundColorSpan(ContextCompat.getColor(context, R.color.base_yellow)); - final ForegroundColorSpan colorRed = new ForegroundColorSpan(ContextCompat.getColor(context, R.color.base_red)); - - // Get next state info - final SpannableStringBuilder openStateString = new SpannableStringBuilder(); - final boolean isOpen = (poiState.state == OhState.State.Open); // False == Closed due to early exit for Unknown - final long nextStateTime = isOpen ? poiState.nextTimeClosed : poiState.nextTimeOpen; // Unix time (seconds) - final int minsToNextState = (int) ((nextStateTime - (System.currentTimeMillis() / 1000)) / 60); - - if (minsToNextState <= 60) // POI opens/closes in 60 mins - { - final String minsToChangeStr = minsToNextState + " " + getString(R.string.minute); - final String nextChangeFormatted = getString(isOpen ? R.string.closes_in : R.string.opens_in, minsToChangeStr); - final ForegroundColorSpan nextChangeColor = isOpen ? colorYellow : colorRed; - // TODO: We should check closed/open time for specific feature's timezone. - ZonedDateTime time = ZonedDateTime.ofInstant(Instant.ofEpochSecond(nextStateTime), ZoneId.systemDefault()); - String localizedTime = - new HoursMinutes(time.getHour(), time.getMinute(), DateUtils.is24HourFormat(context)).toString(); - - openStateString.append(nextChangeFormatted, nextChangeColor, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE) - .append(" • ") // Add spacer - .append(getString(R.string.at, localizedTime)); - } - else if (isOpen) - openStateString.append(getString(R.string.open_now), colorGreen, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); - // TODO: Add "Closes at 18:00" etc - else // Closed - openStateString.append(getString(R.string.closed_now), colorRed, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); - // TODO: Add "Opens at 18:00" etc - - UiUtils.setTextAndHideIfEmpty(mTvOpenState, openStateString); + UiUtils.hide(mTvOpenState); return; } - // No valid timetable - UiUtils.hide(mTvOpenState); + + final Context context = requireContext(); + final OhState poiState = OpeningHours.nativeCurrentState(timetables); + + // Ignore unknown rule state + if (poiState.state == OhState.State.Unknown) + { + UiUtils.hide(mTvOpenState); + return; + } + + // Get colours + final ForegroundColorSpan colorGreen = + new ForegroundColorSpan(ContextCompat.getColor(context, R.color.base_green)); + final ForegroundColorSpan colorYellow = + new ForegroundColorSpan(ContextCompat.getColor(context, R.color.base_yellow)); + final ForegroundColorSpan colorRed = new ForegroundColorSpan(ContextCompat.getColor(context, R.color.base_red)); + + // Get next state info + final SpannableStringBuilder openStateString = new SpannableStringBuilder(); + final boolean isOpen = (poiState.state == OhState.State.Open); // False == Closed due to early exit for Unknown + final long nextStateTime = isOpen ? poiState.nextTimeClosed : poiState.nextTimeOpen; // Unix time (seconds) + final long nowSec = System.currentTimeMillis() / 1000; + final int minsToNextState = (int) ((nextStateTime - nowSec) / 60); + + // NOTE: Timezone is currently device timezone. TODO: use feature-specific timezone. + final ZonedDateTime nextChangeLocal = + ZonedDateTime.ofInstant(Instant.ofEpochSecond(nextStateTime), ZoneId.systemDefault()); + + String localizedTimeString = OpenStateTextFormatter.formatHoursMinutes( + nextChangeLocal.getHour(), nextChangeLocal.getMinute(), DateUtils.is24HourFormat(context)); + + if (minsToNextState <= 60 && minsToNextState >= 0) // POI Opens/Closes in 60 mins • at 18:00 + { + final String minsToChangeStr = getResources().getQuantityString( + R.plurals.minutes, Math.max(minsToNextState, 1), Math.max(minsToNextState, 1)); + final String nextChangeFormatted = getString(isOpen ? R.string.closes_in : R.string.opens_in, minsToChangeStr); + final ForegroundColorSpan nextChangeColor = isOpen ? colorYellow : colorRed; + + openStateString.append(nextChangeFormatted, nextChangeColor, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE) + .append(" • ") // Add spacer + .append(getString(R.string.at, localizedTimeString)); + } + else + { + final String opensAtStr = getString(R.string.opens_at); // "Opens at %s" + final String closesAtStr = getString(R.string.closes_at); // "Closes at %s" + final String opensDayAtStr = getString(R.string.opens_day_at); // "Opens %1$s at %2$s" + final String closesDayAtStr = getString(R.string.closes_day_at); // "Closes %1$s at %2$s" + + final boolean isToday = + OpenStateTextFormatter.isSameLocalDate(nextChangeLocal, ZonedDateTime.now(nextChangeLocal.getZone())); + final String dayShort = + nextChangeLocal.getDayOfWeek().getDisplayName(TextStyle.SHORT, Locale.getDefault()); + + if (isOpen) // > 60 minutes OR negative (safety). Show “Open now • Closes at 18:00” + { + openStateString.append(getString(R.string.open_now), colorGreen, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); + + final String atLabel = + OpenStateTextFormatter.buildAtLabel(false, isToday, dayShort, localizedTimeString, + opensAtStr, closesAtStr, opensDayAtStr, closesDayAtStr); + + if (!TextUtils.isEmpty(atLabel)) + openStateString.append(" • ").append(atLabel); + } + else // Closed + { + openStateString.append(getString(R.string.closed_now), colorRed, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); + + final String atLabel = + OpenStateTextFormatter.buildAtLabel(true, isToday, dayShort, localizedTimeString, + opensAtStr, closesAtStr, opensDayAtStr, closesDayAtStr); + + if (!TextUtils.isEmpty(atLabel)) + openStateString.append(" • ").append(atLabel); + } + } + + UiUtils.setTextAndHideIfEmpty(mTvOpenState, openStateString); } private void addPlace() diff --git a/android/app/src/main/res/values/strings.xml b/android/app/src/main/res/values/strings.xml index 5063610de..303ef9dc8 100644 --- a/android/app/src/main/res/values/strings.xml +++ b/android/app/src/main/res/values/strings.xml @@ -441,6 +441,14 @@ Opens in %s Closes in %s Closed + Opens at %s + Closes at %s + Opens %1$s at %2$s + Closes %1$s at %2$s + + %d minute + %d minutes + Confirmed %s