From abdb78178fd1a13c2f6984e536d85fc7f0a6013b Mon Sep 17 00:00:00 2001 From: Guillaume BOEHM Date: Sat, 26 Apr 2025 19:10:18 +0200 Subject: [PATCH 1/3] Colissimo: Add the french Colissimo delivery service --- .../dev/itsvic/parceltracker/MainActivity.kt | 13 ++ .../api/ColissimoDeliveryService.kt | 135 ++++++++++++++++++ .../java/dev/itsvic/parceltracker/api/Core.kt | 3 + app/src/main/res/values/strings.xml | 2 + 4 files changed, 153 insertions(+) create mode 100644 app/src/main/java/dev/itsvic/parceltracker/api/ColissimoDeliveryService.kt diff --git a/app/src/main/java/dev/itsvic/parceltracker/MainActivity.kt b/app/src/main/java/dev/itsvic/parceltracker/MainActivity.kt index 72b5eef..2897478 100644 --- a/app/src/main/java/dev/itsvic/parceltracker/MainActivity.kt +++ b/app/src/main/java/dev/itsvic/parceltracker/MainActivity.kt @@ -46,6 +46,7 @@ import androidx.navigation.toRoute import dev.itsvic.parceltracker.api.APIKeyMissingException import dev.itsvic.parceltracker.api.ParcelHistoryItem import dev.itsvic.parceltracker.api.ParcelNonExistentException +import dev.itsvic.parceltracker.api.UnsupportedResponseException import dev.itsvic.parceltracker.api.Status import dev.itsvic.parceltracker.api.getParcel import dev.itsvic.parceltracker.db.Parcel @@ -281,6 +282,18 @@ fun ParcelAppNavigation(parcelToOpen: Int) { ), Status.NetworkFailure ) + } catch (_: UnsupportedResponseException) { + apiParcel = APIParcel( + dbParcel.parcelId, + listOf( + ParcelHistoryItem( + context.getString(R.string.error_bad_response), + LocalDateTime.now(), + "" + ) + ), + Status.NoData + ) } } } diff --git a/app/src/main/java/dev/itsvic/parceltracker/api/ColissimoDeliveryService.kt b/app/src/main/java/dev/itsvic/parceltracker/api/ColissimoDeliveryService.kt new file mode 100644 index 0000000..cce6c38 --- /dev/null +++ b/app/src/main/java/dev/itsvic/parceltracker/api/ColissimoDeliveryService.kt @@ -0,0 +1,135 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +package dev.itsvic.parceltracker.api + +import android.content.Context +import android.os.LocaleList +import android.util.Log + +import com.squareup.moshi.JsonClass +import com.squareup.moshi.JsonDataException + +import dev.itsvic.parceltracker.R + +import retrofit2.HttpException +import retrofit2.Retrofit +import retrofit2.http.GET +import retrofit2.http.Path +import retrofit2.http.Query + +import java.time.LocalDateTime +import java.time.format.DateTimeFormatter + +object ColissimoDeliveryService : DeliveryService { + override val nameResource: Int = R.string.service_colissimo + override val acceptsPostCode: Boolean = false + override val requiresPostCode: Boolean = false + override val requiresApiKey: Boolean = false + + override fun acceptsFormat(trackingId: String): Boolean { + val parcelFormat = """^[A-Za-z0-9]{11,15}$""".toRegex() + return parcelFormat.accepts(trackingId) + } + + override suspend fun getParcel( + context: Context, + trackingId: String, + postalCode: String? + ): Parcel { + val locale = LocaleList.getDefault().get(0) + + val resp = try { + service.getShipments(trackingId, locale.language) + } catch (_: HttpException) { + try { + service.getShipments(trackingId, "en") // Retry once with fallback to english + } + catch(_: HttpException) { + throw ParcelNonExistentException() + } + } + catch (e: JsonDataException) { + // Something's wrong with the parsing + Log.d("Colissimo", e.toString()) + throw UnsupportedResponseException() + } + + if(resp.isEmpty()) throw ParcelNonExistentException() + val shipment = resp[0].shipment + + var status = Status.Unknown + val events = shipment.event.sortedByDescending { it.order } + + if(events.isNotEmpty()) { + val lastEvent = events.first() + status = when (lastEvent.code) { + "DR1" -> Status.Preadvice // Delivery declaration received + "DR2" -> Status.DeliveryFailure // Issue during preparation + "PC1" -> Status.InWarehouse // Handled + "PC2" -> Status.InWarehouse // Handled in the expediting country + "ET1" -> Status.InTransit // Processing + "ET2" -> Status.InTransit // Processing in the expediting country + "ET3" -> Status.InTransit // Processing in the destination country + "ET4" -> Status.InTransit // Processing in a transit country + "EP1" -> Status.Unknown // Waiting presentation + "DO1" -> Status.Customs // Entered customs + "DO2" -> Status.Customs // Out of customs + "DO3" -> Status.Customs // Held in customs + "PB1" -> Status.Unknown // Issue in progress + "PB2" -> Status.Unknown // Issue resolved + "MD2" -> Status.OutForDelivery // Distributing + "ND1" -> Status.DeliveryFailure // Impossible to distribute + "AG1" -> Status.AwaitingPickup // Awaiting pickup at counter + "RE1" -> Status.DeliveryFailure // Returned to the expeditor + "DI0" -> Status.Delivered // Distributed in lot + "DI1" -> Status.Delivered // Distributed + "DI2" -> Status.DeliveryFailure // Distributed to the expeditor (If sent back I suppose) + "DI3" -> Status.OutForDelivery // Delayed (This probably means still distributing) + "ID0" -> Status.Customs // Customs information + else -> logUnknownStatus("Colissimo", lastEvent.code) + } + } + + val history = events.map { + ParcelHistoryItem( + it.label, + LocalDateTime.parse(it.date, DateTimeFormatter.ISO_DATE_TIME), + it.country + ) + } + + return Parcel(shipment.idShip, history, status) + } + + private val retrofit = Retrofit.Builder().baseUrl("https://www.laposte.fr/ssu/sun/back/suivi-unifie/").client(api_client) .addConverterFactory(api_factory).build() + private val service = retrofit.create(API::class.java) + + private interface API { + @GET("{id}") + suspend fun getShipments( + @Path("id") trackingId: String, + @Query("lang") lang: String = "en_GB" + ): List + } + + @JsonClass(generateAdapter = true) + internal data class ShipmentResponse( + val shipment: Shipment, + ) + + @JsonClass(generateAdapter = true) + internal data class Shipment( + val idShip: String, + val event: List, + ) + + @JsonClass(generateAdapter = true) + internal data class Event( + val code: String, + val type: String, + val group: String, + val label: String, + val date: String, + val country: String, + val order: Int + ) +} diff --git a/app/src/main/java/dev/itsvic/parceltracker/api/Core.kt b/app/src/main/java/dev/itsvic/parceltracker/api/Core.kt index b63a44d..bc341ab 100644 --- a/app/src/main/java/dev/itsvic/parceltracker/api/Core.kt +++ b/app/src/main/java/dev/itsvic/parceltracker/api/Core.kt @@ -44,6 +44,7 @@ enum class Service { SAMEDAY_RO, UKRPOSHTA, POSTNORD, + COLISSIMO, // Asia EKART, @@ -78,6 +79,7 @@ fun getDeliveryService(service: Service): DeliveryService? { Service.SAMEDAY_RO -> SamedayRomaniaDeliveryService Service.UKRPOSHTA -> UkrposhtaDeliveryService Service.POSTNORD -> PostNordDeliveryService + Service.COLISSIMO -> ColissimoDeliveryService Service.EKART -> EKartDeliveryService Service.SPX_TH -> SPXThailandDeliveryService @@ -165,6 +167,7 @@ interface DeliveryService { } class ParcelNonExistentException : Exception("Parcel does not exist in delivery service API") +class UnsupportedResponseException : Exception("Response is not formatted as excpected") class APIKeyMissingException : Exception("Delivery service requires an API key but none is present") internal fun logUnknownStatus(service: String, data: String): Status { diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 2400c90..90b0db2 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -19,6 +19,7 @@ Polish Post Belpost An Post + Colissimo Add a parcel Go back @@ -80,6 +81,7 @@ eKart Nova Post This delivery service requires an API key, but none was provided. Check the app\'s settings for more information. + There seems to be a problem when trying to retrieve information for this parcel. DHL\'s API Developer Portal and signing up for the \"Shipment Tracking - Unified\" API.]]> API keys Ukrposhta From a48d76b054c40312c6a2711f29c5355513753acf Mon Sep 17 00:00:00 2001 From: Guillaume BOEHM Date: Mon, 9 Jun 2025 09:48:15 +0200 Subject: [PATCH 2/3] Colissimo: MR fixes --- .../java/dev/itsvic/parceltracker/MainActivity.kt | 13 ------------- .../parceltracker/api/ColissimoDeliveryService.kt | 11 +++-------- .../main/java/dev/itsvic/parceltracker/api/Core.kt | 1 - app/src/main/res/values/strings.xml | 1 - 4 files changed, 3 insertions(+), 23 deletions(-) diff --git a/app/src/main/java/dev/itsvic/parceltracker/MainActivity.kt b/app/src/main/java/dev/itsvic/parceltracker/MainActivity.kt index 2897478..72b5eef 100644 --- a/app/src/main/java/dev/itsvic/parceltracker/MainActivity.kt +++ b/app/src/main/java/dev/itsvic/parceltracker/MainActivity.kt @@ -46,7 +46,6 @@ import androidx.navigation.toRoute import dev.itsvic.parceltracker.api.APIKeyMissingException import dev.itsvic.parceltracker.api.ParcelHistoryItem import dev.itsvic.parceltracker.api.ParcelNonExistentException -import dev.itsvic.parceltracker.api.UnsupportedResponseException import dev.itsvic.parceltracker.api.Status import dev.itsvic.parceltracker.api.getParcel import dev.itsvic.parceltracker.db.Parcel @@ -282,18 +281,6 @@ fun ParcelAppNavigation(parcelToOpen: Int) { ), Status.NetworkFailure ) - } catch (_: UnsupportedResponseException) { - apiParcel = APIParcel( - dbParcel.parcelId, - listOf( - ParcelHistoryItem( - context.getString(R.string.error_bad_response), - LocalDateTime.now(), - "" - ) - ), - Status.NoData - ) } } } diff --git a/app/src/main/java/dev/itsvic/parceltracker/api/ColissimoDeliveryService.kt b/app/src/main/java/dev/itsvic/parceltracker/api/ColissimoDeliveryService.kt index cce6c38..65da255 100644 --- a/app/src/main/java/dev/itsvic/parceltracker/api/ColissimoDeliveryService.kt +++ b/app/src/main/java/dev/itsvic/parceltracker/api/ColissimoDeliveryService.kt @@ -47,11 +47,6 @@ object ColissimoDeliveryService : DeliveryService { throw ParcelNonExistentException() } } - catch (e: JsonDataException) { - // Something's wrong with the parsing - Log.d("Colissimo", e.toString()) - throw UnsupportedResponseException() - } if(resp.isEmpty()) throw ParcelNonExistentException() val shipment = resp[0].shipment @@ -70,12 +65,9 @@ object ColissimoDeliveryService : DeliveryService { "ET2" -> Status.InTransit // Processing in the expediting country "ET3" -> Status.InTransit // Processing in the destination country "ET4" -> Status.InTransit // Processing in a transit country - "EP1" -> Status.Unknown // Waiting presentation "DO1" -> Status.Customs // Entered customs "DO2" -> Status.Customs // Out of customs "DO3" -> Status.Customs // Held in customs - "PB1" -> Status.Unknown // Issue in progress - "PB2" -> Status.Unknown // Issue resolved "MD2" -> Status.OutForDelivery // Distributing "ND1" -> Status.DeliveryFailure // Impossible to distribute "AG1" -> Status.AwaitingPickup // Awaiting pickup at counter @@ -85,6 +77,9 @@ object ColissimoDeliveryService : DeliveryService { "DI2" -> Status.DeliveryFailure // Distributed to the expeditor (If sent back I suppose) "DI3" -> Status.OutForDelivery // Delayed (This probably means still distributing) "ID0" -> Status.Customs // Customs information + // EP1 (Waiting presentation), PB1 (Issue in progress), PB2 (Issue resolved) + // Ignored as unknown statuses, these errors are mid process statuses so + // it shouldn't be a big deal else -> logUnknownStatus("Colissimo", lastEvent.code) } } diff --git a/app/src/main/java/dev/itsvic/parceltracker/api/Core.kt b/app/src/main/java/dev/itsvic/parceltracker/api/Core.kt index bc341ab..94cd2a2 100644 --- a/app/src/main/java/dev/itsvic/parceltracker/api/Core.kt +++ b/app/src/main/java/dev/itsvic/parceltracker/api/Core.kt @@ -167,7 +167,6 @@ interface DeliveryService { } class ParcelNonExistentException : Exception("Parcel does not exist in delivery service API") -class UnsupportedResponseException : Exception("Response is not formatted as excpected") class APIKeyMissingException : Exception("Delivery service requires an API key but none is present") internal fun logUnknownStatus(service: String, data: String): Status { diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 90b0db2..32a1f40 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -81,7 +81,6 @@ eKart Nova Post This delivery service requires an API key, but none was provided. Check the app\'s settings for more information. - There seems to be a problem when trying to retrieve information for this parcel. DHL\'s API Developer Portal and signing up for the \"Shipment Tracking - Unified\" API.]]> API keys Ukrposhta From ed7752c0dce4cceb27e7822adc64b436b464a198 Mon Sep 17 00:00:00 2001 From: Guillaume BOEHM Date: Mon, 9 Jun 2025 10:49:06 +0200 Subject: [PATCH 3/3] Colissimo: Format kotlin --- .../api/ColissimoDeliveryService.kt | 216 +++++++++--------- 1 file changed, 107 insertions(+), 109 deletions(-) diff --git a/app/src/main/java/dev/itsvic/parceltracker/api/ColissimoDeliveryService.kt b/app/src/main/java/dev/itsvic/parceltracker/api/ColissimoDeliveryService.kt index 65da255..7de48a0 100644 --- a/app/src/main/java/dev/itsvic/parceltracker/api/ColissimoDeliveryService.kt +++ b/app/src/main/java/dev/itsvic/parceltracker/api/ColissimoDeliveryService.kt @@ -3,128 +3,126 @@ package dev.itsvic.parceltracker.api import android.content.Context import android.os.LocaleList -import android.util.Log - import com.squareup.moshi.JsonClass -import com.squareup.moshi.JsonDataException - import dev.itsvic.parceltracker.R - +import java.time.LocalDateTime +import java.time.format.DateTimeFormatter import retrofit2.HttpException import retrofit2.Retrofit import retrofit2.http.GET import retrofit2.http.Path import retrofit2.http.Query -import java.time.LocalDateTime -import java.time.format.DateTimeFormatter - object ColissimoDeliveryService : DeliveryService { - override val nameResource: Int = R.string.service_colissimo - override val acceptsPostCode: Boolean = false - override val requiresPostCode: Boolean = false - override val requiresApiKey: Boolean = false - - override fun acceptsFormat(trackingId: String): Boolean { - val parcelFormat = """^[A-Za-z0-9]{11,15}$""".toRegex() - return parcelFormat.accepts(trackingId) - } - - override suspend fun getParcel( - context: Context, - trackingId: String, - postalCode: String? - ): Parcel { - val locale = LocaleList.getDefault().get(0) - - val resp = try { - service.getShipments(trackingId, locale.language) + override val nameResource: Int = R.string.service_colissimo + override val acceptsPostCode: Boolean = false + override val requiresPostCode: Boolean = false + override val requiresApiKey: Boolean = false + + override fun acceptsFormat(trackingId: String): Boolean { + val parcelFormat = """^[A-Za-z0-9]{11,15}$""".toRegex() + return parcelFormat.accepts(trackingId) + } + + override suspend fun getParcel( + context: Context, + trackingId: String, + postalCode: String? + ): Parcel { + val locale = LocaleList.getDefault().get(0) + + val resp = + try { + service.getShipments(trackingId, locale.language) } catch (_: HttpException) { - try { - service.getShipments(trackingId, "en") // Retry once with fallback to english - } - catch(_: HttpException) { - throw ParcelNonExistentException() - } + try { + service.getShipments(trackingId, "en") // Retry once with fallback to english + } catch (_: HttpException) { + throw ParcelNonExistentException() + } } - if(resp.isEmpty()) throw ParcelNonExistentException() - val shipment = resp[0].shipment - - var status = Status.Unknown - val events = shipment.event.sortedByDescending { it.order } - - if(events.isNotEmpty()) { - val lastEvent = events.first() - status = when (lastEvent.code) { - "DR1" -> Status.Preadvice // Delivery declaration received - "DR2" -> Status.DeliveryFailure // Issue during preparation - "PC1" -> Status.InWarehouse // Handled - "PC2" -> Status.InWarehouse // Handled in the expediting country - "ET1" -> Status.InTransit // Processing - "ET2" -> Status.InTransit // Processing in the expediting country - "ET3" -> Status.InTransit // Processing in the destination country - "ET4" -> Status.InTransit // Processing in a transit country - "DO1" -> Status.Customs // Entered customs - "DO2" -> Status.Customs // Out of customs - "DO3" -> Status.Customs // Held in customs - "MD2" -> Status.OutForDelivery // Distributing - "ND1" -> Status.DeliveryFailure // Impossible to distribute - "AG1" -> Status.AwaitingPickup // Awaiting pickup at counter - "RE1" -> Status.DeliveryFailure // Returned to the expeditor - "DI0" -> Status.Delivered // Distributed in lot - "DI1" -> Status.Delivered // Distributed - "DI2" -> Status.DeliveryFailure // Distributed to the expeditor (If sent back I suppose) - "DI3" -> Status.OutForDelivery // Delayed (This probably means still distributing) - "ID0" -> Status.Customs // Customs information - // EP1 (Waiting presentation), PB1 (Issue in progress), PB2 (Issue resolved) - // Ignored as unknown statuses, these errors are mid process statuses so - // it shouldn't be a big deal - else -> logUnknownStatus("Colissimo", lastEvent.code) - } - } - - val history = events.map { - ParcelHistoryItem( - it.label, - LocalDateTime.parse(it.date, DateTimeFormatter.ISO_DATE_TIME), - it.country - ) - } - - return Parcel(shipment.idShip, history, status) + if (resp.isEmpty()) throw ParcelNonExistentException() + val shipment = resp[0].shipment + + var status = Status.Unknown + val events = shipment.event.sortedByDescending { it.order } + + if (events.isNotEmpty()) { + val lastEvent = events.first() + status = + when (lastEvent.code) { + "DR1" -> Status.Preadvice // Delivery declaration received + "DR2" -> Status.DeliveryFailure // Issue during preparation + "PC1" -> Status.InWarehouse // Handled + "PC2" -> Status.InWarehouse // Handled in the expediting country + "ET1" -> Status.InTransit // Processing + "ET2" -> Status.InTransit // Processing in the expediting country + "ET3" -> Status.InTransit // Processing in the destination country + "ET4" -> Status.InTransit // Processing in a transit country + "DO1" -> Status.Customs // Entered customs + "DO2" -> Status.Customs // Out of customs + "DO3" -> Status.Customs // Held in customs + "MD2" -> Status.OutForDelivery // Distributing + "ND1" -> Status.DeliveryFailure // Impossible to distribute + "AG1" -> Status.AwaitingPickup // Awaiting pickup at counter + "RE1" -> Status.DeliveryFailure // Returned to the expeditor + "DI0" -> Status.Delivered // Distributed in lot + "DI1" -> Status.Delivered // Distributed + "DI2" -> Status.DeliveryFailure // Distributed to the expeditor (If sent back I suppose) + "DI3" -> Status.OutForDelivery // Delayed (This probably means still distributing) + "ID0" -> Status.Customs // Customs information + // EP1 (Waiting presentation), PB1 (Issue in progress), PB2 (Issue resolved) + // Ignored as unknown statuses, these errors are mid process statuses so + // it shouldn't be a big deal + else -> logUnknownStatus("Colissimo", lastEvent.code) + } } - private val retrofit = Retrofit.Builder().baseUrl("https://www.laposte.fr/ssu/sun/back/suivi-unifie/").client(api_client) .addConverterFactory(api_factory).build() - private val service = retrofit.create(API::class.java) - - private interface API { - @GET("{id}") - suspend fun getShipments( - @Path("id") trackingId: String, - @Query("lang") lang: String = "en_GB" - ): List - } - - @JsonClass(generateAdapter = true) - internal data class ShipmentResponse( - val shipment: Shipment, - ) - - @JsonClass(generateAdapter = true) - internal data class Shipment( - val idShip: String, - val event: List, - ) + val history = + events.map { + ParcelHistoryItem( + it.label, LocalDateTime.parse(it.date, DateTimeFormatter.ISO_DATE_TIME), it.country) + } - @JsonClass(generateAdapter = true) - internal data class Event( - val code: String, - val type: String, - val group: String, - val label: String, - val date: String, - val country: String, - val order: Int - ) + return Parcel(shipment.idShip, history, status) + } + + private val retrofit = + Retrofit.Builder() + .baseUrl("https://www.laposte.fr/ssu/sun/back/suivi-unifie/") + .client(api_client) + .addConverterFactory(api_factory) + .build() + private val service = retrofit.create(API::class.java) + + private interface API { + @GET("{id}") + suspend fun getShipments( + @Path("id") trackingId: String, + @Query("lang") lang: String = "en_GB" + ): List + } + + @JsonClass(generateAdapter = true) + internal data class ShipmentResponse( + val shipment: Shipment, + ) + + @JsonClass(generateAdapter = true) + internal data class Shipment( + val idShip: String, + val event: List, + ) + + @JsonClass(generateAdapter = true) + internal data class Event( + val code: String, + val type: String, + val group: String, + val label: String, + val date: String, + val country: String, + val order: Int + ) }