7 Commits b0f9c5640c ... ba5e8a66f3

Autor SHA1 Nachricht Datum
  Adam Evyčędo ba5e8a66f3 Merge branch 'translations' into develop vor 3 Monaten
  Adam Evyčędo 46c7a266c2 update translations vor 3 Monaten
  Adam Evyčędo 188dcb3d30 localise last geocoding data update vor 3 Monaten
  Adam Evyčędo 2ea93be4b5 add content descriptions vor 3 Monaten
  Adam Evyčędo 60d2e029b8 use geonames to geocode short plus codes vor 3 Monaten
  Adam Evyčędo a279d580f7 geocode short OLC vor 3 Monaten
  Languages add-on c66d69c69f Added translation using Weblate (English (United Kingdom)) vor 3 Monaten

+ 7 - 7
app/build.gradle

@@ -16,7 +16,7 @@ plugins {
 }
 
 android {
-    compileSdk 34
+    compileSdk 34  // https://gitlab.com/fdroid/fdroiddata/-/issues/3299#note_1989808414
 
     defaultConfig {
         applicationId "xyz.apiote.bimba.czwek"
@@ -49,11 +49,7 @@ android {
         viewBinding true
     }
     namespace 'xyz.apiote.bimba.czwek'
-    buildToolsVersion = '34.0.0'
-
-    sourceSets {
-        main.java.srcDirs += 'src/main/proto'
-    }
+    buildToolsVersion = '34.0.0'  // https://gitlab.com/fdroid/fdroiddata/-/issues/3299#note_1989808414
 }
 
 dependencies {
@@ -76,9 +72,13 @@ dependencies {
     implementation 'dev.bandb.graphview:graphview:0.8.1'
     implementation 'org.jetbrains.kotlinx:kotlinx-serialization-core:1.6.3'
     implementation 'com.github.jershell:kbson:0.5.0'
+    implementation 'androidx.preference:preference-ktx:1.2.1'
+    implementation 'androidx.work:work-runtime-ktx:2.9.0'
+    implementation 'com.github.doyaaaaaken:kotlin-csv-jvm:1.9.3'
+    implementation 'commons-io:commons-io:2.16.1'
+
 
     implementation project(path: ':fruchtfleisch')
-    implementation 'androidx.preference:preference:1.2.0'
 
     coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:2.0.4'
 

+ 55 - 0
app/src/androidTest/java/xyz/apiote/bimba/czwek/units/TGMKtTest.kt

@@ -0,0 +1,55 @@
+package xyz.apiote.bimba.czwek.units
+
+import androidx.test.platform.app.InstrumentationRegistry
+import org.junit.Test
+
+class TGMKtTest {
+	@Test
+	fun toDozenalString0(){
+		val i = 0
+		val context = InstrumentationRegistry.getInstrumentation().targetContext
+		assert(i.toDozenalString(context) == "zero") { "got ${i.toDozenalString(context)}, wanted zero" }
+	}
+
+	@Test
+	fun toDozenalString1(){
+		val i = 1
+		val context = InstrumentationRegistry.getInstrumentation().targetContext
+		assert(i.toDozenalString(context) == "one") { "got ${i.toDozenalString(context)}, wanted one" }
+	}
+
+	@Test
+	fun toDozenalString10(){
+		val i = 12
+		val context = InstrumentationRegistry.getInstrumentation().targetContext
+		assert(i.toDozenalString(context) == "one zen") { "got ${i.toDozenalString(context)}, wanted one zen" }
+	}
+
+	@Test
+	fun toDozenalString100(){
+		val i = 144
+		val context = InstrumentationRegistry.getInstrumentation().targetContext
+		assert(i.toDozenalString(context) == "one duna") { "got ${i.toDozenalString(context)}, wanted one duna" }
+	}
+
+	@Test
+	fun toDozenalString23(){
+		val i = 27
+		val context = InstrumentationRegistry.getInstrumentation().targetContext
+		assert(i.toDozenalString(context) == "two zen three") { "got ${i.toDozenalString(context)}, wanted two zen three" }
+	}
+
+	@Test
+	fun toDozenalString234(){
+		val i = 328
+		val context = InstrumentationRegistry.getInstrumentation().targetContext
+		assert(i.toDozenalString(context) == "two duna three zen four") { "got ${i.toDozenalString(context)}, wanted two duna three zen four" }
+	}
+
+	@Test
+	fun toDozenalString204(){
+		val i = 292
+		val context = InstrumentationRegistry.getInstrumentation().targetContext
+		assert(i.toDozenalString(context) == "two duna four") { "got ${i.toDozenalString(context)}, wanted two duna four" }
+	}
+}

+ 2 - 1
app/src/main/AndroidManifest.xml

@@ -6,6 +6,7 @@
 	<uses-permission android:name="android.permission.INTERNET" />
 	<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
 	<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
+	<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
 
 	<application
 		android:name=".Bimba"
@@ -23,7 +24,7 @@
 		<activity
 			android:name=".settings.SettingsActivity"
 			android:exported="false"
-			android:label="@string/title_activity_settings" />
+			android:label="@string/title_settings" />
 		<activity
 			android:name=".AboutActivity"
 			android:exported="false" />

+ 43 - 6
app/src/main/java/xyz/apiote/bimba/czwek/dashboard/MainActivity.kt

@@ -7,15 +7,16 @@ package xyz.apiote.bimba.czwek.dashboard
 import android.Manifest
 import android.content.Intent
 import android.content.pm.PackageManager
+import android.os.Build
 import android.os.Bundle
 import android.view.View
-import android.widget.Toast
 import androidx.activity.OnBackPressedCallback
 import androidx.activity.enableEdgeToEdge
 import androidx.activity.result.ActivityResultLauncher
 import androidx.activity.result.contract.ActivityResultContracts
 import androidx.appcompat.app.AppCompatActivity
 import androidx.appcompat.content.res.AppCompatResources
+import androidx.core.app.ActivityCompat
 import androidx.core.content.ContextCompat
 import androidx.core.content.edit
 import androidx.core.view.ViewCompat
@@ -28,6 +29,7 @@ import androidx.fragment.app.FragmentManager
 import androidx.fragment.app.FragmentManager.FragmentLifecycleCallbacks
 import androidx.navigation.fragment.NavHostFragment
 import androidx.navigation.ui.setupWithNavController
+import androidx.preference.PreferenceManager
 import com.google.android.material.bottomnavigation.BottomNavigationView
 import com.google.android.material.dialog.MaterialAlertDialogBuilder
 import com.google.openlocationcode.OpenLocationCode
@@ -156,12 +158,22 @@ class MainActivity : AppCompatActivity() {
 					)
 						.setTitle(getString(R.string.no_location_access))
 						.setMessage(getString(R.string.no_location_message))
-						.setPositiveButton(R.string.ok) { _, _ ->}
+						.setPositiveButton(R.string.ok) { _, _ -> }
 						.show()
 					locationPermissionDialogShown = true
 				}
 			}
 		}
