[Android] Add current opening hours status to placepage preview

And refresh it every 45s
adjust base_red/yellow/green colours

Signed-off-by: Harry Bond <me@hbond.xyz>
This commit is contained in:
Harry Bond
2025-06-25 23:06:49 +01:00
committed by Konstantin Pastbin
parent 8213a278d7
commit 86dacd70ff
7 changed files with 145 additions and 6 deletions

View File

@@ -321,4 +321,30 @@ Java_app_organicmaps_editor_OpeningHours_nativeIsTimetableStringValid(JNIEnv * e
{
return OpeningHours(jni::ToNativeString(env, jSource)).IsValid();
}
JNIEXPORT jobject JNICALL
Java_app_organicmaps_editor_OpeningHours_nativeCurrentState(JNIEnv * env, jclass clazz, jobjectArray jTts)
{
TimeTableSet tts = NativeTimetableSet(env, jTts);
time_t const now = time(nullptr);
/// @todo We should check closed/open time for specific feature's timezone.
OpeningHours::InfoT ohInfo = MakeOpeningHours(tts).GetInfo(now);
jclass ohStateClass = jni::GetGlobalClassRef(env, "app/organicmaps/editor/OhState");
jclass ruleStateClass = jni::GetGlobalClassRef(env, "app/organicmaps/editor/OhState$State");
static const std::unordered_map<RuleState, const char*> ruleState = {
{RuleState::Open, "Open"},
{RuleState::Closed, "Closed"},
{RuleState::Unknown, "Unknown"}
};
jfieldID stateField = env->GetStaticFieldID(ruleStateClass, ruleState.at(ohInfo.state), "Lapp/organicmaps/editor/OhState$State;");
jobject stateObj = env->GetStaticObjectField(ruleStateClass, stateField);
jmethodID constructor = env->GetMethodID(ohStateClass, "<init>", "(Lapp/organicmaps/editor/OhState$State;JJ)V");
jobject javaOhState = env->NewObject(ohStateClass, constructor, stateObj, (jlong) ohInfo.nextTimeOpen, (jlong) ohInfo.nextTimeClosed);
return javaOhState;
}
} // extern "C"

View File

@@ -0,0 +1,30 @@
package app.organicmaps.editor;
import androidx.annotation.Keep;
// Used by JNI.
@Keep
public class OhState
{
public enum State
{
Open,
Closed,
Unknown
}
public State state;
/** Unix timestamp in seconds**/
public long nextTimeOpen;
/** Unix timestamp in seconds **/
public long nextTimeClosed;
// Used by JNI.
@Keep
public OhState(State state, long nextTimeOpen, long nextTimeClosed)
{
this.state = state;
this.nextTimeOpen = nextTimeOpen;
this.nextTimeClosed = nextTimeClosed;
}
}

View File

@@ -60,4 +60,6 @@ public final class OpeningHours
* @return true if timetable string is valid OSM timetable.
*/
public static native boolean nativeIsTimetableStringValid(String source);
public static native OhState nativeCurrentState(@NonNull Timetable[] timetables);
}

View File

