Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
*.ignore
quest-list.csv
/keystore.properties
/secrets.properties
/projectFilesBackup*/
app/release
*.iml
Expand Down
9 changes: 9 additions & 0 deletions app/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand All @@ -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 {
Expand Down
2 changes: 1 addition & 1 deletion app/src/androidDebug/res/values/conf.xml
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string translatable="false" name="fileprovider_authority">de.westnordost.streetcomplete.debug.fileprovider</string>
<string translatable="false" name="fileprovider_authority">net.opentoall.aviv.scoutroute.debug.fileprovider</string>
</resources>
4 changes: 0 additions & 4 deletions app/src/androidDebug/res/xml/file_paths.xml

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -147,6 +148,8 @@ val appModule = module {
}
}

single { KartaViewApiClient(get(), get(named("kartaViewClient")), get()) }

single<Res> { Res }
single<FileSystem> { SystemFileSystem }
single<ResourceProvider> { DefaultResourceProvider(androidContext(), get()) }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand All @@ -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
Expand All @@ -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
Expand All @@ -95,8 +83,7 @@ abstract class AbstractOsmQuestForm<T> : 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<Intent>

// only used for testing / only used for ShowQuestFormsScreen! Found no better way to do this
Expand Down Expand Up @@ -169,14 +156,13 @@ abstract class AbstractOsmQuestForm<T> : AbstractQuestForm(), IsShowingQuestDeta
if (getElement != null) {
element = getElement
}
val displayedLocation = args.getParcelable<Location>(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<Bitmap>("data")
startKartViewFlow(bitmap, displayedLocation)
startKartViewFlow(bitmap)
} else {

// Handle the error state here
Expand Down Expand Up @@ -446,109 +432,37 @@ abstract class AbstractOsmQuestForm<T> : 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<PhotoLookupResponse>()
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<CreateSequenceResponse>()
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
}
}

Expand All @@ -558,28 +472,6 @@ abstract class AbstractOsmQuestForm<T> : 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<CreateSequenceResponse>()
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"
Expand Down
7 changes: 7 additions & 0 deletions app/src/androidMain/res/layout/form_leave_note.xml
Original file line number Diff line number Diff line change
Expand Up @@ -24,4 +24,11 @@
android:background="@drawable/background_edittext_outline"
/>

<androidx.fragment.app.FragmentContainerView
android:id="@+id/attachPhotoFragment"
android:name="de.westnordost.streetcomplete.quests.note_discussion.AttachPhotoFragment"
android:layout_marginTop="8dp"
android:layout_width="match_parent"
android:layout_height="wrap_content"/>

</LinearLayout>
2 changes: 1 addition & 1 deletion app/src/androidMain/res/values/conf.xml
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string translatable="false" name="fileprovider_authority">de.westnordost.streetcomplete.fileprovider</string>
<string translatable="false" name="fileprovider_authority">net.opentoall.aviv.scoutroute.fileprovider</string>
</resources>
4 changes: 3 additions & 1 deletion app/src/androidMain/res/xml/file_paths.xml
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<paths>
<external-path name="images" path="Android/data/de.westnordost.streetcomplete/files/Pictures" />
<!-- resolves to getExternalFilesDir(DIRECTORY_PICTURES) of whatever package/build this is,
no hardcoded applicationId needed (see AttachPhotoFragment.createImageFile) -->
<external-files-path name="images" path="Pictures" />
</paths>
Loading