diff --git a/.gitignore b/.gitignore index ba3cf311e5a..1a7b071ef7e 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,7 @@ *.ignore quest-list.csv /keystore.properties +/secrets.properties /projectFilesBackup*/ app/release *.iml diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 343a73776b5..a2f74ef2dd4 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -54,6 +54,14 @@ repositories { maven { url = uri("https://www.jitpack.io") } } +/** KartaView access token: from env var, else root secrets.properties (git-ignored), else empty + * (build succeeds but KartaView photo uploads fail auth at runtime). Never commit the token. */ +val kartaViewAccessToken: String = System.getenv("KARTAVIEW_ACCESS_TOKEN") + ?: rootProject.file("secrets.properties").takeIf { it.exists() }?.let { file -> + Properties().apply { FileInputStream(file).use { load(it) } }.getProperty("kartaViewAccessToken") + } + ?: "" + buildkonfig { packageName = "de.westnordost.streetcomplete" objectName = "BuildConfig" @@ -62,6 +70,7 @@ buildkonfig { buildConfigField(BOOLEAN, "IS_FROM_MONOPOLISTIC_APP_STORE", properties["app.streetcomplete.monopolistic_app_store"]!!.toString()) buildConfigField(STRING, "VERSION_NAME", appVersionName) buildConfigField(BOOLEAN, "DEBUG", "true") + buildConfigField(STRING, "KARTAVIEW_ACCESS_TOKEN", kartaViewAccessToken) } targetConfigs { diff --git a/app/src/androidDebug/res/values/conf.xml b/app/src/androidDebug/res/values/conf.xml index e0acbd8a56a..157e85a2a43 100644 --- a/app/src/androidDebug/res/values/conf.xml +++ b/app/src/androidDebug/res/values/conf.xml @@ -1,4 +1,4 @@ - de.westnordost.streetcomplete.debug.fileprovider + net.opentoall.aviv.scoutroute.debug.fileprovider diff --git a/app/src/androidDebug/res/xml/file_paths.xml b/app/src/androidDebug/res/xml/file_paths.xml deleted file mode 100644 index b9f955c5421..00000000000 --- a/app/src/androidDebug/res/xml/file_paths.xml +++ /dev/null @@ -1,4 +0,0 @@ - - - - diff --git a/app/src/androidMain/kotlin/de/westnordost/streetcomplete/ApplicationModule.kt b/app/src/androidMain/kotlin/de/westnordost/streetcomplete/ApplicationModule.kt index f53aef239e0..7ab8599724b 100644 --- a/app/src/androidMain/kotlin/de/westnordost/streetcomplete/ApplicationModule.kt +++ b/app/src/androidMain/kotlin/de/westnordost/streetcomplete/ApplicationModule.kt @@ -3,6 +3,7 @@ package de.westnordost.streetcomplete import android.content.Intent import android.content.res.AssetManager import android.content.res.Resources +import de.westnordost.streetcomplete.data.karta_view.KartaViewApiClient import de.westnordost.streetcomplete.data.preferences.EnvironmentManager import de.westnordost.streetcomplete.data.preferences.Preferences import de.westnordost.streetcomplete.data.workspace.domain.model.LoginResponse @@ -147,6 +148,8 @@ val appModule = module { } } + single { KartaViewApiClient(get(), get(named("kartaViewClient")), get()) } + single { Res } single { SystemFileSystem } single { DefaultResourceProvider(androidContext(), get()) } diff --git a/app/src/androidMain/kotlin/de/westnordost/streetcomplete/quests/AbstractOsmQuestForm.kt b/app/src/androidMain/kotlin/de/westnordost/streetcomplete/quests/AbstractOsmQuestForm.kt index ba10da5be55..3a0ab647c17 100644 --- a/app/src/androidMain/kotlin/de/westnordost/streetcomplete/quests/AbstractOsmQuestForm.kt +++ b/app/src/androidMain/kotlin/de/westnordost/streetcomplete/quests/AbstractOsmQuestForm.kt @@ -27,8 +27,7 @@ import com.google.android.material.snackbar.Snackbar import de.westnordost.osmfeatures.Feature import de.westnordost.osmfeatures.FeatureDictionary import de.westnordost.streetcomplete.R -import de.westnordost.streetcomplete.data.karta_view.domain.model.CreateSequenceResponse -import de.westnordost.streetcomplete.data.karta_view.domain.model.PhotoLookupResponse +import de.westnordost.streetcomplete.data.karta_view.KartaViewApiClient import de.westnordost.streetcomplete.data.location.SurveyChecker import de.westnordost.streetcomplete.data.osm.edits.AddElementEditsController import de.westnordost.streetcomplete.data.osm.edits.ElementEditAction @@ -43,6 +42,7 @@ import de.westnordost.streetcomplete.data.osm.geometry.ElementGeometry import de.westnordost.streetcomplete.data.osm.geometry.ElementPolylinesGeometry import de.westnordost.streetcomplete.data.osm.mapdata.Element import de.westnordost.streetcomplete.data.osm.mapdata.ElementType +import de.westnordost.streetcomplete.data.osm.mapdata.LatLon import de.westnordost.streetcomplete.data.osm.mapdata.Node import de.westnordost.streetcomplete.data.osm.mapdata.Way import de.westnordost.streetcomplete.data.osm.osmquests.OsmElementQuestType @@ -53,7 +53,6 @@ import de.westnordost.streetcomplete.data.quest.Quest import de.westnordost.streetcomplete.data.quest.QuestKey import de.westnordost.streetcomplete.data.visiblequests.HideQuestController import de.westnordost.streetcomplete.data.visiblequests.QuestsHiddenController -import de.westnordost.streetcomplete.data.preferences.Preferences import de.westnordost.streetcomplete.osm.applyReplacePlaceTo import de.westnordost.streetcomplete.quests.sidewalk_long_form.AddGenericLong import de.westnordost.streetcomplete.screens.main.map.Compass @@ -62,17 +61,6 @@ import de.westnordost.streetcomplete.util.ktx.isSplittable import de.westnordost.streetcomplete.util.ktx.viewLifecycleScope import de.westnordost.streetcomplete.view.add import de.westnordost.streetcomplete.view.confirmIsSurvey -import io.ktor.client.HttpClient -import io.ktor.client.call.body -import io.ktor.client.request.forms.MultiPartFormDataContent -import io.ktor.client.request.forms.formData -import io.ktor.client.request.get -import io.ktor.client.request.parameter -import io.ktor.client.request.post -import io.ktor.client.request.setBody -import io.ktor.http.Headers -import io.ktor.http.HttpHeaders -import io.ktor.http.HttpStatusCode import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch import kotlinx.coroutines.withContext @@ -95,8 +83,7 @@ abstract class AbstractOsmQuestForm : AbstractQuestForm(), IsShowingQuestDeta private val surveyChecker: SurveyChecker by inject() protected val featureDictionary: FeatureDictionary get() = featureDictionaryLazy.value - private val httpClient: HttpClient by inject(named("kartaViewClient")) - private val prefs: Preferences by inject() + private val kartaViewApiClient: KartaViewApiClient by inject() private lateinit var cameraLauncher: ActivityResultLauncher // only used for testing / only used for ShowQuestFormsScreen! Found no better way to do this @@ -169,14 +156,13 @@ abstract class AbstractOsmQuestForm : AbstractQuestForm(), IsShowingQuestDeta if (getElement != null) { element = getElement } - val displayedLocation = args.getParcelable(ARG_DISPLAYED_LOCATION) cameraLauncher = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result -> if (result.resultCode == Activity.RESULT_OK) { showProgressbar() // Handle the image capture result here val bitmap = result.data?.extras?.getParcelable("data") - startKartViewFlow(bitmap, displayedLocation) + startKartViewFlow(bitmap) } else { // Handle the error state here @@ -446,109 +432,37 @@ abstract class AbstractOsmQuestForm : AbstractQuestForm(), IsShowingQuestDeta } } - private fun startKartViewFlow(bitmap: Bitmap?, displayedLocation: Location?) { + private fun startKartViewFlow(bitmap: Bitmap?) { viewLifecycleScope.launch { - val sequenceId = createSequence() ?: return@launch - val uploaded = uploadImageInSequence(sequenceId, bitmap, displayedLocation) - if (uploaded) { - closeSequence(sequenceId) - val lthUrl = getPhotoLthUrl(sequenceId, sequenceIndex = 1) - if (lthUrl != null) { - Log.d("KartViewFlow", lthUrl) - onImageUrlReceived(lthUrl) - } else { - hideProgressbar() - Log.e("KartViewFlow", "Failed to retrieve photo URL") - showSnackBar("Failed to retrieve photo URL. Please try again later.", view, requireActivity() as ComponentActivity) - } - } else { + val displayedLocation = listener?.displayedMapLocation + if (bitmap == null || displayedLocation == null) { hideProgressbar() - Log.e("KartViewFlow", "Image upload failed") - showSnackBar("Image upload failed. Please try again later.", view, requireActivity() as ComponentActivity) + return@launch } - } - } - - private suspend fun getPhotoLthUrl(sequenceId: String, sequenceIndex: Int): String? { - val token = prefs.kartaViewAccessToken - val response = httpClient.get("https://api.openstreetcam.org/2.0/photo/") { - parameter("access_token", token) - parameter("sequenceId", sequenceId) - parameter("sequenceIndex", sequenceIndex) - } - if (response.status == HttpStatusCode.OK) { - val photoResponse = response.body() - return photoResponse.result?.data?.firstOrNull()?.imageLthUrl - } - Log.e("KartViewFlow", "Photo lookup failed: ${response.status}") - return null - } - - private suspend fun closeSequence(sequenceId: String) { - val token = prefs.kartaViewAccessToken - val response = - httpClient.post("https://api.openstreetcam.org/1.0/sequence/finished-uploading/") { - setBody(MultiPartFormDataContent(formData { - append("access_token", token) - append("sequenceId", sequenceId) - })) + val bearing = if (displayedLocation.hasBearing() && displayedLocation.bearing != 0f) { + displayedLocation.bearing + } else { + compassBearing.toFloat() + } + val byteArrayOutputStream = ByteArrayOutputStream() + bitmap.compress(Bitmap.CompressFormat.JPEG, 100, byteArrayOutputStream) + try { + val urls = kartaViewApiClient.uploadImages( + listOf(byteArrayOutputStream.toByteArray()), + LatLon(displayedLocation.latitude, displayedLocation.longitude), + bearing + ) + showSnackBar("Image Uploaded Successfully", view, requireActivity() as ComponentActivity) + onImageUrlReceived(urls.first()) + } catch (e: Exception) { + Log.e("KartViewFlow", "KartaView upload failed", e) + showSnackBar( + e.message ?: "Image upload failed. Please try again later.", + view, requireActivity() as ComponentActivity + ) + } finally { + hideProgressbar() } - if (response.status == HttpStatusCode.OK) { - val sequence = response.body() - Log.d("KartViewSequence", sequence.status.httpMessage) - } else { - showSnackBar( - "Failed to close KartaView Sequence. Please try again later " + response.status, - view, requireActivity() as ComponentActivity - ) - } - hideProgressbar() - } - - private suspend fun uploadImageInSequence( - sequenceId: String, - bitmap: Bitmap?, - location: Location?, - ): Boolean { - val displayedLocation = listener?.displayedMapLocation ?: return false - bitmap ?: return false - - val byteArrayOutputStream = ByteArrayOutputStream() - bitmap.compress(Bitmap.CompressFormat.JPEG, 100, byteArrayOutputStream) - val byteArray = byteArrayOutputStream.toByteArray() - - val finalBearing = if (displayedLocation.hasBearing() && displayedLocation.bearing != 0f) { - displayedLocation.bearing - } else { - compassBearing.toFloat() - } - - val response = httpClient.post("https://api.openstreetcam.org/1.0/photo/") { - setBody(MultiPartFormDataContent(formData { - append("access_token", prefs.kartaViewAccessToken) - append("sequenceId", sequenceId) - append("sequenceIndex", 1) - append("coordinate", "${displayedLocation.latitude},${displayedLocation.longitude}") - append("headers", finalBearing.toInt().toString()) - append("photo", byteArray, Headers.build { - append(HttpHeaders.ContentType, "image/jpeg") - append(HttpHeaders.ContentDisposition, "filename=\"wework-kartaview.jpg\"") - }) - })) - } - - return if (response.status == HttpStatusCode.OK) { - showSnackBar("Image Uploaded Successfully", view, requireActivity() as ComponentActivity) - Log.d("UploadImage", "Image uploaded successfully") - true - } else { - Log.e("UploadImage", "Image upload failed: ${response.status}") - showSnackBar( - "Failed to upload image to KartaView. Please try again later " + response.status, - view, requireActivity() as ComponentActivity - ) - hideProgressbar() - false } } @@ -558,28 +472,6 @@ abstract class AbstractOsmQuestForm : AbstractQuestForm(), IsShowingQuestDeta } } - private suspend fun createSequence(): String? { - val token = prefs.kartaViewAccessToken - val response = httpClient.post("https://api.openstreetcam.org/1.0/sequence/") { - setBody(MultiPartFormDataContent(formData { - append("access_token", token) - })) - } - if (response.status == HttpStatusCode.OK) { - val sequence = response.body() - sequence.osv.sequence?.id?.let { Log.d("KartViewSequence", it) } - Log.d("KartViewStatus", sequence.status.httpMessage) - return sequence.osv.sequence?.id - } else { - hideProgressbar() - showSnackBar( - "Failed to create KartaView sequence. Image upload failed. Please try again later " + response.status, - view, requireActivity() as ComponentActivity - ) - return null - } - } - companion object { private const val ARG_ELEMENT = "element" private const val ARG_DISPLAYED_LOCATION = "displayedLocation" diff --git a/app/src/androidMain/res/layout/form_leave_note.xml b/app/src/androidMain/res/layout/form_leave_note.xml index efc9130b0d5..d92be965f84 100644 --- a/app/src/androidMain/res/layout/form_leave_note.xml +++ b/app/src/androidMain/res/layout/form_leave_note.xml @@ -24,4 +24,11 @@ android:background="@drawable/background_edittext_outline" /> + + diff --git a/app/src/androidMain/res/values/conf.xml b/app/src/androidMain/res/values/conf.xml index c5e57139d76..2697885525a 100644 --- a/app/src/androidMain/res/values/conf.xml +++ b/app/src/androidMain/res/values/conf.xml @@ -1,4 +1,4 @@ - de.westnordost.streetcomplete.fileprovider + net.opentoall.aviv.scoutroute.fileprovider diff --git a/app/src/androidMain/res/xml/file_paths.xml b/app/src/androidMain/res/xml/file_paths.xml index 195dcc2cab0..bc55a97853d 100644 --- a/app/src/androidMain/res/xml/file_paths.xml +++ b/app/src/androidMain/res/xml/file_paths.xml @@ -1,4 +1,6 @@ - + + diff --git a/app/src/commonMain/kotlin/de/westnordost/streetcomplete/data/karta_view/KartaViewApiClient.kt b/app/src/commonMain/kotlin/de/westnordost/streetcomplete/data/karta_view/KartaViewApiClient.kt new file mode 100644 index 00000000000..a4a6b708416 --- /dev/null +++ b/app/src/commonMain/kotlin/de/westnordost/streetcomplete/data/karta_view/KartaViewApiClient.kt @@ -0,0 +1,138 @@ +package de.westnordost.streetcomplete.data.karta_view + +import de.westnordost.streetcomplete.data.karta_view.domain.model.CreateSequenceResponse +import de.westnordost.streetcomplete.data.karta_view.domain.model.PhotoLookupResponse +import de.westnordost.streetcomplete.data.osm.mapdata.LatLon +import de.westnordost.streetcomplete.data.preferences.Preferences +import de.westnordost.streetcomplete.util.logs.Log +import io.ktor.client.HttpClient +import io.ktor.client.call.body +import io.ktor.client.request.forms.MultiPartFormDataContent +import io.ktor.client.request.forms.formData +import io.ktor.client.request.get +import io.ktor.client.request.parameter +import io.ktor.client.request.post +import io.ktor.client.request.setBody +import io.ktor.http.Headers +import io.ktor.http.HttpHeaders +import io.ktor.http.HttpStatusCode +import kotlinx.io.buffered +import kotlinx.io.files.FileSystem +import kotlinx.io.files.Path +import kotlinx.io.readByteArray + +/** Uploads photos to KartaView (api.openstreetcam.org). */ +class KartaViewApiClient( + private val fileSystem: FileSystem, + private val httpClient: HttpClient, + private val prefs: Preferences, +) { + /** Uploads the image files at [imagePaths] into one new KartaView sequence at [position], + * in list order. Paths that don't exist are skipped. Returns the lth photo URL for each + * uploaded image, in the same order. + * + * @throws KartaViewException naming the step that failed */ + suspend fun upload(imagePaths: List, position: LatLon, bearing: Float = 0f): List { + val images = imagePaths.mapNotNull { path -> + val file = Path(path) + if (fileSystem.exists(file)) fileSystem.source(file).buffered().readByteArray() else null + } + return uploadImages(images, position, bearing) + } + + /** Uploads [images] (JPEG-encoded) into one new KartaView sequence at [position], in list + * order (sequenceIndex 1..n), closes the sequence and returns the lth photo URL for each + * image, in the same order. + * + * @throws KartaViewException naming the step that failed */ + suspend fun uploadImages(images: List, position: LatLon, bearing: Float = 0f): List { + if (images.isEmpty()) return emptyList() + val sequenceId = createSequence() + images.forEachIndexed { index, image -> + uploadPhoto(sequenceId, index + 1, image, position, bearing) + } + closeSequence(sequenceId) + return List(images.size) { index -> getPhotoLthUrl(sequenceId, index + 1) } + } + + private suspend fun createSequence(): String { + val response = httpClient.post(BASE_URL + "1.0/sequence/") { + setBody(MultiPartFormDataContent(formData { + append("access_token", prefs.kartaViewAccessToken) + })) + } + if (response.status == HttpStatusCode.OK) { + val sequence = response.body() + Log.d(TAG, "Sequence created: ${sequence.status.httpMessage}") + sequence.osv.sequence?.id?.let { return it } + } + throw KartaViewException( + "Failed to create KartaView sequence. Image upload failed. Please try again later " + response.status + ) + } + + private suspend fun uploadPhoto( + sequenceId: String, + sequenceIndex: Int, + image: ByteArray, + position: LatLon, + bearing: Float, + ) { + val response = httpClient.post(BASE_URL + "1.0/photo/") { + setBody(MultiPartFormDataContent(formData { + append("access_token", prefs.kartaViewAccessToken) + append("sequenceId", sequenceId) + append("sequenceIndex", sequenceIndex) + append("coordinate", "${position.latitude},${position.longitude}") + append("headers", bearing.toInt().toString()) + append("photo", image, Headers.build { + append(HttpHeaders.ContentType, "image/jpeg") + append(HttpHeaders.ContentDisposition, "filename=\"wework-kartaview.jpg\"") + }) + })) + } + if (response.status != HttpStatusCode.OK) { + throw KartaViewException( + "Failed to upload image to KartaView. Please try again later " + response.status + ) + } + Log.d(TAG, "Image $sequenceIndex uploaded to sequence $sequenceId") + } + + private suspend fun closeSequence(sequenceId: String) { + val response = httpClient.post(BASE_URL + "1.0/sequence/finished-uploading/") { + setBody(MultiPartFormDataContent(formData { + append("access_token", prefs.kartaViewAccessToken) + append("sequenceId", sequenceId) + })) + } + if (response.status != HttpStatusCode.OK) { + throw KartaViewException( + "Failed to close KartaView Sequence. Please try again later " + response.status + ) + } + Log.d(TAG, "Sequence closed: ${response.body().status.httpMessage}") + } + + private suspend fun getPhotoLthUrl(sequenceId: String, sequenceIndex: Int): String { + val response = httpClient.get(BASE_URL + "2.0/photo/") { + parameter("access_token", prefs.kartaViewAccessToken) + parameter("sequenceId", sequenceId) + parameter("sequenceIndex", sequenceIndex) + } + if (response.status == HttpStatusCode.OK) { + val url = response.body().result?.data?.firstOrNull()?.imageLthUrl + if (url != null) return url + } + throw KartaViewException( + "Failed to retrieve photo URL. Please try again later " + response.status + ) + } + + companion object { + private const val TAG = "KartaViewApiClient" + private const val BASE_URL = "https://api.openstreetcam.org/" + } +} + +class KartaViewException(message: String) : RuntimeException(message) diff --git a/app/src/androidMain/kotlin/de/westnordost/streetcomplete/data/karta_view/domain/model/CreateSequenceResponse.kt b/app/src/commonMain/kotlin/de/westnordost/streetcomplete/data/karta_view/domain/model/CreateSequenceResponse.kt similarity index 100% rename from app/src/androidMain/kotlin/de/westnordost/streetcomplete/data/karta_view/domain/model/CreateSequenceResponse.kt rename to app/src/commonMain/kotlin/de/westnordost/streetcomplete/data/karta_view/domain/model/CreateSequenceResponse.kt diff --git a/app/src/androidMain/kotlin/de/westnordost/streetcomplete/data/karta_view/domain/model/ImageUploadResponse.kt b/app/src/commonMain/kotlin/de/westnordost/streetcomplete/data/karta_view/domain/model/ImageUploadResponse.kt similarity index 100% rename from app/src/androidMain/kotlin/de/westnordost/streetcomplete/data/karta_view/domain/model/ImageUploadResponse.kt rename to app/src/commonMain/kotlin/de/westnordost/streetcomplete/data/karta_view/domain/model/ImageUploadResponse.kt diff --git a/app/src/androidMain/kotlin/de/westnordost/streetcomplete/data/karta_view/domain/model/Osv.kt b/app/src/commonMain/kotlin/de/westnordost/streetcomplete/data/karta_view/domain/model/Osv.kt similarity index 100% rename from app/src/androidMain/kotlin/de/westnordost/streetcomplete/data/karta_view/domain/model/Osv.kt rename to app/src/commonMain/kotlin/de/westnordost/streetcomplete/data/karta_view/domain/model/Osv.kt diff --git a/app/src/androidMain/kotlin/de/westnordost/streetcomplete/data/karta_view/domain/model/OsvX.kt b/app/src/commonMain/kotlin/de/westnordost/streetcomplete/data/karta_view/domain/model/OsvX.kt similarity index 100% rename from app/src/androidMain/kotlin/de/westnordost/streetcomplete/data/karta_view/domain/model/OsvX.kt rename to app/src/commonMain/kotlin/de/westnordost/streetcomplete/data/karta_view/domain/model/OsvX.kt diff --git a/app/src/androidMain/kotlin/de/westnordost/streetcomplete/data/karta_view/domain/model/Photo.kt b/app/src/commonMain/kotlin/de/westnordost/streetcomplete/data/karta_view/domain/model/Photo.kt similarity index 100% rename from app/src/androidMain/kotlin/de/westnordost/streetcomplete/data/karta_view/domain/model/Photo.kt rename to app/src/commonMain/kotlin/de/westnordost/streetcomplete/data/karta_view/domain/model/Photo.kt diff --git a/app/src/androidMain/kotlin/de/westnordost/streetcomplete/data/karta_view/domain/model/PhotoLookupResponse.kt b/app/src/commonMain/kotlin/de/westnordost/streetcomplete/data/karta_view/domain/model/PhotoLookupResponse.kt similarity index 100% rename from app/src/androidMain/kotlin/de/westnordost/streetcomplete/data/karta_view/domain/model/PhotoLookupResponse.kt rename to app/src/commonMain/kotlin/de/westnordost/streetcomplete/data/karta_view/domain/model/PhotoLookupResponse.kt diff --git a/app/src/androidMain/kotlin/de/westnordost/streetcomplete/data/karta_view/domain/model/Sequence.kt b/app/src/commonMain/kotlin/de/westnordost/streetcomplete/data/karta_view/domain/model/Sequence.kt similarity index 100% rename from app/src/androidMain/kotlin/de/westnordost/streetcomplete/data/karta_view/domain/model/Sequence.kt rename to app/src/commonMain/kotlin/de/westnordost/streetcomplete/data/karta_view/domain/model/Sequence.kt diff --git a/app/src/androidMain/kotlin/de/westnordost/streetcomplete/data/karta_view/domain/model/Status.kt b/app/src/commonMain/kotlin/de/westnordost/streetcomplete/data/karta_view/domain/model/Status.kt similarity index 100% rename from app/src/androidMain/kotlin/de/westnordost/streetcomplete/data/karta_view/domain/model/Status.kt rename to app/src/commonMain/kotlin/de/westnordost/streetcomplete/data/karta_view/domain/model/Status.kt diff --git a/app/src/androidMain/kotlin/de/westnordost/streetcomplete/data/karta_view/domain/model/StatusX.kt b/app/src/commonMain/kotlin/de/westnordost/streetcomplete/data/karta_view/domain/model/StatusX.kt similarity index 100% rename from app/src/androidMain/kotlin/de/westnordost/streetcomplete/data/karta_view/domain/model/StatusX.kt rename to app/src/commonMain/kotlin/de/westnordost/streetcomplete/data/karta_view/domain/model/StatusX.kt diff --git a/app/src/commonMain/kotlin/de/westnordost/streetcomplete/data/osmnotes/NotesModule.kt b/app/src/commonMain/kotlin/de/westnordost/streetcomplete/data/osmnotes/NotesModule.kt index 52ef0e7841c..dedf4d32675 100644 --- a/app/src/commonMain/kotlin/de/westnordost/streetcomplete/data/osmnotes/NotesModule.kt +++ b/app/src/commonMain/kotlin/de/westnordost/streetcomplete/data/osmnotes/NotesModule.kt @@ -1,6 +1,5 @@ package de.westnordost.streetcomplete.data.osmnotes -import de.westnordost.streetcomplete.ApplicationConstants import org.koin.core.qualifier.named import org.koin.dsl.module @@ -9,7 +8,6 @@ val notesModule = module { factory { AvatarsInNotesUpdater(get()) } factory { NoteDao(get(), get()) } factory { NotesDownloader(get(), get()) } - factory { PhotoServiceApiClient(get(), get(), ApplicationConstants.SC_PHOTO_SERVICE_URL) } single { NoteController(get()).apply { diff --git a/app/src/commonMain/kotlin/de/westnordost/streetcomplete/data/osmnotes/edits/NoteEditsUploader.kt b/app/src/commonMain/kotlin/de/westnordost/streetcomplete/data/osmnotes/edits/NoteEditsUploader.kt index f7166502eaf..b1dee408b47 100644 --- a/app/src/commonMain/kotlin/de/westnordost/streetcomplete/data/osmnotes/edits/NoteEditsUploader.kt +++ b/app/src/commonMain/kotlin/de/westnordost/streetcomplete/data/osmnotes/edits/NoteEditsUploader.kt @@ -2,9 +2,10 @@ package de.westnordost.streetcomplete.data.osmnotes.edits import de.westnordost.streetcomplete.ApplicationConstants import de.westnordost.streetcomplete.data.ConflictException +import de.westnordost.streetcomplete.data.karta_view.KartaViewApiClient +import de.westnordost.streetcomplete.data.osm.mapdata.LatLon import de.westnordost.streetcomplete.data.osmnotes.NoteController import de.westnordost.streetcomplete.data.osmnotes.NotesApiClient -import de.westnordost.streetcomplete.data.osmnotes.PhotoServiceApiClient import de.westnordost.streetcomplete.data.osmnotes.edits.NoteEditAction.COMMENT import de.westnordost.streetcomplete.data.osmnotes.edits.NoteEditAction.CREATE import de.westnordost.streetcomplete.data.osmtracks.Trackpoint @@ -30,7 +31,7 @@ class NoteEditsUploader( private val userDataSource: UserDataSource, private val notesApi: NotesApiClient, private val tracksApi: TracksApiClient, - private val imageUploader: PhotoServiceApiClient, + private val imageUploader: KartaViewApiClient, private val fileSystem: FileSystem, ) { var uploadedChangeListener: OnUploadedChangeListener? = null @@ -51,13 +52,11 @@ class NoteEditsUploader( } } private suspend fun uploadMissedImageActivations() { + // KartaView photos need no activation step (their URLs are embedded in the note text at + // upload) - just clear any flags still set, e.g. from before the switch to KartaView while (true) { val edit = noteEditsController.getOldestNeedingImagesActivation() ?: break - // see uploadEdits - withContext(scope.coroutineContext) { - imageUploader.activate(edit.noteId) - noteEditsController.markImagesActivated(edit.id) - } + noteEditsController.markImagesActivated(edit.id) } } @@ -73,7 +72,7 @@ class NoteEditsUploader( private suspend fun uploadEdit(edit: NoteEdit) { // try to upload the image and track if we have them - val imageText = uploadAndGetAttachedPhotosText(edit.imagePaths) + val imageText = uploadAndGetAttachedPhotosText(edit.imagePaths, edit.position) val trackText = uploadAndGetAttachedTrackText(edit.track, edit.text) val text = edit.text.orEmpty() + imageText + trackText @@ -94,7 +93,6 @@ class NoteEditsUploader( noteController.put(note) if (edit.imagePaths.isNotEmpty()) { - imageUploader.activate(note.id) noteEditsController.markImagesActivated(note.id) } @@ -124,9 +122,9 @@ class NoteEditsUploader( } } - private suspend fun uploadAndGetAttachedPhotosText(imagePaths: List): String { + private suspend fun uploadAndGetAttachedPhotosText(imagePaths: List, position: LatLon): String { if (imagePaths.isNotEmpty()) { - val urls = imageUploader.upload(imagePaths) + val urls = imageUploader.upload(imagePaths, position) if (urls.isNotEmpty()) { return "\n\nAttached photo(s):\n" + urls.joinToString("\n") } diff --git a/app/src/commonMain/kotlin/de/westnordost/streetcomplete/data/preferences/Preferences.kt b/app/src/commonMain/kotlin/de/westnordost/streetcomplete/data/preferences/Preferences.kt index 8f48ecb427b..d5bba25c365 100644 --- a/app/src/commonMain/kotlin/de/westnordost/streetcomplete/data/preferences/Preferences.kt +++ b/app/src/commonMain/kotlin/de/westnordost/streetcomplete/data/preferences/Preferences.kt @@ -8,6 +8,7 @@ import com.russhwolf.settings.int import com.russhwolf.settings.long import com.russhwolf.settings.nullableString import com.russhwolf.settings.set +import de.westnordost.streetcomplete.BuildConfig import de.westnordost.streetcomplete.data.osm.mapdata.LatLon import de.westnordost.streetcomplete.util.ktx.putStringOrNull import kotlinx.coroutines.flow.MutableStateFlow @@ -214,7 +215,7 @@ class Preferences(private val prefs: ObservableSettings) { var kartaViewAccessToken: String set(value) { prefs.putString(KARTAVIEW_ACCESS_TOKEN, value) } - get() = prefs.getString(KARTAVIEW_ACCESS_TOKEN, DEFAULT_KARTAVIEW_ACCESS_TOKEN) + get() = prefs.getString(KARTAVIEW_ACCESS_TOKEN, BuildConfig.KARTAVIEW_ACCESS_TOKEN) var isFollowModeEnabled: Boolean set(value) { @@ -445,7 +446,5 @@ class Preferences(private val prefs: ObservableSettings) { private const val FOLLOW_MODE_ENABLED = "follow.mode.enabled" private const val DEBUG_MODE_ENABLED = "debug.mode.enabled" private const val KARTAVIEW_ACCESS_TOKEN = "kartaview.access_token" - private const val DEFAULT_KARTAVIEW_ACCESS_TOKEN = - "5b7bfc9104ad198bdba4108ed3048f380f118cfb528d750a2bf31041b7266a99" } } diff --git a/secrets.properties.example b/secrets.properties.example new file mode 100644 index 00000000000..91b949ea791 --- /dev/null +++ b/secrets.properties.example @@ -0,0 +1,8 @@ +# Copy this file to secrets.properties (git-ignored) and fill in the values. +# Alternatively, each value can be supplied via an environment variable of the +# same name as noted below (the env var takes precedence). + +# KartaView (api.openstreetcam.org) access token used for photo uploads. +# Obtain one from the KartaView account that owns the app's photo sequences. +# Env var alternative: KARTAVIEW_ACCESS_TOKEN +kartaViewAccessToken=