@@ -33,11 +33,16 @@ import app.organicmaps.downloader.CountryItem;
import app.organicmaps.downloader.DownloaderStatusIcon;
import app.organicmaps.downloader.MapManager;
import app.organicmaps.editor.Editor;
import app.organicmaps.editor.OhState;
import app.organicmaps.editor.OpeningHours;
import app.organicmaps.editor.data.HoursMinutes;
import app.organicmaps.editor.data.Timetable;
import app.organicmaps.location.LocationHelper;
import app.organicmaps.location.LocationListener;
import app.organicmaps.location.SensorHelper;
import app.organicmaps.location.SensorListener;
import app.organicmaps.routing.RoutingController;
import app.organicmaps.util.DateUtils;
import app.organicmaps.util.SharingUtils;
import app.organicmaps.util.StringUtils;
import app.organicmaps.util.UiUtils;
@@ -54,6 +59,9 @@ import com.google.android.material.appbar.MaterialToolbar;
import com.google.android.material.button.MaterialButton;
import com.google.android.material.textview.MaterialTextView;
import java.time.Instant;
import java.time.ZoneId;
import java.time.ZonedDateTime;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
@@ -83,12 +91,15 @@ public class PlacePageView extends Fragment implements View.OnClickListener,
CoordinatesFormat.MGRS,
CoordinatesFormat.OSMLink);
private View mFrame;
private Context mContext;
// Preview.
private ViewGroup mPreview;
private MaterialToolbar mToolbar;
private MaterialTextView mTvTitle;
private MaterialTextView mTvSecondaryTitle;
private MaterialTextView mTvSubtitle;
private MaterialTextView mTvOpenState;
private ArrowView mAvDirection;
private MaterialTextView mTvDistance;
private MaterialTextView mTvAddress;
@@ -201,7 +212,6 @@ public class PlacePageView extends Fragment implements View.OnClickListener,
mFrame = view;
mFrame.setOnClickListener((v) -> mPlacePageViewListener.onPlacePageRequestToggleState());
mPreview = mFrame.findViewById(R.id.pp__preview);
mFrame.addOnLayoutChangeListener((v, left, top, right, bottom, oldLeft, oldTop, oldRight, oldBottom) -> {
@@ -220,6 +230,7 @@ public class PlacePageView extends Fragment implements View.OnClickListener,
mTvSecondaryTitle.setOnClickListener(this);
mToolbar = mFrame.findViewById(R.id.toolbar);
mTvSubtitle = mPreview.findViewById(R.id.tv__subtitle);
mTvOpenState = mPreview.findViewById(R.id.tv__open_state);
View directionFrame = mPreview.findViewById(R.id.direction_frame);
mTvDistance = mPreview.findViewById(R.id.tv__straight_distance);
@@ -312,6 +323,7 @@ public class PlacePageView extends Fragment implements View.OnClickListener,
mViewModel.getMapObject().removeObserver(this);
LocationHelper.from(requireContext()).removeListener(this);
SensorHelper.from(requireContext()).removeListener(this);
UiThread.cancelDelayedTasks(updateOpenState);
detachCountry();
}
@@ -412,6 +424,8 @@ public class PlacePageView extends Fragment implements View.OnClickListener,
{
UiUtils.setTextAndHideIfEmpty(mTvTitle, mMapObject.getTitle());
UiUtils.setTextAndHideIfEmpty(mTvSecondaryTitle, mMapObject.getSecondaryTitle());
refreshOpenState();
if (mToolbar != null)
mToolbar.setTitle(mMapObject.getTitle());
setTextAndColorizeSubtitle();
@@ -546,6 +560,62 @@ public class PlacePageView extends Fragment implements View.OnClickListener,
mTvLatlon.setText(latLon);
}
Runnable updateOpenState = this::refreshOpenState;
private void refreshOpenState()
{
UiThread.runLater(updateOpenState, 45000); // Refresh every 45s
final String ohStr = mMapObject.getMetadata(Metadata.MetadataType.FMD_OPEN_HOURS);
final Timetable[] timetables = OpeningHours.nativeTimetablesFromString(ohStr);
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);
return;
}
// No valid timetable
UiUtils.hide(mTvOpenState);
}
private void addOrganisation()
{
((MwmActivity) requireActivity()).showPositionChooserForEditor(true, false);
@@ -556,9 +626,6 @@ public class PlacePageView extends Fragment implements View.OnClickListener,
((MwmActivity) requireActivity()).showPositionChooserForEditor(false, true);
}
/// @todo
/// - Why ll__place_editor and ll__place_latlon check if (mMapObject == null)
@Override
public void onClick(View v)
{

View File

@@ -71,6 +71,16 @@
android:textAppearance="@style/MwmTextAppearance.Body3"
tools:background="#300000F0"
tools:text="Subtitle, very very very very very very very long" />
<com.google.android.material.textview.MaterialTextView
android:id="@+id/tv__open_state"
android:textAlignment="viewStart"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="@dimen/margin_quarter"
android:ellipsize="end"
android:textAppearance="@style/MwmTextAppearance.Body3"
tools:background="#300000F0"
tools:text="Closes in 45 min • at 23:00" />
<com.google.android.material.textview.MaterialTextView
android:id="@+id/tv__address"
android:textAlignment="viewStart"

View File

@@ -14,9 +14,9 @@
<color name="base_accent_pressed_night">#FF588157</color>
<color name="base_accent_transparent">#1EF9B42D</color>
<color name="base_red">#FFF54137</color>
<color name="dark_red">#F51E30</color>
<color name="base_yellow">#FFFFC30A</color>
<color name="base_red">#DE241B</color>
<color name="base_yellow">#E6A100</color>
<color name="base_green">#FF558B2F</color>
<color name="light_green">#7CBC7B</color> <!-- secondary light -->
<color name="driving_options_bg_black">#25282B</color>

View File

@@ -967,4 +967,8 @@
<string name="ruler">Ruler</string>
<string name="bookmark_color">Bookmark color</string>
<string name="about_help">About &amp; Help</string>
<string name="open_now">Open now</string>
<string name="closed_now">Closed now</string>
<!-- Used in place page preview for next open/close time. eg. "closing in 30 min • at 19:30" -->
<string name="at">at %s</string>
</resources>