diff --git a/TripKitAndroidUI/schemas/com.skedgo.tripkit.ui.favorites.trips.FavoriteTripsDataBase/1.json b/TripKitAndroidUI/schemas/com.skedgo.tripkit.ui.favorites.trips.FavoriteTripsDataBase/1.json index 9224994a6..b02090fdd 100644 --- a/TripKitAndroidUI/schemas/com.skedgo.tripkit.ui.favorites.trips.FavoriteTripsDataBase/1.json +++ b/TripKitAndroidUI/schemas/com.skedgo.tripkit.ui.favorites.trips.FavoriteTripsDataBase/1.json @@ -2,7 +2,7 @@ "formatVersion": 1, "database": { "version": 1, - "identityHash": "d4ea3ff522f24116edfc1051b31df634", + "identityHash": "9709d743552e75caa23b4587bbdd1140", "entities": [ { "tableName": "favoriteTrips", @@ -47,90 +47,12 @@ }, "indices": [], "foreignKeys": [] - }, - { - "tableName": "waypoints", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`lat` REAL NOT NULL, `lng` REAL NOT NULL, `mode` TEXT, `modeTitle` TEXT, `order` INTEGER NOT NULL, `tripId` TEXT NOT NULL, `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, FOREIGN KEY(`tripId`) REFERENCES `favoriteTrips`(`uuid`) ON UPDATE CASCADE ON DELETE CASCADE )", - "fields": [ - { - "fieldPath": "lat", - "columnName": "lat", - "affinity": "REAL", - "notNull": true - }, - { - "fieldPath": "lng", - "columnName": "lng", - "affinity": "REAL", - "notNull": true - }, - { - "fieldPath": "mode", - "columnName": "mode", - "affinity": "TEXT", - "notNull": false - }, - { - "fieldPath": "modeTitle", - "columnName": "modeTitle", - "affinity": "TEXT", - "notNull": false - }, - { - "fieldPath": "order", - "columnName": "order", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "tripId", - "columnName": "tripId", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "id", - "columnName": "id", - "affinity": "INTEGER", - "notNull": true - } - ], - "primaryKey": { - "autoGenerate": true, - "columnNames": [ - "id" - ] - }, - "indices": [ - { - "name": "index_waypoints_tripId", - "unique": false, - "columnNames": [ - "tripId" - ], - "orders": [], - "createSql": "CREATE INDEX IF NOT EXISTS `index_waypoints_tripId` ON `${TABLE_NAME}` (`tripId`)" - } - ], - "foreignKeys": [ - { - "table": "favoriteTrips", - "onDelete": "CASCADE", - "onUpdate": "CASCADE", - "columns": [ - "tripId" - ], - "referencedColumns": [ - "uuid" - ] - } - ] } ], "views": [], "setupQueries": [ "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)", - "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, 'd4ea3ff522f24116edfc1051b31df634')" + "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '9709d743552e75caa23b4587bbdd1140')" ] } } \ No newline at end of file diff --git a/TripKitAndroidUI/schemas/com.skedgo.tripkit.ui.favorites.v2.data.local.FavoritesDatabase/1.json b/TripKitAndroidUI/schemas/com.skedgo.tripkit.ui.favorites.v2.data.local.FavoritesDatabase/1.json new file mode 100644 index 000000000..7465dd0e6 --- /dev/null +++ b/TripKitAndroidUI/schemas/com.skedgo.tripkit.ui.favorites.v2.data.local.FavoritesDatabase/1.json @@ -0,0 +1,100 @@ +{ + "formatVersion": 1, + "database": { + "version": 1, + "identityHash": "c1298b518303d6c4bc1800fcdeeb8437", + "entities": [ + { + "tableName": "favorites_v2", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`uuid` TEXT NOT NULL, `name` TEXT NOT NULL, `type` TEXT NOT NULL, `order` INTEGER, `region` TEXT, `stopCode` TEXT, `filter` TEXT, `location` TEXT, `start` TEXT, `end` TEXT, `patterns` TEXT, `userId` TEXT, PRIMARY KEY(`uuid`))", + "fields": [ + { + "fieldPath": "uuid", + "columnName": "uuid", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "type", + "columnName": "type", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "order", + "columnName": "order", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "region", + "columnName": "region", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "stopCode", + "columnName": "stopCode", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "filter", + "columnName": "filter", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "location", + "columnName": "location", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "start", + "columnName": "start", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "end", + "columnName": "end", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "patterns", + "columnName": "patterns", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "userId", + "columnName": "userId", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "uuid" + ] + }, + "indices": [], + "foreignKeys": [] + } + ], + "views": [], + "setupQueries": [ + "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)", + "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, 'c1298b518303d6c4bc1800fcdeeb8437')" + ] + } +} \ No newline at end of file diff --git a/TripKitAndroidUI/src/main/java/com/skedgo/tripkit/ui/dialog/GenericListItem.kt b/TripKitAndroidUI/src/main/java/com/skedgo/tripkit/ui/dialog/GenericListItem.kt index 18ce1688e..8e5aab27b 100644 --- a/TripKitAndroidUI/src/main/java/com/skedgo/tripkit/ui/dialog/GenericListItem.kt +++ b/TripKitAndroidUI/src/main/java/com/skedgo/tripkit/ui/dialog/GenericListItem.kt @@ -6,6 +6,9 @@ import com.skedgo.tripkit.booking.quickbooking.Rider import org.joda.time.format.ISODateTimeFormat import java.text.SimpleDateFormat import java.util.Locale +import android.content.Context +import java.util.Date +import com.skedgo.tripkit.ui.utils.SystemTimeFormatManager data class GenericListItem( val label: String, @@ -70,5 +73,26 @@ data class GenericListItem( return emptyList() } + + private fun formatTime(context: Context, timeInMillis: Long): String { + val fromSdf = SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ssZ", Locale.UK) + val toSdfDate = SimpleDateFormat("MMM dd, yyyy", Locale.UK) + // Use SystemTimeFormatManager singleton instead of requiring Context parameter + val timePattern = SystemTimeFormatManager.getTimeFormatPatternWithAmPm() + val toSdfTime = SimpleDateFormat(timePattern, Locale.UK) + val fromDate = Date(timeInMillis) + return "${toSdfDate.format(fromDate)} ${toSdfTime.format(fromDate)}" + } + + private fun formatTimeRange(context: Context, startTimeInMillis: Long, endTimeInMillis: Long): String { + val fromSdf = SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ssZ", Locale.UK) + val toSdfDate = SimpleDateFormat("MMM dd, yyyy", Locale.UK) + // Use SystemTimeFormatManager singleton instead of requiring Context parameter + val timePattern = SystemTimeFormatManager.getTimeFormatPatternWithAmPm() + val toSdfTime = SimpleDateFormat(timePattern, Locale.UK) + val startDate = Date(startTimeInMillis) + val endDate = Date(endTimeInMillis) + return "${toSdfDate.format(startDate)} ${toSdfTime.format(startDate)} - ${toSdfDate.format(endDate)} ${toSdfTime.format(endDate)}" + } } } \ No newline at end of file diff --git a/TripKitAndroidUI/src/main/java/com/skedgo/tripkit/ui/dialog/v2/datetimepicker/TKUIDateTimePickerDialogFragment.kt b/TripKitAndroidUI/src/main/java/com/skedgo/tripkit/ui/dialog/v2/datetimepicker/TKUIDateTimePickerDialogFragment.kt index 48899cb4b..1bb6d282e 100644 --- a/TripKitAndroidUI/src/main/java/com/skedgo/tripkit/ui/dialog/v2/datetimepicker/TKUIDateTimePickerDialogFragment.kt +++ b/TripKitAndroidUI/src/main/java/com/skedgo/tripkit/ui/dialog/v2/datetimepicker/TKUIDateTimePickerDialogFragment.kt @@ -19,6 +19,8 @@ import java.util.Calendar import java.util.Locale import java.util.TimeZone import java.util.concurrent.TimeUnit.MILLISECONDS +import android.text.format.DateFormat +import com.skedgo.tripkit.ui.utils.SystemTimeFormatManager class TKUIDateTimePickerDialogFragment : BaseDialog() { @@ -82,8 +84,8 @@ class TKUIDateTimePickerDialogFragment : BaseDialog if (onInfoWindowClickListener != null) { val poiLocation = marker.tag as IMapPoiLocation? - if (poiLocation != null) { + if (poiLocation != null && isPoiWindowAdapterClickable(poiLocation)) { onInfoWindowClickListener!!.onInfoWindowClick(poiLocation.toLocation()) } } @@ -932,10 +935,14 @@ class TripKitMapFragment : LocationEnhancedMapFragment(), OnInfoWindowClickListe transportModes = it } - poiMarkers?.clear() viewModel.showMarkers.set(show) if (show) { + tripLocationMarkers?.showAll() + poiMarkers?.showAll() loadMarkers() + } else { + tripLocationMarkers?.hideAll() + poiMarkers?.hideAll() } } @@ -947,6 +954,18 @@ class TripKitMapFragment : LocationEnhancedMapFragment(), OnInfoWindowClickListe map?.let { cameraController.moveToPolygonBounds(it, polygon) } } + /** + * Check if a POI location should be clickable based on its type + */ + private fun isPoiWindowAdapterClickable(poiLocation: IMapPoiLocation): Boolean { + return when (poiLocation) { + is StopPOILocation -> false + is CarParkPOILocation -> false + is FacilityPOILocation -> false + else -> true + } + } + companion object { private fun asMarkerIcon(mode: SelectionType): BitmapDescriptor { return if (mode === SelectionType.DEPARTURE) { diff --git a/TripKitAndroidUI/src/main/java/com/skedgo/tripkit/ui/timetables/GetRealtimeText.kt b/TripKitAndroidUI/src/main/java/com/skedgo/tripkit/ui/timetables/GetRealtimeText.kt index b6d4f0bb5..6caf17ae4 100644 --- a/TripKitAndroidUI/src/main/java/com/skedgo/tripkit/ui/timetables/GetRealtimeText.kt +++ b/TripKitAndroidUI/src/main/java/com/skedgo/tripkit/ui/timetables/GetRealtimeText.kt @@ -1,6 +1,7 @@ package com.skedgo.tripkit.ui.timetables import android.content.Context +import android.text.format.DateFormat import com.skedgo.tripkit.common.model.realtimealert.RealTimeStatus import com.skedgo.tripkit.common.util.TimeUtils import com.skedgo.tripkit.datetime.PrintTime @@ -27,7 +28,9 @@ open class GetRealtimeText @Inject constructor( val isRightToLeft = context.resources.getBoolean(R.bool.is_right_to_left) - val dateTimeFormatter = DateTimeFormat.forPattern("HH:mm") + // Use system-aware time format instead of hardcoded "HH:mm" + val timePattern = if (DateFormat.is24HourFormat(context)) "H:mm" else "h:mm a" + val dateTimeFormatter = DateTimeFormat.forPattern(timePattern) val startTime = realTimeDeparture(service, service.realtimeVehicle) val endTime = realTimeArrival(service, service.realtimeVehicle) val startDateTime = DateTime(TimeUnit.SECONDS.toMillis(startTime)) diff --git a/TripKitAndroidUI/src/main/java/com/skedgo/tripkit/ui/timetables/TimetableMapContributor.kt b/TripKitAndroidUI/src/main/java/com/skedgo/tripkit/ui/timetables/TimetableMapContributor.kt index 507fca8fc..7a656e90f 100644 --- a/TripKitAndroidUI/src/main/java/com/skedgo/tripkit/ui/timetables/TimetableMapContributor.kt +++ b/TripKitAndroidUI/src/main/java/com/skedgo/tripkit/ui/timetables/TimetableMapContributor.kt @@ -22,6 +22,7 @@ import com.google.android.gms.maps.model.Marker import com.google.android.gms.maps.model.MarkerOptions import com.google.android.gms.maps.model.Polyline import com.google.android.gms.maps.model.PolylineOptions +import com.skedgo.rxtry.printThrowableStackTrace import com.skedgo.rxtry.subscribeWithErrorHandling import com.skedgo.tripkit.common.model.stop.ScheduledStop import com.skedgo.tripkit.common.model.stop.ServiceStop @@ -155,12 +156,15 @@ class TimetableMapContributor(val fragment: Fragment) : TripKitMapContributor { val marker = map.addMarker(first) stopCodesToMarkerMap[second!!] = marker } + fitAllMapElementsToBounds() } ) - autoDisposable.add(viewModel.viewPort - .subscribeWithErrorHandling { coordinates: List? -> this.centerMapOver(map, coordinates) }) +// autoDisposable.add(viewModel.viewPort +// .subscribeWithErrorHandling { coordinates: List? -> +// this.centerMapOver(map, coordinates) } +// ) autoDisposable.add(viewModel.drawServiceLine .subscribeWithErrorHandling { polylineOptions: List -> @@ -179,10 +183,7 @@ class TimetableMapContributor(val fragment: Fragment) : TripKitMapContributor { } } - val bounds = builder.build() - val padding = 50 // Optional padding around the bounds - val cameraUpdate = CameraUpdateFactory.newLatLngBounds(bounds, padding) - map.animateCamera(cameraUpdate) + fitAllMapElementsToBounds() }) autoDisposable.add(viewModel.realtimeVehicle @@ -192,6 +193,7 @@ class TimetableMapContributor(val fragment: Fragment) : TripKitMapContributor { } else { setRealTimeVehicle(null) // Handle empty OptionalCompat } + fitAllMapElementsToBounds() }) } @@ -419,4 +421,65 @@ class TimetableMapContributor(val fragment: Fragment) : TripKitMapContributor { fun getMapPreviousPosition(): CameraPosition? { return previousCameraPosition } + + private fun fitAllMapElementsToBounds( + paddingPx: Int = 160, + includeRealtimeVehicle: Boolean = true, + singlePointZoom: Float = 16f + ) { + val map = googleMap ?: return + + val builder = LatLngBounds.Builder() + var count = 0 + var firstPoint: LatLng? = null + + // 1) All stop markers + for (marker in stopCodesToMarkerMap.values) { + val p = marker.position + builder.include(p) + if (count == 0) firstPoint = p + count++ + } + + // 2) All polyline points + for (poly in serviceLines) { + for (p in poly.points) { + builder.include(p) + if (count == 0) firstPoint = p + count++ + } + } + + // 3) Realtime vehicle marker + if (includeRealtimeVehicle) { + realTimeVehicleMarker?.position?.let { p -> + builder.include(p) + if (count == 0) firstPoint = p + count++ + } + } + + if (count == 0) return // nothing to show + + try { + if (count == 1) { + // Only one point -> just zoom to it + map.animateCamera(CameraUpdateFactory.newLatLngZoom(firstPoint!!, singlePointZoom)) + } else { + // Multiple points -> fit bounds + val bounds = builder.build() + try { + map.animateCamera(CameraUpdateFactory.newLatLngBounds(bounds, paddingPx)) + } catch (_: IllegalStateException) { + // Fallback if called before map has size; post to the view to retry + fragment.view?.post { + map.animateCamera(CameraUpdateFactory.newLatLngBounds(bounds, paddingPx)) + } + } + } + } catch (e: Exception) { + e.printThrowableStackTrace() + } + } + } \ No newline at end of file diff --git a/TripKitAndroidUI/src/main/java/com/skedgo/tripkit/ui/trip/options/RoutingTimeViewModelMapper.kt b/TripKitAndroidUI/src/main/java/com/skedgo/tripkit/ui/trip/options/RoutingTimeViewModelMapper.kt index 1bc7891e3..91b8fea62 100644 --- a/TripKitAndroidUI/src/main/java/com/skedgo/tripkit/ui/trip/options/RoutingTimeViewModelMapper.kt +++ b/TripKitAndroidUI/src/main/java/com/skedgo/tripkit/ui/trip/options/RoutingTimeViewModelMapper.kt @@ -6,6 +6,7 @@ import com.skedgo.tripkit.ui.trip.ArriveBy import com.skedgo.tripkit.ui.trip.LeaveAfter import com.skedgo.tripkit.ui.trip.Now import com.skedgo.tripkit.ui.trip.RoutingTime +import com.skedgo.tripkit.ui.utils.SystemTimeFormatManager import io.reactivex.Single import org.joda.time.DateTime import java.text.SimpleDateFormat @@ -13,8 +14,6 @@ import java.util.Date import java.util.Locale import javax.inject.Inject -private const val ROUTING_TIME_PATTERN = "MMM dd, h:mm a" - open class RoutingTimeViewModelMapper @Inject internal constructor( private val resources: Resources ) { @@ -27,7 +26,9 @@ open class RoutingTimeViewModelMapper @Inject internal constructor( } private fun DateTime.format(): String { - val simpleDateFormat = SimpleDateFormat(ROUTING_TIME_PATTERN, Locale.US) + val timePattern = SystemTimeFormatManager.getTimeFormatPattern() + val datePattern = "MMM dd, $timePattern" + val simpleDateFormat = SimpleDateFormat(datePattern, Locale.US) simpleDateFormat.timeZone = zone.toTimeZone() return simpleDateFormat.format(Date(millis)) } diff --git a/TripKitAndroidUI/src/main/java/com/skedgo/tripkit/ui/trippreview/TripPreviewPagerItemViewModel.kt b/TripKitAndroidUI/src/main/java/com/skedgo/tripkit/ui/trippreview/TripPreviewPagerItemViewModel.kt index a4f11a8e0..83b31b3aa 100644 --- a/TripKitAndroidUI/src/main/java/com/skedgo/tripkit/ui/trippreview/TripPreviewPagerItemViewModel.kt +++ b/TripKitAndroidUI/src/main/java/com/skedgo/tripkit/ui/trippreview/TripPreviewPagerItemViewModel.kt @@ -27,10 +27,14 @@ import com.skedgo.tripkit.ui.utils.checkDateForStringLabel import io.reactivex.Observable import io.reactivex.android.schedulers.AndroidSchedulers import io.reactivex.schedulers.Schedulers +import org.joda.time.DateTime +import org.joda.time.Days import org.joda.time.DateTimeZone import org.joda.time.format.DateTimeFormat import timber.log.Timber import java.util.concurrent.TimeUnit +import android.text.format.DateFormat +import com.skedgo.tripkit.ui.utils.SystemTimeFormatManager open class TripPreviewPagerItemViewModel : RxViewModel() { var title = ObservableField() @@ -140,11 +144,7 @@ open class TripPreviewPagerItemViewModel : RxViewModel() { fromLocation.set(segment.from?.address ?: "") toLocation.set(segment.to?.address ?: "") - if (!DateUtils.isToday(segment.startTimeInSecs)) { - duration.set(segment.startDateTime.toString(DateTimeFormat.forPattern("MMMM dd HH:mm"))) - } else { - duration.set("Today ${segment.startDateTime.toString(DateTimeFormat.forPattern("HH:mm"))}") - } + updateDuration(segment) requestedPickUp.set("") requestedDropOff.set("") @@ -152,7 +152,9 @@ open class TripPreviewPagerItemViewModel : RxViewModel() { if ((segment.trip?.queryTime ?: 0L) > 0) { val queryDateTime = segment.trip?.queryDateTime val date = queryDateTime?.toString(DateTimeFormat.forPattern("MMM d, yyyy")) - val time = queryDateTime?.toString(DateTimeFormat.forPattern("h:mm aa")) + // Use system-aware time format instead of hardcoded "H:mm" + val timePattern = if (DateFormat.is24HourFormat(context)) "H:mm" else "h:mm a" + val time = queryDateTime?.toString(DateTimeFormat.forPattern(timePattern)) val label = String.format(context.getString(R.string.requested_time), date, time) if (segment.trip?.queryIsLeaveAfter == true) { requestedPickUp.set(label) @@ -165,13 +167,17 @@ open class TripPreviewPagerItemViewModel : RxViewModel() { val labelForStartDate = startDateTime.toDate().checkDateForStringLabel(context) val startDate = labelForStartDate ?: startDateTime.toString(DateTimeFormat.forPattern("MMM d, yyyy")) - val startTime = startDateTime.toString(DateTimeFormat.forPattern("h:mm aa")) + // Use system-aware time format instead of hardcoded "H:mm" + val startTimePattern = if (DateFormat.is24HourFormat(context)) "H:mm" else "h:mm a" + val startTime = startDateTime.toString(DateTimeFormat.forPattern(startTimePattern)) val endDateTime = segment.endDateTime val labelForEndDate = endDateTime.toDate().checkDateForStringLabel(context) val endDate = labelForEndDate ?: endDateTime.toString(DateTimeFormat.forPattern("MMM d, yyyy")) - val endTime = endDateTime.toString(DateTimeFormat.forPattern("h:mm aa")) + // Use system-aware time format instead of hardcoded "H:mm" + val endTimePattern = if (DateFormat.is24HourFormat(context)) "H:mm" else "h:mm a" + val endTime = endDateTime.toString(DateTimeFormat.forPattern(endTimePattern)) requestedPickUp.set("$startDate $startTime") requestedDropOff.set("$endDate $endTime") @@ -186,6 +192,27 @@ open class TripPreviewPagerItemViewModel : RxViewModel() { ) } + private fun updateDuration(segment: TripSegment) { + val now = DateTime.now() + val segmentDate = segment.startDateTime + val daysDiff = Days.daysBetween(now.toLocalDate(), segmentDate.toLocalDate()).days + + when { + daysDiff == 0 -> { + val timePattern = SystemTimeFormatManager.getTimeFormatPattern() + duration.set("Today ${segment.startDateTime.toString(DateTimeFormat.forPattern(timePattern))}") + } + daysDiff == 1 -> { + val timePattern = SystemTimeFormatManager.getTimeFormatPattern() + duration.set("Tomorrow ${segment.startDateTime.toString(DateTimeFormat.forPattern(timePattern))}") + } + else -> { + val timePattern = SystemTimeFormatManager.getTimeFormatPattern() + duration.set(segment.startDateTime.toString(DateTimeFormat.forPattern("MMMM dd $timePattern"))) + } + } + } + private fun fetchRegionAndSetupPickUpMessage(trip: Trip) { TripKitUI.getInstance().regionService().getRegionByLocationAsync(trip.from) .subscribeOn(Schedulers.io()) @@ -210,8 +237,10 @@ open class TripPreviewPagerItemViewModel : RxViewModel() { DateTimeFormat.forPattern("MMM d, yyyy") .withZone(DateTimeZone.forID(timeZone)) ) + + val timePattern = SystemTimeFormatManager.getTimeFormatPatternWithAmPm() val time = dateTime.toString( - DateTimeFormat.forPattern("h:mm aa") + DateTimeFormat.forPattern(timePattern) .withZone(DateTimeZone.forID(timeZone)) ) diff --git a/TripKitAndroidUI/src/main/java/com/skedgo/tripkit/ui/tripresults/TripResultListFragment.kt b/TripKitAndroidUI/src/main/java/com/skedgo/tripkit/ui/tripresults/TripResultListFragment.kt index 0a98eaa78..cc1ef5851 100644 --- a/TripKitAndroidUI/src/main/java/com/skedgo/tripkit/ui/tripresults/TripResultListFragment.kt +++ b/TripKitAndroidUI/src/main/java/com/skedgo/tripkit/ui/tripresults/TripResultListFragment.kt @@ -46,13 +46,28 @@ import timber.log.Timber import java.util.* import java.util.concurrent.TimeUnit import javax.inject.Inject +import android.text.format.DateFormat +import com.skedgo.tripkit.ui.utils.SystemTimeFormatManager class TripResultListFragment : BaseTripKitFragment() { companion object { - const val DATE_TIME_FORMATTER_LEAVE_NOW = "HH:mm aa" - const val DATE_TIME_FORMATTER_LEAVE = "MMMM dd HH:mm aa" + private const val ARG_QUERY = "query" + private const val ARG_ACTION_BUTTON_HANDLER_FACTORY = "action_button_handler_factory" + private const val ARG_SHOW_CLOSE_BUTTON = "show_close_button" + private const val ARG_USER_MODES = "user_modes" + private const val ARG_BOOK_RIDE_HELP_CALLBACK = "book_ride_help_callback" + private const val ARG_TRIP_KIT_MAP_FRAGMENT = "trip_kit_map_fragment" + + // Use SystemTimeFormatManager singleton instead of requiring Context parameter + fun getDateTimeFormatterLeaveNow(): String { + return SystemTimeFormatManager.getTimeFormatPatternWithAmPm() + } + + fun getDateTimeFormatterLeave(): String { + return SystemTimeFormatManager.getDateTimeFormatPatternWithAmPm("MMMM dd") + } } /** @@ -224,9 +239,9 @@ class TripResultListFragment : BaseTripKitFragment() { timeTag?.let { val timezone: String? = region.timezone val dateFormatter = if (it.isLeaveNow) { - DATE_TIME_FORMATTER_LEAVE_NOW + getDateTimeFormatterLeaveNow() } else { - DATE_TIME_FORMATTER_LEAVE + getDateTimeFormatterLeave() } val dt = DateTime(timeTag.timeInMillis, DateTimeZone.forID(timezone)) val formatter = DateTimeFormat.forPattern(dateFormatter) diff --git a/TripKitAndroidUI/src/main/java/com/skedgo/tripkit/ui/utils/DateUtils.kt b/TripKitAndroidUI/src/main/java/com/skedgo/tripkit/ui/utils/DateUtils.kt index 501727de0..dfd94d571 100644 --- a/TripKitAndroidUI/src/main/java/com/skedgo/tripkit/ui/utils/DateUtils.kt +++ b/TripKitAndroidUI/src/main/java/com/skedgo/tripkit/ui/utils/DateUtils.kt @@ -21,11 +21,13 @@ fun getDisplayDateFormatter(tz: DateTimeZone? = null): DateTimeFormatter { } fun getDisplayTimeFormatter(tz: DateTimeZone? = null): DateTimeFormatter { - return DateTimeFormat.forPattern("h:mm aa").withZone(tz) + val timePattern = SystemTimeFormatManager.getTimeFormatPatternWithAmPm() + return DateTimeFormat.forPattern(timePattern).withZone(tz) } fun getDisplayDateTimeFormatter(tz: DateTimeZone? = null): DateTimeFormatter { - return DateTimeFormat.forPattern("MMM dd, yyyy h:mm aa").withZone(tz) + val timePattern = SystemTimeFormatManager.getTimeFormatPatternWithAmPm() + return DateTimeFormat.forPattern("MMM dd, yyyy $timePattern").withZone(tz) } fun Date.checkDateForStringLabel(context: Context): String? { diff --git a/TripKitAndroidUI/src/main/java/com/skedgo/tripkit/ui/utils/SystemTimeFormatManager.kt b/TripKitAndroidUI/src/main/java/com/skedgo/tripkit/ui/utils/SystemTimeFormatManager.kt new file mode 100644 index 000000000..5d8af2e98 --- /dev/null +++ b/TripKitAndroidUI/src/main/java/com/skedgo/tripkit/ui/utils/SystemTimeFormatManager.kt @@ -0,0 +1,69 @@ +package com.skedgo.tripkit.ui.utils + +import android.content.Context +import android.text.format.DateFormat + +/** + * Singleton class to manage system time format detection. + * This eliminates the need to pass Context around to classes that don't have access to it. + */ +object SystemTimeFormatManager { + + private var applicationContext: Context? = null + + /** + * Initialize the manager with the Application context. + * Should be called in Application.onCreate() + */ + fun initialize(context: Context) { + applicationContext = context.applicationContext + } + + /** + * Check if the system is using 24-hour format. + * @return true if 24-hour format is enabled, false for 12-hour format + */ + fun is24HourFormat(): Boolean { + val context = applicationContext + return if (context != null) { + DateFormat.is24HourFormat(context) + } else { + // Default to 24-hour format if not initialized + true + } + } + + /** + * Get the appropriate time format pattern based on system settings. + * @return "H:mm" for 24-hour format, "h:mm a" for 12-hour format + */ + fun getTimeFormatPattern(): String { + return if (is24HourFormat()) "H:mm" else "h:mm a" + } + + /** + * Get the appropriate time format pattern with AM/PM indicator for 12-hour format. + * @return "H:mm" for 24-hour format, "h:mm aa" for 12-hour format + */ + fun getTimeFormatPatternWithAmPm(): String { + return if (is24HourFormat()) "H:mm" else "h:mm aa" + } + + /** + * Get a date-time format pattern with the appropriate time format. + * @param datePattern The date pattern (e.g., "MMM dd, yyyy") + * @return Combined date-time pattern with system-aware time format + */ + fun getDateTimeFormatPattern(datePattern: String): String { + return "$datePattern ${getTimeFormatPattern()}" + } + + /** + * Get a date-time format pattern with AM/PM indicator. + * @param datePattern The date pattern (e.g., "MMM dd, yyyy") + * @return Combined date-time pattern with system-aware time format and AM/PM + */ + fun getDateTimeFormatPatternWithAmPm(datePattern: String): String { + return "$datePattern ${getTimeFormatPatternWithAmPm()}" + } +} diff --git a/TripKitAndroidUI/src/main/java/com/skedgo/tripkit/ui/utils/TimeTagUtils.kt b/TripKitAndroidUI/src/main/java/com/skedgo/tripkit/ui/utils/TimeTagUtils.kt index 1bc7d3c65..2ba3d5984 100644 --- a/TripKitAndroidUI/src/main/java/com/skedgo/tripkit/ui/utils/TimeTagUtils.kt +++ b/TripKitAndroidUI/src/main/java/com/skedgo/tripkit/ui/utils/TimeTagUtils.kt @@ -27,7 +27,9 @@ fun TimeTag.formatString(context: Context, timezone: String?): String { stringBuilder.append(prefix) stringBuilder.append(" ") val date = Date(millis) - val dateFormat = SimpleDateFormat("MMM dd, h:mm a", Locale.US) + + val timePattern = SystemTimeFormatManager.getTimeFormatPattern() + val dateFormat = SimpleDateFormat("MMM dd, $timePattern", Locale.US) dateFormat.timeZone = if (timezone != null) { TimeZone.getTimeZone(timezone) } else { diff --git a/TripKitAndroidUI/src/main/java/com/skedgo/tripkit/ui/utils/TripSegmentUIExtension.kt b/TripKitAndroidUI/src/main/java/com/skedgo/tripkit/ui/utils/TripSegmentUIExtension.kt index 4fca43ca5..65f99333e 100644 --- a/TripKitAndroidUI/src/main/java/com/skedgo/tripkit/ui/utils/TripSegmentUIExtension.kt +++ b/TripKitAndroidUI/src/main/java/com/skedgo/tripkit/ui/utils/TripSegmentUIExtension.kt @@ -24,6 +24,8 @@ import io.reactivex.Observable import io.reactivex.android.schedulers.AndroidSchedulers import org.joda.time.format.DateTimeFormat import timber.log.Timber +import android.text.format.DateFormat +import com.skedgo.tripkit.ui.utils.SystemTimeFormatManager fun TripSegment.getSegmentIconObservable( @@ -100,7 +102,8 @@ private fun TripSegment.shouldAttachAlertIconToSubtitle(): Boolean { } fun TripSegment.generateTripPreviewHeader(icon: Drawable): TripSegmentSummary { - val dateTimeFormatter = DateTimeFormat.forPattern("hh:mm a") + val timePattern = SystemTimeFormatManager.getTimeFormatPattern() + val dateTimeFormatter = DateTimeFormat.forPattern(timePattern) return TripSegmentSummary( id = this.segmentId, title = this.getTitle(), @@ -121,7 +124,8 @@ fun TripSegment.generateTripPreviewHeader( icon: Drawable, printTime: PrintTime ): TripSegmentSummary { - val dateTimeFormatter = DateTimeFormat.forPattern("hh:mm a") + val timePattern = SystemTimeFormatManager.getTimeFormatPattern() + val dateTimeFormatter = DateTimeFormat.forPattern(timePattern) return TripSegmentSummary( id = this.segmentId, title = this.getTitle(), @@ -200,4 +204,16 @@ private fun TripSegment.getTitle(): String { "" } } +} + +private fun TripSegment.getTimeText(context: Context): String { + val timePattern = SystemTimeFormatManager.getTimeFormatPattern() + val dateTimeFormatter = DateTimeFormat.forPattern(timePattern) + return startDateTime.toString(dateTimeFormatter) +} + +private fun TripSegment.getTimeRangeText(context: Context): String { + val timePattern = SystemTimeFormatManager.getTimeFormatPattern() + val dateTimeFormatter = DateTimeFormat.forPattern(timePattern) + return "${startDateTime.toString(dateTimeFormatter)} - ${endDateTime.toString(dateTimeFormatter)}" } \ No newline at end of file diff --git a/TripKitAndroidUI/src/main/res/values/colors.xml b/TripKitAndroidUI/src/main/res/values/colors.xml index a218a0304..ac51aae77 100644 --- a/TripKitAndroidUI/src/main/res/values/colors.xml +++ b/TripKitAndroidUI/src/main/res/values/colors.xml @@ -30,7 +30,7 @@ #1880e7 #3467CE - #303467CE + #703467CE #23B15E #6665B2 #9A579D diff --git a/TripKitAndroidUI/src/test/java/com/skedgo/tripkit/ui/timetables/GetRealtimeTextTest.kt b/TripKitAndroidUI/src/test/java/com/skedgo/tripkit/ui/timetables/GetRealtimeTextTest.kt index 747e83b06..7e4221abc 100644 --- a/TripKitAndroidUI/src/test/java/com/skedgo/tripkit/ui/timetables/GetRealtimeTextTest.kt +++ b/TripKitAndroidUI/src/test/java/com/skedgo/tripkit/ui/timetables/GetRealtimeTextTest.kt @@ -2,6 +2,7 @@ package com.skedgo.tripkit.ui.timetables import android.content.Context import android.content.res.Resources +import android.text.format.DateFormat import com.skedgo.tripkit.common.model.realtimealert.RealTimeStatus import com.skedgo.tripkit.datetime.PrintTime import com.skedgo.tripkit.routing.RealTimeVehicle @@ -9,9 +10,12 @@ import com.skedgo.tripkit.ui.R import com.skedgo.tripkit.ui.model.TimetableEntry import io.mockk.every import io.mockk.mockk +import io.mockk.mockkStatic +import io.mockk.unmockkStatic import org.joda.time.DateTime import org.joda.time.DateTimeZone import org.joda.time.tz.UTCProvider +import org.junit.After import org.junit.Assert.assertEquals import org.junit.Before import org.junit.BeforeClass @@ -35,11 +39,18 @@ class GetRealtimeTextTest { @Before fun setUp() { + mockkStatic(DateFormat::class) + every { DateFormat.is24HourFormat(any()) } returns true every { context.resources } returns resources every { resources.getBoolean(R.bool.is_right_to_left) } returns false // Mock getBoolean() getRealtimeText = GetRealtimeText(context, printTime) } + @After + fun tearDown() { + unmockkStatic(DateFormat::class) + } + @Test fun `should return scheduled time if real-time status is null`() { val service: TimetableEntry = mockk { diff --git a/TripKitAndroidUI/src/test/java/com/skedgo/tripkit/ui/trip/options/RoutingTimeViewModelMapperTest.kt b/TripKitAndroidUI/src/test/java/com/skedgo/tripkit/ui/trip/options/RoutingTimeViewModelMapperTest.kt index 6a51339aa..11840cd78 100644 --- a/TripKitAndroidUI/src/test/java/com/skedgo/tripkit/ui/trip/options/RoutingTimeViewModelMapperTest.kt +++ b/TripKitAndroidUI/src/test/java/com/skedgo/tripkit/ui/trip/options/RoutingTimeViewModelMapperTest.kt @@ -1,16 +1,24 @@ package com.skedgo.tripkit.ui.trip.options import android.content.res.Resources +import android.text.format.DateFormat import com.skedgo.tripkit.ui.R import com.skedgo.tripkit.ui.trip.ArriveBy import com.skedgo.tripkit.ui.trip.LeaveAfter import com.skedgo.tripkit.ui.trip.Now +import com.skedgo.tripkit.ui.utils.SystemTimeFormatManager import io.mockk.every import io.mockk.mockk +import io.mockk.mockkObject +import io.mockk.mockkStatic +import io.mockk.unmockkObject +import io.mockk.unmockkStatic import io.reactivex.Single import io.reactivex.observers.TestObserver +import org.amshove.kluent.internal.assertEquals import org.joda.time.DateTime import org.joda.time.DateTimeZone +import org.junit.After import org.junit.Before import org.junit.Test import java.text.SimpleDateFormat @@ -23,8 +31,22 @@ class RoutingTimeViewModelMapperTest { @Before fun setUp() { + // Mock SystemTimeFormatManager + mockkObject(SystemTimeFormatManager) + every { SystemTimeFormatManager.getTimeFormatPattern() } returns "h:mm a" + + // Mock DateFormat.is24HourFormat for SystemTimeFormatManager internal usage + mockkStatic(DateFormat::class) + every { DateFormat.is24HourFormat(any()) } returns false + mapper = RoutingTimeViewModelMapper(resources) } + + @After + fun tearDown() { + unmockkObject(SystemTimeFormatManager) + unmockkStatic(DateFormat::class) + } @Test fun `toText returns correct text for Now`() { @@ -60,9 +82,25 @@ class RoutingTimeViewModelMapperTest { testObserver.assertValue("Arrive $formattedTime") } + @Test + fun `should format leave after time correctly`() { + val dateTime = DateTime(2023, 1, 1, 14, 30, DateTimeZone.UTC) + val leaveAfter = LeaveAfter(dateTime) + + // Mock the string resource for "Leave" + every { resources.getString(R.string.leave) } returns "Leave" + + val result = mapper.toText(leaveAfter).blockingGet() + + assertEquals("Leave Jan 01, 2:30 PM", result) + } + // Helper function to match the ViewModel's formatting private fun DateTime.format(): String { - val simpleDateFormat = SimpleDateFormat("MMM dd, h:mm a", Locale.US) + // Use SystemTimeFormatManager singleton instead of requiring Context parameter + val timePattern = SystemTimeFormatManager.getTimeFormatPattern() + val datePattern = "MMM dd, $timePattern" + val simpleDateFormat = SimpleDateFormat(datePattern, Locale.US) simpleDateFormat.timeZone = zone.toTimeZone() return simpleDateFormat.format(Date(millis)) } diff --git a/TripKitAndroidUI/src/test/java/com/skedgo/tripkit/ui/trippreview/TripPreviewPagerItemViewModelTest.kt b/TripKitAndroidUI/src/test/java/com/skedgo/tripkit/ui/trippreview/TripPreviewPagerItemViewModelTest.kt index f58ae50fb..396108d19 100644 --- a/TripKitAndroidUI/src/test/java/com/skedgo/tripkit/ui/trippreview/TripPreviewPagerItemViewModelTest.kt +++ b/TripKitAndroidUI/src/test/java/com/skedgo/tripkit/ui/trippreview/TripPreviewPagerItemViewModelTest.kt @@ -3,6 +3,7 @@ package com.skedgo.tripkit.ui.trippreview import android.content.Context import android.content.res.Resources import android.text.TextUtils +import android.text.format.DateFormat import androidx.arch.core.executor.testing.InstantTaskExecutorRule import androidx.core.content.ContextCompat import com.skedgo.tripkit.common.model.location.Location @@ -13,6 +14,7 @@ import com.skedgo.tripkit.routing.TripSegment import com.skedgo.tripkit.routing.endDateTime import com.skedgo.tripkit.routing.startDateTime import com.skedgo.tripkit.ui.base.MockKTest +import com.skedgo.tripkit.ui.utils.SystemTimeFormatManager import io.mockk.* import org.amshove.kluent.internal.assertEquals import org.joda.time.DateTime @@ -63,6 +65,13 @@ class TripPreviewPagerItemViewModelTest: MockKTest() { mockkStatic(TextUtils::class) every { TextUtils.isEmpty(any()) } answers { false } + mockkStatic(DateFormat::class) + every { DateFormat.is24HourFormat(any()) } returns false + + mockkObject(SystemTimeFormatManager) + every { SystemTimeFormatManager.getTimeFormatPattern() } returns "h:mm a" + every { SystemTimeFormatManager.getTimeFormatPatternWithAmPm() } returns "h:mm a" + mockkStatic("android.text.format.DateUtils") every { @@ -75,6 +84,8 @@ class TripPreviewPagerItemViewModelTest: MockKTest() { @After fun tearDown() { tearDownRx() + unmockkObject(SystemTimeFormatManager) + unmockkStatic(DateFormat::class) unmockkAll() } diff --git a/TripKitAndroidUI/src/test/java/com/skedgo/tripkit/ui/trippreview/external/ExternalActionTripPreviewItemViewModelTest.kt b/TripKitAndroidUI/src/test/java/com/skedgo/tripkit/ui/trippreview/external/ExternalActionTripPreviewItemViewModelTest.kt index 69d889009..4cb36c593 100644 --- a/TripKitAndroidUI/src/test/java/com/skedgo/tripkit/ui/trippreview/external/ExternalActionTripPreviewItemViewModelTest.kt +++ b/TripKitAndroidUI/src/test/java/com/skedgo/tripkit/ui/trippreview/external/ExternalActionTripPreviewItemViewModelTest.kt @@ -1,17 +1,23 @@ package com.skedgo.tripkit.ui.trippreview.external import android.content.Context +import android.text.format.DateFormat import android.webkit.URLUtil import androidx.arch.core.executor.testing.InstantTaskExecutorRule import com.skedgo.tripkit.common.model.booking.Booking import com.skedgo.tripkit.routing.TripSegment import com.skedgo.tripkit.ui.R import com.skedgo.tripkit.ui.trippreview.handleExternalAction +import com.skedgo.tripkit.ui.utils.SystemTimeFormatManager import io.mockk.every import io.mockk.mockk +import io.mockk.mockkObject import io.mockk.mockkStatic import io.mockk.slot +import io.mockk.unmockkObject +import io.mockk.unmockkStatic import org.joda.time.DateTimeZone +import org.junit.After import org.junit.Assert.assertEquals import org.junit.Before import org.junit.Rule @@ -41,10 +47,21 @@ class ExternalActionTripPreviewItemViewModelTest { booking = mockBooking } + // Mock static classes mockkStatic(URLUtil::class) every { URLUtil.isNetworkUrl(any()) } returns true + mockkStatic(DateTimeZone::class) every { DateTimeZone.forID(any()) } answers { DateTimeZone.UTC } + + // Mock DateFormat.is24HourFormat to prevent NullPointerException + mockkStatic(DateFormat::class) + every { DateFormat.is24HourFormat(any()) } returns false + + // Mock SystemTimeFormatManager + mockkObject(SystemTimeFormatManager) + every { SystemTimeFormatManager.getTimeFormatPattern() } returns "h:mm a" + every { SystemTimeFormatManager.getTimeFormatPatternWithAmPm() } returns "h:mm a" viewModel = ExternalActionTripPreviewItemViewModel() @@ -61,6 +78,14 @@ class ExternalActionTripPreviewItemViewModelTest { every { mockContext.handleExternalAction(any()) } returns mockk(relaxed = true) } + @After + fun tearDown() { + unmockkStatic(URLUtil::class) + unmockkStatic(DateTimeZone::class) + unmockkStatic(DateFormat::class) + unmockkObject(SystemTimeFormatManager) + } + @Test fun `setSegment correctly populates items`() { every { mockSegment.timeZone } returns "Australia/Sydney"