[Android] Add current opening hours status to placepage preview

And refresh it every 45s

Signed-off-by: Harry Bond <me@hbond.xyz>


Signed-off-by: Harry Bond <me@hbond.xyz>

Signed-off-by: Harry Bond <me@hbond.xyz>

Signed-off-by: Harry Bond <me@hbond.xyz>

Signed-off-by: Harry Bond <me@hbond.xyz>

Signed-off-by: Harry Bond <me@hbond.xyz>

Signed-off-by: Harry Bond <me@hbond.xyz>

Signed-off-by: Harry Bond <me@hbond.xyz>

Signed-off-by: Harry Bond <me@hbond.xyz>
This commit is contained in:
Harry Bond
2025-06-25 23:06:49 +01:00
parent f59fb509c9
commit f7e4fdad6a
6 changed files with 142 additions and 4 deletions

View File

@@ -321,4 +321,30 @@ Java_app_organicmaps_editor_OpeningHours_nativeIsTimetableStringValid(JNIEnv * e
{ {
return OpeningHours(jni::ToNativeString(env, jSource)).IsValid(); 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" } // 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. * @return true if timetable string is valid OSM timetable.
*/ */
public static native boolean nativeIsTimetableStringValid(String source); 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.DownloaderStatusIcon;
import app.organicmaps.downloader.MapManager; import app.organicmaps.downloader.MapManager;
import app.organicmaps.editor.Editor; 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.LocationHelper;
import app.organicmaps.location.LocationListener; import app.organicmaps.location.LocationListener;
import app.organicmaps.location.SensorHelper; import app.organicmaps.location.SensorHelper;
import app.organicmaps.location.SensorListener; import app.organicmaps.location.SensorListener;
import app.organicmaps.routing.RoutingController; import app.organicmaps.routing.RoutingController;
import app.organicmaps.util.DateUtils;
import app.organicmaps.util.SharingUtils; import app.organicmaps.util.SharingUtils;
import app.organicmaps.util.StringUtils; import app.organicmaps.util.StringUtils;
import app.organicmaps.util.UiUtils; 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.button.MaterialButton;
import com.google.android.material.textview.MaterialTextView; 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.ArrayList;
import java.util.Arrays; import java.util.Arrays;
import java.util.List; import java.util.List;
@@ -83,12 +91,15 @@ public class PlacePageView extends Fragment implements View.OnClickListener,
CoordinatesFormat.MGRS, CoordinatesFormat.MGRS,
CoordinatesFormat.OSMLink); CoordinatesFormat.OSMLink);
private View mFrame; private View mFrame;
private Context mContext;
// Preview. // Preview.
private ViewGroup mPreview; private ViewGroup mPreview;
private MaterialToolbar mToolbar; private MaterialToolbar mToolbar;
private MaterialTextView mTvTitle; private MaterialTextView mTvTitle;
private MaterialTextView mTvSecondaryTitle; private MaterialTextView mTvSecondaryTitle;
private MaterialTextView mTvSubtitle; private MaterialTextView mTvSubtitle;
private MaterialTextView mTvOpenState;
private ArrowView mAvDirection; private ArrowView mAvDirection;
private MaterialTextView mTvDistance; private MaterialTextView mTvDistance;
private MaterialTextView mTvAddress; private MaterialTextView mTvAddress;
@@ -201,7 +212,6 @@ public class PlacePageView extends Fragment implements View.OnClickListener,
mFrame = view; mFrame = view;
mFrame.setOnClickListener((v) -> mPlacePageViewListener.onPlacePageRequestToggleState()); mFrame.setOnClickListener((v) -> mPlacePageViewListener.onPlacePageRequestToggleState());
mPreview = mFrame.findViewById(R.id.pp__preview); mPreview = mFrame.findViewById(R.id.pp__preview);
mFrame.addOnLayoutChangeListener((v, left, top, right, bottom, oldLeft, oldTop, oldRight, oldBottom) -> { 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); mTvSecondaryTitle.setOnClickListener(this);
mToolbar = mFrame.findViewById(R.id.toolbar); mToolbar = mFrame.findViewById(R.id.toolbar);
mTvSubtitle = mPreview.findViewById(R.id.tv__subtitle); mTvSubtitle = mPreview.findViewById(R.id.tv__subtitle);
mTvOpenState = mPreview.findViewById(R.id.tv__open_state);
View directionFrame = mPreview.findViewById(R.id.direction_frame); View directionFrame = mPreview.findViewById(R.id.direction_frame);
mTvDistance = mPreview.findViewById(R.id.tv__straight_distance); mTvDistance = mPreview.findViewById(R.id.tv__straight_distance);
@@ -312,6 +323,7 @@ public class PlacePageView extends Fragment implements View.OnClickListener,
mViewModel.getMapObject().removeObserver(this); mViewModel.getMapObject().removeObserver(this);
LocationHelper.from(requireContext()).removeListener(this); LocationHelper.from(requireContext()).removeListener(this);
SensorHelper.from(requireContext()).removeListener(this); SensorHelper.from(requireContext()).removeListener(this);
UiThread.cancelDelayedTasks(updateOpenState);
detachCountry(); detachCountry();
} }
@@ -412,6 +424,8 @@ public class PlacePageView extends Fragment implements View.OnClickListener,
{ {
UiUtils.setTextAndHideIfEmpty(mTvTitle, mMapObject.getTitle()); UiUtils.setTextAndHideIfEmpty(mTvTitle, mMapObject.getTitle());
UiUtils.setTextAndHideIfEmpty(mTvSecondaryTitle, mMapObject.getSecondaryTitle()); UiUtils.setTextAndHideIfEmpty(mTvSecondaryTitle, mMapObject.getSecondaryTitle());
refreshOpenState();
if (mToolbar != null) if (mToolbar != null)
mToolbar.setTitle(mMapObject.getTitle()); mToolbar.setTitle(mMapObject.getTitle());
setTextAndColorizeSubtitle(); setTextAndColorizeSubtitle();
@@ -546,6 +560,62 @@ public class PlacePageView extends Fragment implements View.OnClickListener,
mTvLatlon.setText(latLon); 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() private void addOrganisation()
{ {
((MwmActivity) requireActivity()).showPositionChooserForEditor(true, false); ((MwmActivity) requireActivity()).showPositionChooserForEditor(true, false);
@@ -556,9 +626,6 @@ public class PlacePageView extends Fragment implements View.OnClickListener,
((MwmActivity) requireActivity()).showPositionChooserForEditor(false, true); ((MwmActivity) requireActivity()).showPositionChooserForEditor(false, true);
} }
/// @todo
/// - Why ll__place_editor and ll__place_latlon check if (mMapObject == null)
@Override @Override
public void onClick(View v) public void onClick(View v)
{ {

View File

@@ -71,6 +71,16 @@
android:textAppearance="@style/MwmTextAppearance.Body3" android:textAppearance="@style/MwmTextAppearance.Body3"
tools:background="#300000F0" tools:background="#300000F0"
tools:text="Subtitle, very very very very very very very long" /> 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="Open • Closing in 45 min" />
<com.google.android.material.textview.MaterialTextView <com.google.android.material.textview.MaterialTextView
android:id="@+id/tv__address" android:id="@+id/tv__address"
android:textAlignment="viewStart" android:textAlignment="viewStart"

View File

@@ -967,4 +967,7 @@
<string name="ruler">Ruler</string> <string name="ruler">Ruler</string>
<string name="bookmark_color">Bookmark color</string> <string name="bookmark_color">Bookmark color</string>
<string name="about_help">About &amp; Help</string> <string name="about_help">About &amp; Help</string>
<string name="open_now">Open now</string>
<string name="closed_now">Closed now</string>
<string name="at">at %s</string>
</resources> </resources>