Author: Adam Evyčędo <git@apiote.xyz>
use geonames to geocode short plus codes
%!v(PANIC=String method: strings: negative Repeat count)
diff --git a/app/build.gradle b/app/build.gradle index f931b1b48dc9a36904514b488590addb09d64851..07698bbf2dd7f2ab28d0183138fdf1d0cda83efd 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -16,7 +16,7 @@ id "com.google.protobuf" version "0.9.4" } android { - compileSdk 34 + compileSdk 34 // https://gitlab.com/fdroid/fdroiddata/-/issues/3299#note_1989808414 defaultConfig { applicationId "xyz.apiote.bimba.czwek" @@ -49,11 +49,7 @@ buildFeatures { 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 @@ implementation 'com.otaliastudios:zoomlayout:1.9.0' 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' diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 3cd49cd487e5d6f55d771451b68bbea53a362c70..c7a89b60166547a29e090a5aa32fc59f21652325 100644 --- a/app/src/main/AndroidManifest.xml +++ b/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" diff --git a/app/src/main/java/xyz/apiote/bimba/czwek/dashboard/MainActivity.kt b/app/src/main/java/xyz/apiote/bimba/czwek/dashboard/MainActivity.kt index 7ec5dde6fb6c598c55b32452aadb4de2f68380f4..5b7701049711808d7606ecf398e3d60a65f685aa 100644 --- a/app/src/main/java/xyz/apiote/bimba/czwek/dashboard/MainActivity.kt +++ b/app/src/main/java/xyz/apiote/bimba/czwek/dashboard/MainActivity.kt @@ -7,18 +7,16 @@ import android.Manifest import android.content.Intent import android.content.pm.PackageManager -import android.location.Address -import android.location.Geocoder 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 @@ -31,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 @@ -159,10 +158,20 @@ ) ) .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) } } } @@ -208,37 +217,39 @@ fun onSearchClicked(text: CharSequence?) { 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) } - val area = olc.decode() - showResults(olc.code, area.centerLatitude, area.centerLongitude) - } else if (text.toString().contains(",") && OpenLocationCode.isValidCode(text.toString().split(",").last().trim()) && Geocoder.isPresent()) { - val split = text.toString().split(",") - val code = split.last().trim() - val freePart = split.dropLast(1).joinToString(",") - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { - Geocoder(this).getFromLocationName(freePart, 1) { - useShortOlc(it, code, text) + } 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 { - @Suppress("DEPRECATION") - val addresses = Geocoder(this).getFromLocationName(freePart, 1) - useShortOlc(addresses, code, text) + } else { + showResults(ResultsActivity.Mode.MODE_SHORT_CODE, text.toString()) } } else { showResults(ResultsActivity.Mode.MODE_SEARCH, text.toString()) } - } - - private fun useShortOlc(addresses: List<Address>?, code: String, text: CharSequence?) { - if (addresses.isNullOrEmpty()) { - showResults(ResultsActivity.Mode.MODE_SEARCH, text.toString()) - return - } - - val area = OpenLocationCode(code).recover(addresses[0].latitude, addresses[0].longitude).decode() - showResults(text.toString(), area.centerLatitude, area.centerLongitude) } private fun showResults(query: String, centerLatitude: Double, centerLongitude: Double) { diff --git a/app/src/main/java/xyz/apiote/bimba/czwek/onboarding/FirstRunActivity.kt b/app/src/main/java/xyz/apiote/bimba/czwek/onboarding/FirstRunActivity.kt index 8f29cb95067119e2ee24b1688138d347ad55b74b..117278a8722bd5277e13416aeb8abdeea2ea8c24 100644 --- a/app/src/main/java/xyz/apiote/bimba/czwek/onboarding/FirstRunActivity.kt +++ b/app/src/main/java/xyz/apiote/bimba/czwek/onboarding/FirstRunActivity.kt @@ -4,13 +4,23 @@ // SPDX-License-Identifier: GPL-3.0-or-later 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 @@ val preferences = getSharedPreferences("shp", MODE_PRIVATE) 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 @@ startActivity(intent) 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) + } + } } \ No newline at end of file diff --git a/app/src/main/java/xyz/apiote/bimba/czwek/search/ResultsActivity.kt b/app/src/main/java/xyz/apiote/bimba/czwek/search/ResultsActivity.kt index b6790e7d43cb8f79a365e3bb9b3fa4d5f7bf7e25..9a778552b653033536f19fbbb2deb113457e765b 100644 --- a/app/src/main/java/xyz/apiote/bimba/czwek/search/ResultsActivity.kt +++ b/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 @@ private val handler = Handler(Looper.getMainLooper()) 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 @@ supportActionBar?.title = getString(R.string.stops_nearby) 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 @@ } 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 @@ val success = SensorManager.getRotationMatrix(r, FloatArray(9), gravity, geomagnetic) 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()) } } } diff --git a/app/src/main/java/xyz/apiote/bimba/czwek/search/geocoder.kt b/app/src/main/java/xyz/apiote/bimba/czwek/search/geocoder.kt new file mode 100644 index 0000000000000000000000000000000000000000..7fd75e381c417517d76ecd5d8b2f68ac68718d0a --- /dev/null +++ b/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 +} \ No newline at end of file diff --git a/app/src/main/java/xyz/apiote/bimba/czwek/settings/DownloadCitiesWorker.kt b/app/src/main/java/xyz/apiote/bimba/czwek/settings/DownloadCitiesWorker.kt new file mode 100644 index 0000000000000000000000000000000000000000..02de1d344e0666614ee02207fe90531cd3cb5626 --- /dev/null +++ b/app/src/main/java/xyz/apiote/bimba/czwek/settings/DownloadCitiesWorker.kt @@ -0,0 +1,221 @@ +package xyz.apiote.bimba.czwek.settings + +import android.Manifest +import android.app.NotificationManager +import android.content.Context +import android.content.pm.PackageManager +import android.database.sqlite.SQLiteDatabase +import android.util.Log +import androidx.core.app.ActivityCompat +import androidx.core.app.NotificationCompat +import androidx.core.app.NotificationManagerCompat +import androidx.core.content.edit +import androidx.core.database.sqlite.transaction +import androidx.preference.PreferenceManager +import androidx.work.Worker +import androidx.work.WorkerParameters +import com.github.doyaaaaaken.kotlincsv.dsl.csvReader +import org.apache.commons.io.input.BoundedInputStream +import xyz.apiote.bimba.czwek.R +import java.io.BufferedInputStream +import java.io.BufferedOutputStream +import java.io.File +import java.net.URL +import java.time.ZonedDateTime +import java.util.UUID +import java.util.zip.ZipEntry +import java.util.zip.ZipInputStream + +class DownloadCitiesWorker(appContext: Context, workerParams: WorkerParameters) : + Worker(appContext, workerParams) { + + override fun doWork(): Result { + val notificationBuilder = NotificationCompat.Builder(applicationContext, "cities_channel") + .setSmallIcon(R.drawable.geocoding) + .setContentTitle(applicationContext.getString(R.string.updating_geocoding_data)) + .setContentText(applicationContext.getString(R.string.downloading_cities_list)) + .setPriority(NotificationCompat.PRIORITY_LOW) + .setProgress(100, 0, true) + try { + if (ActivityCompat.checkSelfPermission( + applicationContext, + Manifest.permission.POST_NOTIFICATIONS + ) == PackageManager.PERMISSION_GRANTED + ) { + NotificationManagerCompat.from(applicationContext).notify(0, notificationBuilder.build()) + } + + val db = SQLiteDatabase.openOrCreateDatabase( + applicationContext.getDatabasePath("geocoding").path, + null + ) + val url = URL("https://download.geonames.org/export/dump/cities15000.zip") + val connection = url.openConnection() + var length = connection.contentLength.toLong() + val connectionEtag = connection.getHeaderField("ETag") + val savedEtag = PreferenceManager.getDefaultSharedPreferences(applicationContext) + .getString("cities_etag", null) + if (savedEtag != null && savedEtag == connectionEtag) { + if (ActivityCompat.checkSelfPermission( + applicationContext, + Manifest.permission.POST_NOTIFICATIONS + ) == PackageManager.PERMISSION_GRANTED + ) { + NotificationManagerCompat.from(applicationContext).cancel(0) + } + return Result.success() + } + + db.execSQL("drop table if exists place_names2") + db.execSQL("drop table if exists places2") + db.execSQL("create table places2(id text primary key, lat real, lon real)") + db.execSQL("create table place_names2(id text references places(id), name text primary key)") + + var countingStream = + BoundedInputStream.Builder() + .setInputStream(BufferedInputStream(connection.getInputStream())).get() + val zipFileStream = BufferedOutputStream( + File( + applicationContext.noBackupFilesDir.path, + "cities.zip" + ).outputStream() + ) + + val buffer = ByteArray(DEFAULT_BUFFER_SIZE) + var bytes = countingStream.read(buffer) + while (bytes >= 0) { + zipFileStream.write(buffer, 0, bytes) + Log.i( + "geocoding", + "zip_download: downloaded ${countingStream.count}/$length: ${countingStream.count.toFloat() / length * 100}%" + ) + if (ActivityCompat.checkSelfPermission( + applicationContext, + Manifest.permission.POST_NOTIFICATIONS + ) == PackageManager.PERMISSION_GRANTED + ) { + notificationBuilder + .setProgress(100, (countingStream.count.toFloat() / length * 100).toInt(), false) + NotificationManagerCompat.from(applicationContext).notify(0, notificationBuilder.build()) + } + bytes = countingStream.read(buffer) + } + countingStream.close() + zipFileStream.close() + + notificationBuilder + .setProgress(100, 0, true) + .setContentText(applicationContext.getString(R.string.saving_cities_list)) + if (ActivityCompat.checkSelfPermission( + applicationContext, + Manifest.permission.POST_NOTIFICATIONS + ) == PackageManager.PERMISSION_GRANTED + ) { + NotificationManagerCompat.from(applicationContext).notify(0, notificationBuilder.build()) + } + val zipFile = File(applicationContext.noBackupFilesDir.path, "cities.zip") + length = zipFile.length() + countingStream = + BoundedInputStream.Builder().setInputStream(BufferedInputStream(zipFile.inputStream())) + .get() + val stream = ZipInputStream(countingStream) + var entry: ZipEntry? = stream.nextEntry + while (entry != null) { + if (entry.name != "cities15000.txt") { + entry = stream.nextEntry + continue + } + var count = 0 + db.transaction { + csvReader { delimiter = '\t' }.open(stream) { + readAllAsSequence().forEach { row -> + val names = if (row[3] == "") { + "${row[1]},${row[2]}" + } else { + row[3] + } + if (count % 1000 == 0) { + Log.i( + "geocoding", + "${countingStream.count}/$length=${countingStream.count.toFloat() / length * 100}% $names" + ) + if (ActivityCompat.checkSelfPermission( + applicationContext, + Manifest.permission.POST_NOTIFICATIONS + ) == PackageManager.PERMISSION_GRANTED + ) { + notificationBuilder + .setProgress( + 100, + (countingStream.count.toFloat() / length * 100).toInt(), + false + ) + NotificationManagerCompat.from(applicationContext) + .notify(0, notificationBuilder.build()) + } + } + count++ + + val id = UUID.randomUUID() + db.execSQL("insert into places2 values(?, ?, ?)", arrayOf(id, row[4], row[5])) + names.split(",").toSet().forEach { name -> + db.execSQL( + "insert into place_names2 values(?, ?) on conflict(name) do nothing", + arrayOf(id, name) + ) + db.execSQL( + "insert into place_names2 values(?, ?) on conflict(name) do nothing", + arrayOf(id, "$name, ${row[8]}") + ) + } + } + } + } + Log.i("geocoding", "COMPLETE") + break + } + stream.close() + zipFile.delete() + + db.execSQL("drop index if exists place_names__name") + db.execSQL("drop table if exists place_names") + db.execSQL("drop table if exists places") + db.execSQL("alter table places2 rename to places") + db.execSQL("alter table place_names2 rename to place_names") + db.execSQL("create unique index place_names__name on place_names(name)") + + PreferenceManager.getDefaultSharedPreferences(applicationContext).edit { + putLong("cities_last_update", ZonedDateTime.now().toEpochSecond()) + putString("cities_etag", connectionEtag) + } + + db.close() + if (ActivityCompat.checkSelfPermission( + applicationContext, + Manifest.permission.POST_NOTIFICATIONS + ) == PackageManager.PERMISSION_GRANTED + ) { + notificationBuilder + .setContentText("") + .setContentTitle(applicationContext.getString(R.string.finished_updating_geocoding_data)) + .setProgress(100, 100, false) + NotificationManagerCompat.from(applicationContext).notify(0, notificationBuilder.build()) + } + return Result.success() + } catch (e: Exception) { + e.printStackTrace() + if (ActivityCompat.checkSelfPermission( + applicationContext, + Manifest.permission.POST_NOTIFICATIONS + ) == PackageManager.PERMISSION_GRANTED + ) { + notificationBuilder + .setContentText("") + .setContentTitle(applicationContext.getString(R.string.updating_geocoding_data_failed)) + .setProgress(100, 100, false) + NotificationManagerCompat.from(applicationContext).notify(0, notificationBuilder.build()) + } + return Result.failure() + } + } +} \ No newline at end of file diff --git a/app/src/main/java/xyz/apiote/bimba/czwek/settings/SettingsActivity.kt b/app/src/main/java/xyz/apiote/bimba/czwek/settings/SettingsActivity.kt index be458c29a7f4d36e99f1e0d0189c4eb7879aa16e..9b5fc9f9d366eacf9d9bcbeee9f5dc7e38268986 100644 --- a/app/src/main/java/xyz/apiote/bimba/czwek/settings/SettingsActivity.kt +++ b/app/src/main/java/xyz/apiote/bimba/czwek/settings/SettingsActivity.kt @@ -1,6 +1,8 @@ package xyz.apiote.bimba.czwek.settings +import android.content.Context import android.os.Bundle +import android.util.Log import android.view.View import android.view.ViewGroup import androidx.activity.enableEdgeToEdge @@ -8,8 +10,22 @@ import androidx.appcompat.app.AppCompatActivity import androidx.core.view.ViewCompat import androidx.core.view.WindowInsetsCompat import androidx.core.view.updateLayoutParams +import androidx.preference.Preference import androidx.preference.PreferenceFragmentCompat +import androidx.preference.PreferenceManager +import androidx.preference.SwitchPreferenceCompat +import androidx.work.ExistingWorkPolicy +import androidx.work.OneTimeWorkRequestBuilder +import androidx.work.OutOfQuotaPolicy +import androidx.work.WorkInfo +import androidx.work.WorkManager import xyz.apiote.bimba.czwek.R +import java.time.Instant +import java.time.ZoneId +import java.time.ZonedDateTime +import java.time.format.DateTimeFormatter.RFC_1123_DATE_TIME +import java.util.concurrent.ExecutionException + class SettingsActivity : AppCompatActivity() { @@ -24,7 +40,6 @@ .replace(R.id.settings, SettingsFragment()) .commit() } supportActionBar?.setDisplayHomeAsUpEnabled(true) - // TODO insets val root = findViewById<View>(R.id.settings) @@ -40,6 +55,52 @@ class SettingsFragment : PreferenceFragmentCompat() { override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) { setPreferencesFromResource(R.xml.root_preferences, rootKey) + + findPreference<Preference>("download_cities_list")?.setOnPreferenceClickListener { + val request = OneTimeWorkRequestBuilder<DownloadCitiesWorker>() + .setExpedited(OutOfQuotaPolicy.RUN_AS_NON_EXPEDITED_WORK_REQUEST) + .build() + WorkManager.getInstance(requireContext()) + .enqueueUniqueWork("download_cities", ExistingWorkPolicy.KEEP, request) + findPreference<Preference>("download_cities_list")?.isEnabled = false + true + } + + if (isWorkScheduled(requireContext(), "download_cities")) { + findPreference<Preference>("download_cities_list")?.isEnabled = false // TODO more reliable + } + + val citiesLastUpdate = PreferenceManager.getDefaultSharedPreferences(requireContext()) + .getLong("cities_last_update", -1) + Log.i("geocoding", "$citiesLastUpdate") + if (citiesLastUpdate > 0) { + // TODO localise + val lastUpdateTime = + ZonedDateTime.ofInstant(Instant.ofEpochSecond(citiesLastUpdate), ZoneId.systemDefault()) + .format(RFC_1123_DATE_TIME) + findPreference<SwitchPreferenceCompat>("autoupdate_cities_list")?.summary = + "Last update $lastUpdateTime" + } + } + + private fun isWorkScheduled(context: Context, name: String): Boolean { + val instance = WorkManager.getInstance(context) + val statuses = instance.getWorkInfosForUniqueWork(name) + try { + var running = false + val workInfoList = statuses.get() + for (workInfo in workInfoList) { + val state: WorkInfo.State = workInfo.state + running = (state == WorkInfo.State.RUNNING) or (state == WorkInfo.State.ENQUEUED) + } + return running + } catch (e: ExecutionException) { + e.printStackTrace() + return false + } catch (e: InterruptedException) { + e.printStackTrace() + return false + } } } } \ No newline at end of file diff --git a/app/src/main/res/drawable/download.xml b/app/src/main/res/drawable/download.xml new file mode 100644 index 0000000000000000000000000000000000000000..287b023df7d8e7aceb629b78d32c3048d63636aa --- /dev/null +++ b/app/src/main/res/drawable/download.xml @@ -0,0 +1,16 @@ +<!-- +SPDX-FileCopyrightText: Google + +SPDX-License-Identifier: Apache-2.0 +--> + +<vector xmlns:android="http://schemas.android.com/apk/res/android" + android:width="24dp" + android:height="24dp" + android:tint="?attr/colorOnSurface" + android:viewportWidth="24" + android:viewportHeight="24"> + <path + android:fillColor="@android:color/white" + android:pathData="M5,20h14v-2H5V20zM19,9h-4V3H9v6H5l7,7L19,9z" /> +</vector> diff --git a/app/src/main/res/drawable/download_black.xml b/app/src/main/res/drawable/download_black.xml deleted file mode 100644 index 460eafe98b28c5f4230ff2d599cd098d373acb67..0000000000000000000000000000000000000000 --- a/app/src/main/res/drawable/download_black.xml +++ /dev/null @@ -1,11 +0,0 @@ -<!-- -SPDX-FileCopyrightText: Google - -SPDX-License-Identifier: Apache-2.0 ---> - -<vector android:height="24dp" android:tint="#000000" - android:viewportHeight="24" android:viewportWidth="24" - android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android"> - <path android:fillColor="@android:color/white" android:pathData="M5,20h14v-2H5V20zM19,9h-4V3H9v6H5l7,7L19,9z"/> -</vector> diff --git a/app/src/main/res/drawable/geocoding.xml b/app/src/main/res/drawable/geocoding.xml new file mode 100644 index 0000000000000000000000000000000000000000..79eaee48d8d2f42d73cb6bdb8e0ae88ff8183512 --- /dev/null +++ b/app/src/main/res/drawable/geocoding.xml @@ -0,0 +1,15 @@ +<!-- +SPDX-FileCopyrightText: Google + +SPDX-License-Identifier: Apache-2.0 +--> +<vector xmlns:android="http://schemas.android.com/apk/res/android" + android:width="24dp" + android:height="24dp" + android:tint="?attr/colorOnSurface" + android:viewportWidth="960" + android:viewportHeight="960"> + <path + android:fillColor="@android:color/white" + android:pathData="M480,880Q397,880 324,848.5Q251,817 197,763Q143,709 111.5,636Q80,563 80,480Q80,397 111.5,324Q143,251 197,197Q251,143 324,111.5Q397,80 480,80Q626,80 735.5,171.5Q845,263 872,401L790,401Q771,328 721.5,270.5Q672,213 600,184L600,200Q600,233 576.5,256.5Q553,280 520,280L440,280L440,360Q440,377 428.5,388.5Q417,400 400,400L320,400L320,480L400,480L400,600L360,600L168,408Q165,426 162.5,444Q160,462 160,480Q160,611 252,705Q344,799 480,800L480,880ZM844,860L716,732Q695,744 671,752Q647,760 620,760Q545,760 492.5,707.5Q440,655 440,580Q440,505 492.5,452.5Q545,400 620,400Q695,400 747.5,452.5Q800,505 800,580Q800,607 792,631Q784,655 772,676L900,804L844,860ZM620,680Q662,680 691,651Q720,622 720,580Q720,538 691,509Q662,480 620,480Q578,480 549,509Q520,538 520,580Q520,622 549,651Q578,680 620,680Z" /> +</vector> \ No newline at end of file diff --git a/app/src/main/res/drawable/sync.xml b/app/src/main/res/drawable/sync.xml new file mode 100644 index 0000000000000000000000000000000000000000..81dbed5fa3d03152beba444b8f9712edee421b84 --- /dev/null +++ b/app/src/main/res/drawable/sync.xml @@ -0,0 +1,17 @@ +<!-- +SPDX-FileCopyrightText: Google + +SPDX-License-Identifier: Apache-2.0 +--> +<vector xmlns:android="http://schemas.android.com/apk/res/android" + android:width="24dp" + android:height="24dp" + android:tint="?attr/colorOnSurface" + android:viewportWidth="24" + android:viewportHeight="24"> + + <path + android:fillColor="@android:color/white" + android:pathData="M12,4L12,1L8,5l4,4L12,6c3.31,0 6,2.69 6,6 0,1.01 -0.25,1.97 -0.7,2.8l1.46,1.46C19.54,15.03 20,13.57 20,12c0,-4.42 -3.58,-8 -8,-8zM12,18c-3.31,0 -6,-2.69 -6,-6 0,-1.01 0.25,-1.97 0.7,-2.8L5.24,7.74C4.46,8.97 4,10.43 4,12c0,4.42 3.58,8 8,8v3l4,-4 -4,-4v3z" /> + +</vector> diff --git a/app/src/main/res/layout/preferences_switch_material.xml b/app/src/main/res/layout/preferences_switch_material.xml new file mode 100644 index 0000000000000000000000000000000000000000..9bd2f24892abc6f705c6ce92d4392c206e2a9855 --- /dev/null +++ b/app/src/main/res/layout/preferences_switch_material.xml @@ -0,0 +1,16 @@ +<?xml version="1.0" encoding="utf-8"?> + +<!-- +SPDX-FileCopyrightText: Adam Evyčędo + +SPDX-License-Identifier: GPL-3.0-or-later +--> + +<!-- Derived from https://github.com/androidx/androidx/blob/005e9694795cee9a42375d80b0d813af9e700ac1/preference/preference/res/layout/preference_widget_switch_compat.xml --> +<com.google.android.material.materialswitch.MaterialSwitch xmlns:android="http://schemas.android.com/apk/res/android" + android:id="@+id/switchWidget" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:background="@null" + android:clickable="false" + android:focusable="false" /> \ No newline at end of file diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 9318bde41054eae756a82ecd5ff0e04c004189af..46b385efeb43d4d3eedd555983a0cc5383a1fe59 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -6,7 +6,7 @@ --> <!-- NOTE base strings are in en-GB --> -<resources> +<resources xmlns:tools="http://schemas.android.com/tools" tools:ignore="MissingTranslation"> <string name="app_name">Bimba</string> <string name="title_home">Home</string> <string name="title_map">Map</string> @@ -163,7 +163,7 @@ Stop on demand <string name="stop_stub_in_zone">Stop in zone %1$s</string> <string name="vehicle_headsign_content_description">%1$s towards %2$s</string> <string name="departure_headsign">» %1$s</string> - <string name="credits">Font yellowcircle8 (https://git.apiote.xyz/yellowcircle8.git) based on Railway Sans © Greg Fleming, OFL-1.1 https://github.com/davelab6/Railway-Sans\n\n Mastodon icon (https://github.com/mastodon/joinmastodon) © Mastodon contributors, AGPL-3.0-or-later\n\n Bimba logo created by https://github.com/tebriz159\n\n Material icons © Google, Apache-2.0\n\n Map data © OpenStreetMap contributors (https://www.openstreetmap.org/copyright), ODbL-1.0</string> + <string name="credits">Font yellowcircle8 (https://git.apiote.xyz/yellowcircle8.git) based on Railway Sans © Greg Fleming, OFL-1.1 https://github.com/davelab6/Railway-Sans\n\n Mastodon icon (https://github.com/mastodon/joinmastodon) © Mastodon contributors, AGPL-3.0-or-later\n\n Bimba logo created by https://github.com/tebriz159\n\n Material icons © Google, Apache-2.0\n\n Map data © OpenStreetMap contributors (https://www.openstreetmap.org/copyright), ODbL-1.0\n\n Cities list used for geocoding short plus codes © Geonames (https://geonames.org), CC BY</string> <string name="title_about">About</string> <string name="translation_button_description">link to translations service</string> <string name="app_description">FLOSS public transport passenger companion; a timetable in your pocket.</string> @@ -208,4 +208,14 @@US Customary <string name="units_tgm10">TGM (base 10)</string> <string name="units_tgm12">TGM (base 12)</string> <string name="title_settings">Settings</string> + <string name="no_geocoding_data">No geocoding data</string> + <string name="no_geocoding_data_description">The query contains a short plus code but there is no geocoding data present. Download geocoding data or enable auto updating in settings.</string> + <string name="error_geocoding">City not found</string> + <string name="cities_channel_name">Cities update channel</string> + <string name="cities_channel_description">Notifications showing progress of updating geocoding local data</string> + <string name="saving_cities_list">saving cities list</string> + <string name="updating_geocoding_data">Updating geocoding data</string> + <string name="downloading_cities_list">downloading cities list</string> + <string name="finished_updating_geocoding_data">Finished updating geocoding data</string> + <string name="updating_geocoding_data_failed">Updating geocoding data failed</string> </resources> diff --git a/app/src/main/res/values/themes.xml b/app/src/main/res/values/themes.xml index 2ed4f97f64d1cafcc78e7558ca4486830dc8c3ea..042048b10e8ca72c5956032013ac943358cfae06 100644 --- a/app/src/main/res/values/themes.xml +++ b/app/src/main/res/values/themes.xml @@ -43,7 +43,7 @@- .2
</style> <declare-styleable name="Theme.Bimba"> - <attr name="randomColourLightness" format="float"/> + <attr name="randomColourLightness" format="float" /> <attr name="lightStatusBar" format="boolean" /> </declare-styleable> @@ -73,5 +73,9 @@ <style name="roundedImageView" parent=""> <item name="cornerFamily">rounded</item> <item name="cornerSize">24dp</item> + </style> + + <style name="Preference.SwitchPreferenceCompat" parent="@style/Preference.SwitchPreferenceCompat.Material" tool:ignore="ResourceCycle"> + <item name="widgetLayout">@layout/preferences_switch_material</item> </style> </resources> \ No newline at end of file diff --git a/app/src/main/res/values-night/themes.xml b/app/src/main/res/values-night/themes.xml index e97857fb100d4f924cc140f36f4fb2c5e36955e1..18cc0e701931c44646e7e0c5b028a021e85a355c 100644 --- a/app/src/main/res/values-night/themes.xml +++ b/app/src/main/res/values-night/themes.xml @@ -51,4 +51,9 @@