+
+		if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
+			if (ActivityCompat.checkSelfPermission(
+					this,
+					Manifest.permission.POST_NOTIFICATIONS
+				) != PackageManager.PERMISSION_GRANTED && shouldShowRequestPermissionRationale(Manifest.permission.POST_NOTIFICATIONS)
+			) {
+				requestPermissions(arrayOf(Manifest.permission.POST_NOTIFICATIONS), 1)
+			}
+		}
 	}
 
 	fun onNavigationClicked() {
@@ -205,11 +217,36 @@ class MainActivity : AppCompatActivity() {
 		if (OpenLocationCode.isValidCode(text.toString())) {
 			val olc = OpenLocationCode(text.toString())
 			if (!olc.isFull) {
-				Toast.makeText(this, getString(R.string.code_is_not_full), Toast.LENGTH_LONG).show()
-				return
+				showResults(ResultsActivity.Mode.MODE_SHORT_CODE_LOCATION, text.toString())
+			} else {
+				val area = olc.decode()
+				showResults(olc.code, area.centerLatitude, area.centerLongitude)
+			}
+		} else if (OpenLocationCode.isValidCode(
+				text.toString().trim().split(" ").first().trim(',').trim()
+			)
+		) {
+			if (PreferenceManager.getDefaultSharedPreferences(applicationContext)
+					.getLong("cities_last_update", -1) < 0
+			) {
+				if (!PreferenceManager.getDefaultSharedPreferences(applicationContext)
+						.getBoolean("no_geocoding_data_shown", false)
+				) {
+					MaterialAlertDialogBuilder(this)
+						.setIcon(R.drawable.geocoding)
+						.setTitle(R.string.no_geocoding_data)
+						.setMessage(R.string.no_geocoding_data_description)
+						.setPositiveButton(R.string.ok) { _, _ ->
+							showResults(
+								ResultsActivity.Mode.MODE_SEARCH,
+								text.toString()
+							)
+						}
+						.show()
+				}
+			} else {
+				showResults(ResultsActivity.Mode.MODE_SHORT_CODE, text.toString())
 			}
-			val area = olc.decode()
-			showResults(olc.code, area.centerLatitude, area.centerLongitude)
 		} else {
 			showResults(ResultsActivity.Mode.MODE_SEARCH, text.toString())
 		}

+ 1 - 1
app/src/main/java/xyz/apiote/bimba/czwek/departures/DeparturesActivity.kt

@@ -232,7 +232,7 @@ class DeparturesActivity : AppCompatActivity() {
 							)
 						)
 							.setTitle("Filtered departures")
-							.setMessage("Do you want to save a favourite filtered with selected lines?")
+							.setMessage(R.string.filtered_stop_question)
 							.setPositiveButton(R.string.filtered) { _, _ ->
 								saveFavourite(viewModel.linesFilter.value!!.keys)
 							}

+ 37 - 0
app/src/main/java/xyz/apiote/bimba/czwek/onboarding/FirstRunActivity.kt

@@ -4,13 +4,23 @@
 
 package xyz.apiote.bimba.czwek.onboarding
 
+import android.app.NotificationChannel
+import android.app.NotificationManager
 import android.content.Intent
+import android.os.Build
 import android.os.Bundle
 import androidx.appcompat.app.AppCompatActivity
 import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen
+import androidx.preference.PreferenceManager
+import androidx.work.OneTimeWorkRequest
+import androidx.work.WorkManager
+import xyz.apiote.bimba.czwek.R
 import xyz.apiote.bimba.czwek.dashboard.MainActivity
 import xyz.apiote.bimba.czwek.repo.migrateDB
+import xyz.apiote.bimba.czwek.settings.DownloadCitiesWorker
 import xyz.apiote.bimba.czwek.settings.feeds.migrateFeedsSettings
+import java.time.Instant
+import java.time.temporal.ChronoUnit
 
 class FirstRunActivity : AppCompatActivity() {
 	override fun onCreate(savedInstanceState: Bundle?) {
@@ -21,6 +31,20 @@ class FirstRunActivity : AppCompatActivity() {
 
 		migrateFeedsSettings(this)
 		migrateDB(this)
+		createNotificationChannels()
+
+		val (updatesEnabled, weekPassed) = PreferenceManager.getDefaultSharedPreferences(this).let {
+			arrayOf(
+				it.getBoolean("autoupdate_cities_list", false),
+				Instant.ofEpochSecond(it.getLong("cities_last_update", 0)).plus(7, ChronoUnit.DAYS)
+					.isBefore(Instant.now())
+			)
+		}
+
+		if (updatesEnabled && weekPassed) {
+			WorkManager.getInstance(this)
+				.enqueue(OneTimeWorkRequest.from(DownloadCitiesWorker::class.java))
+		}
 
 		val intent = if (preferences.getBoolean("firstRun", true)) {
 			Intent(this, OnboardingActivity::class.java)
@@ -31,4 +55,17 @@ class FirstRunActivity : AppCompatActivity() {
 		finish()
 	}
 
+	private fun createNotificationChannels() {
+		if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
+			val name = getString(R.string.cities_channel_name)
+			val descriptionText = getString(R.string.cities_channel_description)
+			val importance = NotificationManager.IMPORTANCE_LOW
+			val channel = NotificationChannel("cities_channel", name, importance).apply {
+				description = descriptionText
+			}
+			val notificationManager: NotificationManager =
+				getSystemService(NOTIFICATION_SERVICE) as NotificationManager
+			notificationManager.createNotificationChannel(channel)
+		}
+	}
 }

+ 2 - 2
app/src/main/java/xyz/apiote/bimba/czwek/repo/Departure.kt

@@ -17,6 +17,7 @@ import xyz.apiote.bimba.czwek.api.DepartureV4
 import xyz.apiote.bimba.czwek.api.Time
 import xyz.apiote.bimba.czwek.api.UnknownResourceVersionException
 import xyz.apiote.bimba.czwek.units.Second
+import xyz.apiote.bimba.czwek.units.TGM
 import xyz.apiote.bimba.czwek.units.UnitSystem
 import java.time.Instant
 import java.time.ZoneId
@@ -157,7 +158,6 @@ data class Departure(
 		d.alerts.map { Alert(it) }
 	)
 
-	// TODO content description
 	fun statusText(context: Context?, showAsTime: Boolean, at: ZonedDateTime? = null): String {
 		val now = at ?: Instant.now().atZone(ZoneId.systemDefault())
 		val departureTime = ZonedDateTime.of(
@@ -172,7 +172,7 @@ data class Departure(
 			r = 0u
 		}
 		return when (r) {
-			0u -> if (context != null && UnitSystem.getSelected(context).base == 12) {
+			0u -> if (context != null && UnitSystem.getSelected(context) is TGM) {
 				val us = UnitSystem.getSelected(context)
 				us.toString(
 					context,

+ 37 - 3
app/src/main/java/xyz/apiote/bimba/czwek/search/ResultsActivity.kt

@@ -27,6 +27,7 @@ import androidx.core.view.WindowInsetsCompat
 import androidx.core.view.updateLayoutParams
 import androidx.core.view.updatePadding
 import androidx.recyclerview.widget.LinearLayoutManager
+import com.google.openlocationcode.OpenLocationCode
 import kotlinx.coroutines.MainScope
 import kotlinx.coroutines.Runnable
 import kotlinx.coroutines.launch
@@ -42,7 +43,7 @@ import xyz.apiote.bimba.czwek.settings.feeds.FeedsSettings
 
 class ResultsActivity : AppCompatActivity(), LocationListener, SensorEventListener {
 	enum class Mode {
-		MODE_LOCATION, MODE_SEARCH, MODE_POSITION
+		MODE_LOCATION, MODE_SEARCH, MODE_POSITION, MODE_SHORT_CODE_LOCATION, MODE_SHORT_CODE
 	}
 
 	private var _binding: ActivityResultsBinding? = null
@@ -54,6 +55,7 @@ class ResultsActivity : AppCompatActivity(), LocationListener, SensorEventListen
 	private var runnable = Runnable {}
 	private var gravity: FloatArray? = null
 	private var geomagnetic: FloatArray? = null
+	private var shortOLC: OpenLocationCode? = null
 
 	override fun onCreate(savedInstanceState: Bundle?) {
 		enableEdgeToEdge()
@@ -88,6 +90,30 @@ class ResultsActivity : AppCompatActivity(), LocationListener, SensorEventListen
 				locate()
 			}
 
+			Mode.MODE_SHORT_CODE_LOCATION -> {
+				val query = intent.extras?.getString("query")
+				getString(R.string.stops_near_code, query)
+				shortOLC = OpenLocationCode(query)
+				locate()
+			}
+
+			Mode.MODE_SHORT_CODE -> {
+				val query = intent.extras?.getString("query")
+				val split = query!!.trim().split(" ")
+				val code = split.first().trim(',').trim()
+				val freePart = split.drop(1).joinToString(" ")
+				val location = findPlace(this, freePart)
+				if (location == null) {
+					showError(Error(0, R.string.error_geocoding, R.drawable.geocoding))
+				} else {
+					val area = OpenLocationCode(code).recover(location.latitude, location.longitude).decode()
+					getQueryablesByLocation(Location(null).apply {
+						latitude = area.centerLatitude
+						longitude = area.centerLongitude
+					}, this)
+				}
+			}
+
 			Mode.MODE_POSITION -> {
 				val query = intent.extras?.getString("query")
 				val lat = intent.extras?.getDouble("lat")
@@ -142,7 +168,15 @@ class ResultsActivity : AppCompatActivity(), LocationListener, SensorEventListen
 
 	override fun onLocationChanged(location: Location) {
 		handler.removeCallbacks(runnable)
-		getQueryablesByLocation(location, this, true)
+		val area = shortOLC?.recover(location.latitude, location.longitude)?.decode()
+		if (area != null) {
+			getQueryablesByLocation(Location(null).apply {
+				latitude = area.centerLatitude
+				longitude = area.centerLongitude
+			}, this, false)
+		} else {
+			getQueryablesByLocation(location, this, true)
+		}
 	}
 
 	override fun onAccuracyChanged(sensor: Sensor?, accuracy: Int) {}
@@ -155,7 +189,7 @@ class ResultsActivity : AppCompatActivity(), LocationListener, SensorEventListen
 			if (success) {
 				val orientation = FloatArray(3)
 				SensorManager.getOrientation(r, orientation)
-				adapter.update((orientation[0]*180/Math.PI).toFloat())
+				adapter.update((orientation[0] * 180 / Math.PI).toFloat())
 			}
 		}
 	}

+ 27 - 0
app/src/main/java/xyz/apiote/bimba/czwek/search/geocoder.kt

@@ -0,0 +1,27 @@
+package xyz.apiote.bimba.czwek.search
+
+import android.content.Context
+import android.database.sqlite.SQLiteDatabase
+import android.location.Location
+
+fun findPlace(context: Context, name: String): Location? {
+	val db = SQLiteDatabase.openOrCreateDatabase(context.getDatabasePath("geocoding").path, null)
+	val cursor = db.rawQuery(
+		"select lat, lon from place_names join places using(id) where name = ?",
+		arrayOf(name)
+	)
+
+	if (!cursor.moveToNext()) {
+		cursor.close()
+		db.close()
+		return null
+	}
+
+	val location = Location(null).apply {
+		latitude = cursor.getDouble(0)
+		longitude = cursor.getDouble(1)
+	}
+	cursor.close()
+	db.close()
+	return location
+}

+ 0 - 0
app/src/main/java/xyz/apiote/bimba/czwek/settings/DownloadCitiesWorker.kt


Einige Dateien werden nicht angezeigt, da zu viele Dateien in diesem Diff geändert wurden.