Author: Adam Pioterek <adam.pioterek@protonmail.ch>
downloading timetable
%!v(PANIC=String method: strings: negative Repeat count)
diff --git a/app/build.gradle b/app/build.gradle index 2e38408235a731b14219e2edd70096a7be39d188..2088d6ce90688cb51c30cd965bbd14dacfd8da5b 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -36,7 +36,6 @@ implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version" implementation 'com.github.arimorty:floatingsearchview:2.1.1' implementation 'com.google.code.gson:gson:2.8.1' implementation 'com.squareup.okhttp3:okhttp:3.8.1' - implementation 'com.github.ghost1372:Mzip-Android:0.4.0' implementation 'io.requery:sqlite-android:3.22.0' implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-core:0.22.5' implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:0.22.5" diff --git a/app/src/main/java/ml/adamsprogs/bimba/activities/DashActivity.kt b/app/src/main/java/ml/adamsprogs/bimba/activities/DashActivity.kt index 29d8b1cecf62d180a40890da4190c8f48404098f..683b8373f85d34425892e6e9e46d403f01826df5 100644 --- a/app/src/main/java/ml/adamsprogs/bimba/activities/DashActivity.kt +++ b/app/src/main/java/ml/adamsprogs/bimba/activities/DashActivity.kt @@ -23,6 +23,8 @@ import ml.adamsprogs.bimba.models.suggestions.* import com.arlib.floatingsearchview.FloatingSearchView import com.arlib.floatingsearchview.suggestions.model.SearchSuggestion +import ml.adamsprogs.bimba.collections.FavouriteStorage +import ml.adamsprogs.bimba.models.adapters.FavouritesAdapter //todo<p:1> searchView integration //todo something devours RAM @@ -275,7 +277,6 @@ } override fun onTimetableDownload(result: String?) { val message: String = when (result) { - TimetableDownloader.RESULT_DOWNLOADED -> getString(R.string.refreshing_cache) TimetableDownloader.RESULT_NO_CONNECTIVITY -> getString(R.string.no_connectivity) TimetableDownloader.RESULT_UP_TO_DATE -> getString(R.string.timetable_up_to_date) TimetableDownloader.RESULT_FINISHED -> getString(R.string.timetable_downloaded) diff --git a/app/src/main/java/ml/adamsprogs/bimba/activities/EditFavouriteActivity.kt b/app/src/main/java/ml/adamsprogs/bimba/activities/EditFavouriteActivity.kt index 06d645b1b8e90386e934e7ed8b43289a5553cdf3..372f6448099ab199590f3e2e2241039aff3c3af1 100644 --- a/app/src/main/java/ml/adamsprogs/bimba/activities/EditFavouriteActivity.kt +++ b/app/src/main/java/ml/adamsprogs/bimba/activities/EditFavouriteActivity.kt @@ -7,8 +7,8 @@ import android.support.v7.widget.LinearLayoutManager import android.widget.EditText import ml.adamsprogs.bimba.R import ml.adamsprogs.bimba.models.Favourite -import ml.adamsprogs.bimba.models.FavouriteEditRowAdapter -import ml.adamsprogs.bimba.models.FavouriteStorage +import ml.adamsprogs.bimba.models.adapters.FavouriteEditRowAdapter +import ml.adamsprogs.bimba.collections.FavouriteStorage import kotlinx.android.synthetic.main.activity_edit_favourite.* import android.app.Activity import android.content.Intent diff --git a/app/src/main/java/ml/adamsprogs/bimba/activities/NoDbActivity.kt b/app/src/main/java/ml/adamsprogs/bimba/activities/NoDbActivity.kt index 5bd0095df723e6c3629556cd2b2a0f41debc5caa..91c215fc7f8a067541bcc9c03258c8f4ecd07ba7 100644 --- a/app/src/main/java/ml/adamsprogs/bimba/activities/NoDbActivity.kt +++ b/app/src/main/java/ml/adamsprogs/bimba/activities/NoDbActivity.kt @@ -77,8 +77,6 @@ networkStateReceiver.removeOnConnectivityChangeListener(this) startActivity(Intent(this, DashActivity::class.java)) finish() } - TimetableDownloader.RESULT_DOWNLOADED -> - no_db_caption.text = getString(R.string.timetable_converting) else -> no_db_caption.text = getString(R.string.error_try_later) } } diff --git a/app/src/main/java/ml/adamsprogs/bimba/activities/StopActivity.kt b/app/src/main/java/ml/adamsprogs/bimba/activities/StopActivity.kt index d5c41453695eee6011f0541d8028e1a3a4afda8a..de7311f4afc699144bab997b1efed17f81366798 100644 --- a/app/src/main/java/ml/adamsprogs/bimba/activities/StopActivity.kt +++ b/app/src/main/java/ml/adamsprogs/bimba/activities/StopActivity.kt @@ -13,9 +13,11 @@ import java.util.Calendar import kotlinx.android.synthetic.main.activity_stop.* import ml.adamsprogs.bimba.* +import ml.adamsprogs.bimba.collections.FavouriteStorage import ml.adamsprogs.bimba.datasources.* import ml.adamsprogs.bimba.models.gtfs.AgencyAndId import ml.adamsprogs.bimba.models.* +import ml.adamsprogs.bimba.models.adapters.DeparturesAdapter class StopActivity : AppCompatActivity(), MessageReceiver.OnTimetableDownloadListener, MessageReceiver.OnVmListener, Favourite.OnVmPreparedListener { @@ -170,7 +172,6 @@ private fun ticked() = Calendar.getInstance().timeInMillis - lastUpdated >= VmClient.TICK_6_ZINA_TIM_WITH_MARGIN override fun onTimetableDownload(result: String?) { val message: String = when (result) { - TimetableDownloader.RESULT_DOWNLOADED -> getString(R.string.refreshing_cache) TimetableDownloader.RESULT_NO_CONNECTIVITY -> getString(R.string.no_connectivity) TimetableDownloader.RESULT_UP_TO_DATE -> getString(R.string.timetable_up_to_date) TimetableDownloader.RESULT_FINISHED -> getString(R.string.timetable_downloaded) diff --git a/app/src/main/java/ml/adamsprogs/bimba/collections/FavouriteStorage.kt b/app/src/main/java/ml/adamsprogs/bimba/collections/FavouriteStorage.kt new file mode 100644 index 0000000000000000000000000000000000000000..83d651369fecf9bf9e7798cc12a262885ea6e2f1 --- /dev/null +++ b/app/src/main/java/ml/adamsprogs/bimba/collections/FavouriteStorage.kt @@ -0,0 +1,180 @@ +package ml.adamsprogs.bimba.collections + +import android.content.* +import com.google.gson.* +import ml.adamsprogs.bimba.* +import ml.adamsprogs.bimba.models.* +import ml.adamsprogs.bimba.models.gtfs.AgencyAndId +import java.util.Calendar + + +class FavouriteStorage private constructor(context: Context) : Iterable<Favourite> { + companion object { + private var favouriteStorage: FavouriteStorage? = null + fun getFavouriteStorage(context: Context? = null): FavouriteStorage { + return if (favouriteStorage == null) { + if (context == null) + throw IllegalArgumentException("requested new storage appContext not given") + else { + favouriteStorage = FavouriteStorage(context) + favouriteStorage as FavouriteStorage + } + } else + favouriteStorage as FavouriteStorage + } + } + + val favourites = HashMap<String, Favourite>() + private val positionIndex = IndexableTreeSet<String>() + private val preferences: SharedPreferences = context.getSharedPreferences("ml.adamsprogs.bimba.prefs", Context.MODE_PRIVATE) + + init { + val favouritesString = preferences.getString("favourites", "{}") + val favouritesMap = Gson().fromJson(favouritesString, JsonObject::class.java) + for ((name, jsonTimetables) in favouritesMap.entrySet()) { + val timetables = HashSet<StopSegment>() + jsonTimetables.asJsonArray.mapTo(timetables) { + val stopSegment = StopSegment(AgencyAndId(it.asJsonObject["stop"].asString), null) + val plates = HashSet<Plate.ID>() + it.asJsonObject["plates"].asJsonArray.mapTo(plates) { + Plate.ID(AgencyAndId(it.asJsonObject["line"].asString), + AgencyAndId(it.asJsonObject["stop"].asString), + it.asJsonObject["headsign"].asString) + } + stopSegment.plates = plates + stopSegment + } + favourites[name] = Favourite(name, timetables) + positionIndex.add(name) + } + } + + override fun iterator(): Iterator<Favourite> = favourites.values.iterator() + + fun has(name: String): Boolean = favourites.contains(name) + + fun add(name: String, timetables: HashSet<StopSegment>) { + if (favourites[name] == null) { + favourites[name] = Favourite(name, timetables) + addIndex(name) + serialize() + } + } + + fun add(name: String, favourite: Favourite) { + if (favourites[name] == null) { + favourites[name] = favourite + addIndex(name) + serialize() + } + } + + private fun addIndex(name:String) { + positionIndex.apply { + this.add(name) + } + } + + fun delete(name: String) { + favourites.remove(name) + positionIndex.remove(name) + serialize() + } + + fun delete(name: String, plate: Plate.ID) { + favourites[name]?.delete(plate) + serialize() + } + + private fun serialize() { + val rootObject = JsonObject() + for ((name, favourite) in favourites) { + val timetables = JsonArray() + for (timetable in favourite.segments) { + val segment = JsonObject() + segment.addProperty("stop", timetable.stop.id) + val plates = JsonArray() + for (plate in timetable.plates ?: HashSet()) { + val element = JsonObject() + element.addProperty("stop", plate.stop.id) + element.addProperty("line", plate.line.id) + element.addProperty("headsign", plate.headsign) + plates.add(element) + } + segment.add("plates", plates) + timetables.add(segment) + } + rootObject.add(name, timetables) + } + val favouritesString = Gson().toJson(rootObject) + val editor = preferences.edit() + editor.putString("favourites", favouritesString) + editor.apply() + + } + + fun merge(names: List<String>) { + if (names.size < 2) + return + + val newCache = HashMap<AgencyAndId, ArrayList<Departure>>() + names.forEach { + favourites[it]!!.fullDepartures.forEach { + if (newCache[it.key] == null) + newCache[it.key] = ArrayList() + newCache[it.key]!!.addAll(it.value) + } + } + val now = Calendar.getInstance().secondsAfterMidnight() + newCache.forEach { + it.value.sortBy { it.timeTill(now) } + } + val newFavourite = Favourite(names[0], HashSet(), newCache) + for (name in names) { + newFavourite.segments.addAll(favourites[name]!!.segments) + favourites.remove(name) + positionIndex.remove(name) + } + favourites[names[0]] = newFavourite + addIndex(names[0]) + + serialize() + } + + fun rename(oldName: String, newName: String) { + val favourite = favourites[oldName] ?: return + favourite.rename(newName) + favourites.remove(oldName) + positionIndex.remove(oldName) + favourites[newName] = favourite + addIndex(newName) + serialize() + } + + fun registerOnVm(receiver: MessageReceiver, context: Context) { + favourites.values.forEach { + it.registerOnVm(receiver, context) + } + } + + fun deregisterOnVm(receiver: MessageReceiver, context: Context) { + favourites.values.forEach { + it.deregisterOnVm(receiver, context) + } + } + + operator fun get(name: String): Favourite? { + return favourites[name] + } + + operator fun get(position: Int): Favourite? { + return favourites[positionIndex[position]] + } + + fun indexOf(name: String): Int { + return positionIndex.indexOf(name) + } + + val size + get() = favourites.size +} \ No newline at end of file diff --git a/app/src/main/java/ml/adamsprogs/bimba/collections/IndexableTreeSet.kt b/app/src/main/java/ml/adamsprogs/bimba/collections/IndexableTreeSet.kt new file mode 100644 index 0000000000000000000000000000000000000000..19719f1a61a394bd4cf61dcd35e09ac21a0dc08a --- /dev/null +++ b/app/src/main/java/ml/adamsprogs/bimba/collections/IndexableTreeSet.kt @@ -0,0 +1,11 @@ +package ml.adamsprogs.bimba.collections + +import java.util.TreeSet + +class IndexableTreeSet<T>: TreeSet<T>() { + operator fun get(position: Int): T { + @Suppress("UNCHECKED_CAST") + return this.toArray()[position] as T + } + +} \ No newline at end of file diff --git a/app/src/main/java/ml/adamsprogs/bimba/datasources/TimetableDownloader.kt b/app/src/main/java/ml/adamsprogs/bimba/datasources/TimetableDownloader.kt index a81b3802419bfeb5d983673227868ee67099a198..91c9cb88d3acc22103c40c3c18880e557db2b309 100644 --- a/app/src/main/java/ml/adamsprogs/bimba/datasources/TimetableDownloader.kt +++ b/app/src/main/java/ml/adamsprogs/bimba/datasources/TimetableDownloader.kt @@ -1,28 +1,15 @@ package ml.adamsprogs.bimba.datasources import android.annotation.TargetApi -import android.app.IntentService -import android.app.Notification -import android.content.Context -import android.content.Intent +import android.app.* +import android.content.* import android.support.v4.app.NotificationCompat import java.io.* -import android.app.NotificationManager import android.os.Build -import com.google.gson.Gson -import com.google.gson.JsonArray -import com.google.gson.JsonObject -import ir.mahdi.mzip.zip.ZipArchive -import ml.adamsprogs.bimba.NetworkStateReceiver -import ml.adamsprogs.bimba.NotificationChannels -import ml.adamsprogs.bimba.R -import ml.adamsprogs.bimba.getSecondaryExternalFilesDir -import ml.adamsprogs.bimba.models.Timetable -import java.net.ConnectException -import java.net.URL -import java.util.Calendar +import ml.adamsprogs.bimba.* +import java.net.* +import java.util.zip.GZIPInputStream import javax.net.ssl.HttpsURLConnection -import kotlin.collections.* class TimetableDownloader : IntentService("TimetableDownloader") { companion object { @@ -31,12 +18,12 @@ const val EXTRA_FORCE = "force" const val EXTRA_RESULT = "result" const val RESULT_NO_CONNECTIVITY = "no connectivity" const val RESULT_UP_TO_DATE = "up-to-date" - const val RESULT_DOWNLOADED = "downloaded" const val RESULT_FINISHED = "finished" } private lateinit var notificationManager: NotificationManager - private var size: Int = 0 + private var sizeCompressed: Int = 0 + private var sizeUncompressed: Int = 0 override fun onHandleIntent(intent: Intent?) { @@ -48,14 +35,14 @@ sendResult(RESULT_NO_CONNECTIVITY) return } - //todo download timetable - sendResult(RESULT_UP_TO_DATE) - return + val localETag = prefs.getString("etag", "") val httpCon: HttpsURLConnection try { - val url = URL("https://adamsprogs.ml/gtfs") + val url = URL("https://adamsprogs.ml/gtfs") //todo if https fails -> http httpCon = url.openConnection() as HttpsURLConnection + httpCon.addRequestProperty("ETag", localETag) + httpCon.connect() if (httpCon.responseCode == HttpsURLConnection.HTTP_NOT_MODIFIED) { sendResult(RESULT_UP_TO_DATE) return @@ -75,44 +62,26 @@ sendResult(RESULT_NO_CONNECTIVITY) return } - val lastModified = httpCon.getHeaderField("Content-Disposition").split("=")[1].trim('\"').split("_")[0] - size = httpCon.getHeaderField("Content-Length").toInt() / 1024 + val newETag = httpCon.getHeaderField("ETag") + sizeCompressed = httpCon.getHeaderField("Content-Length").toInt() / 1024 + sizeUncompressed = httpCon.getHeaderField("X-Uncompressed-Content-Length").toInt() / 1024 - val force = intent.getBooleanExtra(EXTRA_FORCE, false) - val currentLastModified = prefs.getString("timetableLastModified", "19791012") - if (lastModified <= currentLastModified && !force) { - sendResult(RESULT_UP_TO_DATE) - return - } + notify(0, R.string.timetable_downloading, R.string.timetable_uncompressing, sizeCompressed, sizeUncompressed) + - notify(0, getString(R.string.timetable_downloading), size) + val gtfsDb = File(getSecondaryExternalFilesDir(), "timetable.db") + val inputStream = httpCon.inputStream + val gzipInputStream = GZIPInputStream(inputStream) + val outputStream = FileOutputStream(gtfsDb) + + gzipInputStream.listenableCopyTo(outputStream) { + notify((it / 1024).toInt(), R.string.timetable_downloading, R.string.timetable_uncompressing, sizeCompressed, sizeUncompressed) + } - val gtfs = File(getSecondaryExternalFilesDir(), "timetable.zip") - copyInputStreamToFile(httpCon.inputStream, gtfs) val prefsEditor = prefs.edit() - prefsEditor.putString("timetableLastModified", lastModified) + prefsEditor.putString("etag", newETag) prefsEditor.apply() - sendResult(RESULT_DOWNLOADED) - - notify(getString(R.string.timetable_converting)) - - val target = File(getSecondaryExternalFilesDir(), "gtfs_files") - target.deleteRecursively() - target.mkdir() - ZipArchive.unzip(gtfs.path, target.path, "") - - val string = getString(R.string.timetable_converting) - notify(0, string, 1_030_000) - - println(Calendar.getInstance().timeInMillis) - - gtfs.delete() - - createIndices() - Timetable.getTimetable(this).refresh() - - println(Calendar.getInstance().timeInMillis) cancelNotification() @@ -120,73 +89,6 @@ sendResult(RESULT_FINISHED) } } - private fun createIndices() { - /*val settings = CsvParserSettings() - settings.format.setLineSeparator("\r\n") - settings.format.quote = '"' - settings.isHeaderExtractionEnabled = true - - val parser = CsvParser(settings) - - val stopIndexFile = File(getSecondaryExternalFilesDir(), "gtfs_files/stop_index.yml") - val tripIndexFile = File(getSecondaryExternalFilesDir(), "gtfs_files/trip_index.yml") - - val stopsIndex = HashMap<String, List<Long>>() - val tripsIndex = HashMap<String, List<Long>>() - - parser.parseAll(File(getSecondaryExternalFilesDir(), "gtfs_files/trips.txt")).forEach { - tripsIndex[it[2]] = ArrayList() - } - - parser.parseAll(File(getSecondaryExternalFilesDir(), "gtfs_files/stops.txt")).forEach { - stopsIndex[it[0]] = ArrayList() - } - - val string = getString(R.string.timetable_converting) - - parser.beginParsing(File(getSecondaryExternalFilesDir(), "gtfs_files/stop_times.txt")) - var line: Array<String>? = null - while ({ line = parser.parseNext(); line }() != null) { - val lineNumber = parser.appContext.currentLine() - (tripsIndex[line!![0]] as ArrayList).add(lineNumber) - (stopsIndex[line!![3]] as ArrayList).add(lineNumber) - if (lineNumber % 10_300 == 0L) - notify(lineNumber.toInt(), string, 1_030_000) - } - - println(Calendar.getInstance().timeInMillis) - stopsIndex.filter { it.value.contains(0) }.forEach { println("${it.key}: ${it.value.joinToString()}") } - println(Calendar.getInstance().timeInMillis) - - serialiseIndex(stopsIndex, stopIndexFile) - serialiseIndex(tripsIndex, tripIndexFile) - */ - } - - private fun serialiseIndex(index: HashMap<String, List<Long>>, file: File) { - val stopsRootObject = JsonObject() - index.forEach { - val stop = JsonArray() - it.value.forEach { - stop.add(it) - } - stopsRootObject.add(it.key, stop) - } - - val writer = BufferedWriter(file.writer()) - writer.write(Gson().toJson(stopsRootObject)) - writer.close() - } - - private fun today(): String { - val cal = Calendar.getInstance() - val d = cal[Calendar.DAY_OF_MONTH] - val m = cal[Calendar.MONTH] + 1 - val y = cal[Calendar.YEAR] - - return "%d%02d%02d".format(y, m, d) - } - private fun sendResult(result: String) { val broadcastIntent = Intent() broadcastIntent.action = ACTION_DOWNLOADED @@ -195,19 +97,23 @@ broadcastIntent.putExtra(EXTRA_RESULT, result) sendBroadcast(broadcastIntent) } - private fun notify(progress: Int, message: String, max: Int) { + private fun notify(progress: Int, titleId: Int, messageId: Int, sizeCompressed: Int, sizeUncompressed: Int) { + val quotient = sizeCompressed.toFloat() / sizeUncompressed.toFloat() + val message = getString(messageId, Math.max(progress * quotient, sizeCompressed.toFloat()), + sizeCompressed, (progress.toFloat() / sizeUncompressed.toFloat()) * 100) + val title = getString(titleId) if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) - notifyCompat(progress, message, max) + notifyCompat(progress, title, message, sizeUncompressed) else - notifyStandard(progress, message, max) + notifyStandard(progress, title, message, sizeUncompressed) } @Suppress("DEPRECATION") - private fun notifyCompat(progress: Int, message: String, max: Int) { + private fun notifyCompat(progress: Int, title: String, message: String, max: Int) { val builder = NotificationCompat.Builder(this) .setSmallIcon(R.drawable.ic_download) - .setContentTitle(message) - .setContentText("${(progress.toDouble() / max.toDouble() * 100).toInt()} %") + .setContentTitle(title) + .setContentText(message) .setCategory(NotificationCompat.CATEGORY_PROGRESS) .setOngoing(true) .setProgress(max, progress, false) @@ -215,74 +121,19 @@ notificationManager.notify(42, builder.build()) } @TargetApi(Build.VERSION_CODES.O) - private fun notifyStandard(progress: Int, message: String, max: Int) { + private fun notifyStandard(progress: Int, title: String, message: String, max: Int) { NotificationChannels.makeChannel(NotificationChannels.CHANNEL_UPDATES, "Updates", notificationManager) val builder = Notification.Builder(this, NotificationChannels.CHANNEL_UPDATES) .setSmallIcon(R.drawable.ic_download) - .setContentTitle(message) - .setContentText("${(progress.toDouble() / max.toDouble() * 100).toInt()} %") + .setContentTitle(title) + .setContentText(message) .setCategory(Notification.CATEGORY_PROGRESS) .setOngoing(true) .setProgress(max, progress, false) notificationManager.notify(42, builder.build()) } - private fun notify(message: String) { - if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) - notifyCompat(message) - else - notifyStandard(message) - - } - - @Suppress("DEPRECATION") - private fun notifyCompat(message: String) { - val builder = NotificationCompat.Builder(this) - .setSmallIcon(R.drawable.ic_download) - .setContentTitle(message) - .setContentText("") - .setCategory(NotificationCompat.CATEGORY_PROGRESS) - .setOngoing(true) - .setProgress(0, 0, true) - notificationManager.notify(42, builder.build()) - } - - @TargetApi(Build.VERSION_CODES.O) - private fun notifyStandard(message: String) { - NotificationChannels.makeChannel(NotificationChannels.CHANNEL_UPDATES, "Updates", notificationManager) - val builder = Notification.Builder(this, NotificationChannels.CHANNEL_UPDATES) - .setSmallIcon(R.drawable.ic_download) - .setContentTitle(message) - .setContentText("") - .setCategory(Notification.CATEGORY_PROGRESS) - .setOngoing(true) - .setProgress(0, 0, true) - notificationManager.notify(42, builder.build()) - } - private fun cancelNotification() { notificationManager.cancel(42) - } - - private fun copyInputStreamToFile(ins: InputStream, file: File) { - try { - val out = FileOutputStream(file) - val buf = ByteArray(5 * 1024) - var lenSum = 0.0f - var len = 42 - while (len > 0) { - len = ins.read(buf) - if (len <= 0) - break - out.write(buf, 0, len) - lenSum += len.toFloat() / 1024.0f - notify(lenSum.toInt(), getString(R.string.timetable_downloading), size) - } - out.close() - } catch (e: Exception) { - e.printStackTrace() - } finally { - ins.close() - } } } diff --git a/app/src/main/java/ml/adamsprogs/bimba/extensions.kt b/app/src/main/java/ml/adamsprogs/bimba/extensions.kt index 936c1475dcc3dff606bfe123ed1fae3ad14e7b1f..869d2e6b5201d15a2da4f46ef93f9fce2111227c 100644 --- a/app/src/main/java/ml/adamsprogs/bimba/extensions.kt +++ b/app/src/main/java/ml/adamsprogs/bimba/extensions.kt @@ -5,7 +5,7 @@ import android.content.Context import android.graphics.drawable.Drawable import android.os.Build import ml.adamsprogs.bimba.activities.StopActivity -import java.io.File +import java.io.* import java.text.SimpleDateFormat import java.util.* import kotlin.collections.ArrayList @@ -81,3 +81,16 @@ val dirs = this.getExternalFilesDirs(null) return dirs[0] // return dirs[dirs.size - 1] } + +internal fun InputStream.listenableCopyTo(out: OutputStream, bufferSize: Int = DEFAULT_BUFFER_SIZE, listener: (Long) -> Unit): Long { + var bytesCopied: Long = 0 + val buffer = ByteArray(bufferSize) + var bytes = read(buffer) + while (bytes >= 0) { + out.write(buffer, 0, bytes) + bytesCopied += bytes + listener(bytesCopied) + bytes = read(buffer) + } + return bytesCopied +} diff --git a/app/src/main/java/ml/adamsprogs/bimba/models/DeparturesAdapter.kt b/app/src/main/java/ml/adamsprogs/bimba/models/DeparturesAdapter.kt deleted file mode 100644 index 457e6b09f404b66d2aefad7d99d3950fa14bf685..0000000000000000000000000000000000000000 --- a/app/src/main/java/ml/adamsprogs/bimba/models/DeparturesAdapter.kt +++ /dev/null @@ -1,117 +0,0 @@ -package ml.adamsprogs.bimba.models - -import android.app.AlertDialog -import android.content.Context -import android.content.DialogInterface -import android.support.v4.content.res.ResourcesCompat -import android.support.v7.widget.RecyclerView -import android.view.View -import android.view.ViewGroup -import android.widget.ImageView -import android.widget.TextView -import ml.adamsprogs.bimba.R -import android.view.LayoutInflater -import ml.adamsprogs.bimba.Declinator -import ml.adamsprogs.bimba.rollTime -import java.util.* - -//todo<p:1> on click show time (HH:MM) -class DeparturesAdapter(val context: Context, private val departures: List<Departure>?, private val relativeTime: Boolean) : - RecyclerView.Adapter<DeparturesAdapter.ViewHolder>() { - - companion object { - const val VIEW_TYPE_LOADING: Int = 0 - const val VIEW_TYPE_CONTENT: Int = 1 - const val VIEW_TYPE_EMPTY: Int = 2 - } - -// init { -// departures?.forEach { -// println("${it.line} -> ${it.headsign} @${it.time} (${if (it.isModified) it.modification[0] else{} })") -// } -// } - - override fun getItemCount(): Int { - if (departures == null || departures.isEmpty()) - return 1 - return departures.size - } - - override fun getItemViewType(position: Int): Int { - return when { - departures == null -> VIEW_TYPE_EMPTY - departures.isEmpty() -> VIEW_TYPE_LOADING - else -> VIEW_TYPE_CONTENT - } - } - - override fun onBindViewHolder(holder: ViewHolder, position: Int) { - if (departures == null) { - return - } - val line = holder.lineTextView - val time = holder.timeTextView - val direction = holder.directionTextView - if (departures.isEmpty()) { - time.text = context.getString(R.string.no_departures) - return - } - val departure = departures[position] - //println("${departure.line} -> ${departure.headsign} @${departure.time} (${if (departure.isModified) departure.modification[0] else {}})") - val now = Calendar.getInstance() - val departureTime = Calendar.getInstance().rollTime(departure.time) - if (departure.tomorrow) - departureTime.add(Calendar.DAY_OF_MONTH, 1) - - val departureIn = ((departureTime.timeInMillis - now.timeInMillis) / (1000 * 60)).toInt() - val timeString: String - - timeString = if (departureIn > 60 || departureIn < 0 || !relativeTime) - context.getString(R.string.departure_at, "${String.format("%02d", departureTime.get(Calendar.HOUR_OF_DAY))}:${String.format("%02d", departureTime.get(Calendar.MINUTE))}") - else if (departureIn > 0 && !departure.onStop) - context.getString(Declinator.decline(departureIn), departureIn.toString()) - else - context.getString(R.string.now) - - line.text = departure.lineText - time.text = timeString - direction.text = context.getString(R.string.departure_to, departure.headsign) - val icon = holder.typeIcon - if (departure.vm) - icon.setImageDrawable(ResourcesCompat.getDrawable(context.resources, R.drawable.ic_departure_vm, context.theme)) - else - icon.setImageDrawable(ResourcesCompat.getDrawable(context.resources, R.drawable.ic_departure_timetable, context.theme)) - - if (departure.lowFloor) - holder.floorIcon.visibility = View.VISIBLE - if (departure.isModified) { - holder.infoIcon.visibility = View.VISIBLE - holder.root.setOnClickListener { - AlertDialog.Builder(context) - .setPositiveButton(context.getText(android.R.string.ok), - { dialog: DialogInterface, _: Int -> dialog.cancel() }) - .setCancelable(true) - .setMessage(departure.modification.joinToString("; ")) - .create().show() - } - } - } - - override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder { - val context = parent.context - val inflater = LayoutInflater.from(context) - - val rowView = inflater.inflate(R.layout.row_departure, parent, false) - return ViewHolder(rowView) - } - - inner class ViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) { - val root = itemView.findViewById<View>(R.id.departureRow)!! - val lineTextView: TextView = itemView.findViewById(R.id.lineNumber) - val timeTextView: TextView = itemView.findViewById(R.id.departureTime) - val directionTextView: TextView = itemView.findViewById(R.id.departureDirection) - val typeIcon: ImageView = itemView.findViewById(R.id.departureTypeIcon) - val infoIcon: ImageView = itemView.findViewById(R.id.departureInfoIcon) - val floorIcon: ImageView = itemView.findViewById(R.id.departureFloorIcon) - } -} \ No newline at end of file diff --git a/app/src/main/java/ml/adamsprogs/bimba/models/FavouriteEditRowAdapter.kt b/app/src/main/java/ml/adamsprogs/bimba/models/FavouriteEditRowAdapter.kt deleted file mode 100644 index 7e78913947deebee44524e88984d9e3d0b71f4bc..0000000000000000000000000000000000000000 --- a/app/src/main/java/ml/adamsprogs/bimba/models/FavouriteEditRowAdapter.kt +++ /dev/null @@ -1,49 +0,0 @@ -package ml.adamsprogs.bimba.models - -import android.support.v7.widget.RecyclerView -import android.view.LayoutInflater -import android.view.View -import android.view.ViewGroup -import android.widget.ImageView -import android.widget.TextView -import ml.adamsprogs.bimba.R - -class FavouriteEditRowAdapter(private var favourite: Favourite) : - RecyclerView.Adapter<FavouriteEditRowAdapter.ViewHolder>() { - override fun getItemCount(): Int { - return favourite.size - } - - override fun onBindViewHolder(holder: ViewHolder, position: Int) { - val timetable = Timetable.getTimetable() - val favourites = FavouriteStorage.getFavouriteStorage() - val id = favourite.segments.flatMap { it.plates!! }.sortedBy { "${it.line}${it.stop}"}[position] - val plate = Plate(id,null) - val favouriteElement = "${timetable.getStopName(plate.id.stop)} ( ${timetable.getStopCode(plate.id.stop)}):\n${plate.id.line} → ${plate.id.headsign}" - holder.rowTextView.text = favouriteElement -// holder?.splitButton?.setOnClickListener { -// favourites.detach(favourite.name, id, favouriteElement) -// favourite = favourites.favourites[favourite.name]!! -// notifyDataSetChanged() -// } - holder.deleteButton.setOnClickListener { - favourites.delete(favourite.name, id) - favourite = favourites.favourites[favourite.name]!! - notifyDataSetChanged() - } - } - - override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder { - val context = parent.context - val inflater = LayoutInflater.from(context) - - val rowView = inflater.inflate(R.layout.row_favourite_edit, parent, false) - return ViewHolder(rowView) - } - - inner class ViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) { - val rowTextView:TextView = itemView.findViewById(R.id.favourite_edit_row) -// val splitButton:ImageView = itemView.findViewById(R.id.favourite_edit_split) - val deleteButton:ImageView = itemView.findViewById(R.id.favourite_edit_delete) - } -} \ No newline at end of file diff --git a/app/src/main/java/ml/adamsprogs/bimba/models/FavouriteStorage.kt b/app/src/main/java/ml/adamsprogs/bimba/models/FavouriteStorage.kt deleted file mode 100644 index 65dee0ce3208e8e493b450ffc3d4fa8c1b80d51d..0000000000000000000000000000000000000000 --- a/app/src/main/java/ml/adamsprogs/bimba/models/FavouriteStorage.kt +++ /dev/null @@ -1,185 +0,0 @@ -package ml.adamsprogs.bimba.models - -import android.content.Context -import android.content.SharedPreferences -import com.google.gson.Gson -import com.google.gson.JsonArray -import com.google.gson.JsonObject -import ml.adamsprogs.bimba.MessageReceiver -import ml.adamsprogs.bimba.models.gtfs.AgencyAndId -import ml.adamsprogs.bimba.secondsAfterMidnight -import java.util.Calendar - - -class FavouriteStorage private constructor(context: Context) : Iterable<Favourite> { - companion object { - private var favouriteStorage: FavouriteStorage? = null - fun getFavouriteStorage(context: Context? = null): FavouriteStorage { - return if (favouriteStorage == null) { - if (context == null) - throw IllegalArgumentException("requested new storage appContext not given") - else { - favouriteStorage = FavouriteStorage(context) - favouriteStorage as FavouriteStorage - } - } else - favouriteStorage as FavouriteStorage - } - } - - val favourites = HashMap<String, Favourite>() - private val positionIndex = ArrayList<String>() - private val preferences: SharedPreferences = context.getSharedPreferences("ml.adamsprogs.bimba.prefs", Context.MODE_PRIVATE) - - init { - val favouritesString = preferences.getString("favourites", "{}") - val favouritesMap = Gson().fromJson(favouritesString, JsonObject::class.java) - for ((name, jsonTimetables) in favouritesMap.entrySet()) { - val timetables = HashSet<StopSegment>() - jsonTimetables.asJsonArray.mapTo(timetables) { - val stopSegment = StopSegment(AgencyAndId(it.asJsonObject["stop"].asString), null) - val plates = HashSet<Plate.ID>() - it.asJsonObject["plates"].asJsonArray.mapTo(plates) { - Plate.ID(AgencyAndId(it.asJsonObject["line"].asString), - AgencyAndId(it.asJsonObject["stop"].asString), - it.asJsonObject["headsign"].asString) - } - stopSegment.plates = plates - stopSegment - } - favourites[name] = Favourite(name, timetables) - positionIndex.add(name) - } - positionIndex.sort() - } - - override fun iterator(): Iterator<Favourite> = favourites.values.iterator() - - fun has(name: String): Boolean = favourites.contains(name) - - fun add(name: String, timetables: HashSet<StopSegment>) { - if (favourites[name] == null) { - favourites[name] = Favourite(name, timetables) - addIndex(name) - serialize() - } - } - - fun add(name: String, favourite: Favourite) { - if (favourites[name] == null) { - favourites[name] = favourite - addIndex(name) - serialize() - } - } - - private fun addIndex(name:String) { - positionIndex.apply { - this.add(name) - this.sort() - } - } - - fun delete(name: String) { - favourites.remove(name) - positionIndex.remove(name) - serialize() - } - - fun delete(name: String, plate: Plate.ID) { - favourites[name]?.delete(plate) - serialize() - } - - private fun serialize() { - val rootObject = JsonObject() - for ((name, favourite) in favourites) { - val timetables = JsonArray() - for (timetable in favourite.segments) { - val segment = JsonObject() - segment.addProperty("stop", timetable.stop.id) - val plates = JsonArray() - for (plate in timetable.plates ?: HashSet()) { - val element = JsonObject() - element.addProperty("stop", plate.stop.id) - element.addProperty("line", plate.line.id) - element.addProperty("headsign", plate.headsign) - plates.add(element) - } - segment.add("plates", plates) - timetables.add(segment) - } - rootObject.add(name, timetables) - } - val favouritesString = Gson().toJson(rootObject) - val editor = preferences.edit() - editor.putString("favourites", favouritesString) - editor.apply() - - } - - fun merge(names: List<String>) { - if (names.size < 2) - return - - val newCache = HashMap<AgencyAndId, ArrayList<Departure>>() - names.forEach { - favourites[it]!!.fullDepartures.forEach { - if (newCache[it.key] == null) - newCache[it.key] = ArrayList() - newCache[it.key]!!.addAll(it.value) - } - } - val now = Calendar.getInstance().secondsAfterMidnight() - newCache.forEach { - it.value.sortBy { it.timeTill(now) } - } - val newFavourite = Favourite(names[0], HashSet(), newCache) - for (name in names) { - newFavourite.segments.addAll(favourites[name]!!.segments) - favourites.remove(name) - positionIndex.remove(name) - } - favourites[names[0]] = newFavourite - addIndex(names[0]) - - serialize() - } - - fun rename(oldName: String, newName: String) { - val favourite = favourites[oldName] ?: return - favourite.rename(newName) - favourites.remove(oldName) - positionIndex.remove(oldName) - favourites[newName] = favourite - addIndex(newName) - serialize() - } - - fun registerOnVm(receiver: MessageReceiver, context: Context) { - favourites.values.forEach { - it.registerOnVm(receiver, context) - } - } - - fun deregisterOnVm(receiver: MessageReceiver, context: Context) { - favourites.values.forEach { - it.deregisterOnVm(receiver, context) - } - } - - operator fun get(name: String): Favourite? { - return favourites[name] - } - - operator fun get(position: Int): Favourite? { - return favourites[positionIndex[position]] - } - - fun indexOf(name: String): Int { - return positionIndex.indexOf(name) - } - - val size - get() = favourites.size -} \ No newline at end of file diff --git a/app/src/main/java/ml/adamsprogs/bimba/models/FavouritesAdapter.kt b/app/src/main/java/ml/adamsprogs/bimba/models/FavouritesAdapter.kt deleted file mode 100644 index 2643b27522224a6eab1474d0edbb427af0c8a72d..0000000000000000000000000000000000000000 --- a/app/src/main/java/ml/adamsprogs/bimba/models/FavouritesAdapter.kt +++ /dev/null @@ -1,144 +0,0 @@ -package ml.adamsprogs.bimba.models - -import android.content.Context -import android.support.v4.content.res.ResourcesCompat -import android.support.v7.widget.* -import android.support.v7.widget.PopupMenu -import android.util.SparseBooleanArray -import android.view.* -import android.widget.* -import ml.adamsprogs.bimba.R -import android.view.LayoutInflater -import kotlinx.coroutines.experimental.CommonPool -import kotlinx.coroutines.experimental.android.UI -import kotlinx.coroutines.experimental.async -import kotlinx.coroutines.experimental.launch -import java.util.* -import ml.adamsprogs.bimba.Declinator -import ml.adamsprogs.bimba.secondsAfterMidnight - - -class FavouritesAdapter(val appContext: Context, var favourites: FavouriteStorage, - private val onMenuItemClickListener: OnMenuItemClickListener, - private val onClickListener: ViewHolder.OnClickListener) : - RecyclerView.Adapter<FavouritesAdapter.ViewHolder>() { - - private val selectedItems = SparseBooleanArray() - - private fun isSelected(position: Int) = getSelectedItems().contains(position) - - fun toggleSelection(position: Int) { - if (selectedItems.get(position, false)) { - selectedItems.delete(position) - } else { - selectedItems.put(position, true) - } - notifyItemChanged(position) - } - - fun clearSelection() { - val selection = getSelectedItems() - selectedItems.clear() - for (i in selection) { - notifyItemChanged(i) - } - } - - fun getSelectedItemCount() = selectedItems.size() - - fun getSelectedItems(): List<Int> { - val items = ArrayList<Int>(selectedItems.size()) - (0 until selectedItems.size()).mapTo(items) { selectedItems.keyAt(it) } - return items - } - - override fun getItemCount() = favourites.size - - override fun onBindViewHolder(holder: ViewHolder, position: Int) { - launch(UI) { - val favourite = favourites[position]!! - holder.nameTextView.text = favourite.name - - holder.selectedOverlay.visibility = if (isSelected(position)) View.VISIBLE else View.INVISIBLE - holder.moreButton.setOnClickListener { - val popup = PopupMenu(appContext, it) - val inflater = popup.menuInflater - popup.setOnMenuItemClickListener { - when (it.itemId) { - R.id.favourite_edit -> onMenuItemClickListener.edit(favourite.name) - R.id.favourite_delete -> onMenuItemClickListener.delete(favourite.name) - else -> false - } - } - inflater.inflate(R.menu.favourite_actions, popup.menu) - popup.show() - } - - val nextDeparture = async(CommonPool) { - favourite.nextDeparture() - }.await() - - val nextDepartureText: String - val nextDepartureLineText: String - if (nextDeparture != null) { - val interval = nextDeparture.timeTill(Calendar.getInstance().secondsAfterMidnight()) - nextDepartureLineText = appContext.getString(R.string.departure_to_line, nextDeparture.line, nextDeparture.headsign) - nextDepartureText = if (interval < 0) - appContext.getString(R.string.just_departed) - else - appContext.getString(Declinator.decline(interval), interval.toString()) - } else { - nextDepartureText = appContext.getString(R.string.no_next_departure) - nextDepartureLineText = "" - } - holder.timeTextView.text = nextDepartureText - holder.lineTextView.text = nextDepartureLineText - if (nextDeparture != null) { - if (nextDeparture.vm) - holder.typeIcon.setImageDrawable(ResourcesCompat.getDrawable(appContext.resources, R.drawable.ic_departure_vm, appContext.theme)) - else - holder.typeIcon.setImageDrawable(ResourcesCompat.getDrawable(appContext.resources, R.drawable.ic_departure_timetable, appContext.theme)) - } - } - } - - override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder { - val context = parent.context - val inflater = LayoutInflater.from(context) - - val rowView = inflater.inflate(R.layout.row_favourite, parent, false) - return ViewHolder(rowView, onClickListener) - } - - class ViewHolder(itemView: View, private val listener: OnClickListener) : RecyclerView.ViewHolder(itemView), View.OnClickListener, View.OnLongClickListener { - override fun onLongClick(v: View?): Boolean { - return listener.onItemLongClicked(adapterPosition) - } - - override fun onClick(v: View?) { - listener.onItemClicked(adapterPosition) - } - - val selectedOverlay: View = itemView.findViewById(R.id.selected_overlay) - val nameTextView: TextView = itemView.findViewById(R.id.favourite_name) - val timeTextView: TextView = itemView.findViewById(R.id.favourite_time) - val lineTextView: TextView = itemView.findViewById(R.id.favourite_line) - val moreButton: ImageView = itemView.findViewById(R.id.favourite_more_button) - val typeIcon: ImageView = itemView.findViewById(R.id.departureTypeIcon) - - init { - itemView.setOnClickListener(this) - itemView.setOnLongClickListener(this) - } - - interface OnClickListener { - fun onItemClicked(position: Int) - fun onItemLongClicked(position: Int): Boolean - } - } - - interface OnMenuItemClickListener { - fun edit(name: String): Boolean - fun delete(name: String): Boolean - } -} \ No newline at end of file diff --git a/app/src/main/java/ml/adamsprogs/bimba/models/adapters/DeparturesAdapter.kt b/app/src/main/java/ml/adamsprogs/bimba/models/adapters/DeparturesAdapter.kt new file mode 100644 index 0000000000000000000000000000000000000000..fce1683f0fddeeefd30ec9ce69e2bc938bddc237 --- /dev/null +++ b/app/src/main/java/ml/adamsprogs/bimba/models/adapters/DeparturesAdapter.kt @@ -0,0 +1,118 @@ +package ml.adamsprogs.bimba.models.adapters + +import android.app.AlertDialog +import android.content.Context +import android.content.DialogInterface +import android.support.v4.content.res.ResourcesCompat +import android.support.v7.widget.RecyclerView +import android.view.View +import android.view.ViewGroup +import android.widget.ImageView +import android.widget.TextView +import ml.adamsprogs.bimba.R +import android.view.LayoutInflater +import ml.adamsprogs.bimba.Declinator +import ml.adamsprogs.bimba.models.Departure +import ml.adamsprogs.bimba.rollTime +import java.util.* + +//todo<p:1> on click show time (HH:MM) +class DeparturesAdapter(val context: Context, private val departures: List<Departure>?, private val relativeTime: Boolean) : + RecyclerView.Adapter<DeparturesAdapter.ViewHolder>() { + + companion object { + const val VIEW_TYPE_LOADING: Int = 0 + const val VIEW_TYPE_CONTENT: Int = 1 + const val VIEW_TYPE_EMPTY: Int = 2 + } + +// init { +// departures?.forEach { +// println("${it.line} -> ${it.headsign} @${it.time} (${if (it.isModified) it.modification[0] else{} })") +// } +// } + + override fun getItemCount(): Int { + if (departures == null || departures.isEmpty()) + return 1 + return departures.size + } + + override fun getItemViewType(position: Int): Int { + return when { + departures == null -> VIEW_TYPE_EMPTY + departures.isEmpty() -> VIEW_TYPE_LOADING + else -> VIEW_TYPE_CONTENT + } + } + + override fun onBindViewHolder(holder: ViewHolder, position: Int) { + if (departures == null) { + return + } + val line = holder.lineTextView + val time = holder.timeTextView + val direction = holder.directionTextView + if (departures.isEmpty()) { + time.text = context.getString(R.string.no_departures) + return + } + val departure = departures[position] + //println("${departure.line} -> ${departure.headsign} @${departure.time} (${if (departure.isModified) departure.modification[0] else {}})") + val now = Calendar.getInstance() + val departureTime = Calendar.getInstance().rollTime(departure.time) + if (departure.tomorrow) + departureTime.add(Calendar.DAY_OF_MONTH, 1) + + val departureIn = ((departureTime.timeInMillis - now.timeInMillis) / (1000 * 60)).toInt() + val timeString: String + + timeString = if (departureIn > 60 || departureIn < 0 || !relativeTime) + context.getString(R.string.departure_at, "${String.format("%02d", departureTime.get(Calendar.HOUR_OF_DAY))}:${String.format("%02d", departureTime.get(Calendar.MINUTE))}") + else if (departureIn > 0 && !departure.onStop) + context.getString(Declinator.decline(departureIn), departureIn.toString()) + else + context.getString(R.string.now) + + line.text = departure.lineText + time.text = timeString + direction.text = context.getString(R.string.departure_to, departure.headsign) + val icon = holder.typeIcon + if (departure.vm) + icon.setImageDrawable(ResourcesCompat.getDrawable(context.resources, R.drawable.ic_departure_vm, context.theme)) + else + icon.setImageDrawable(ResourcesCompat.getDrawable(context.resources, R.drawable.ic_departure_timetable, context.theme)) + + if (departure.lowFloor) + holder.floorIcon.visibility = View.VISIBLE + if (departure.isModified) { + holder.infoIcon.visibility = View.VISIBLE + holder.root.setOnClickListener { + AlertDialog.Builder(context) + .setPositiveButton(context.getText(android.R.string.ok), + { dialog: DialogInterface, _: Int -> dialog.cancel() }) + .setCancelable(true) + .setMessage(departure.modification.joinToString("; ")) + .create().show() + } + } + } + + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder { + val context = parent.context + val inflater = LayoutInflater.from(context) + + val rowView = inflater.inflate(R.layout.row_departure, parent, false) + return ViewHolder(rowView) + } + + inner class ViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) { + val root = itemView.findViewById<View>(R.id.departureRow)!! + val lineTextView: TextView = itemView.findViewById(R.id.lineNumber) + val timeTextView: TextView = itemView.findViewById(R.id.departureTime) + val directionTextView: TextView = itemView.findViewById(R.id.departureDirection) + val typeIcon: ImageView = itemView.findViewById(R.id.departureTypeIcon) + val infoIcon: ImageView = itemView.findViewById(R.id.departureInfoIcon) + val floorIcon: ImageView = itemView.findViewById(R.id.departureFloorIcon) + } +} \ No newline at end of file diff --git a/app/src/main/java/ml/adamsprogs/bimba/models/adapters/FavouriteEditRowAdapter.kt b/app/src/main/java/ml/adamsprogs/bimba/models/adapters/FavouriteEditRowAdapter.kt new file mode 100644 index 0000000000000000000000000000000000000000..29ce0c2a7c8880ee71be0409fa4034ef5b16a6ad --- /dev/null +++ b/app/src/main/java/ml/adamsprogs/bimba/models/adapters/FavouriteEditRowAdapter.kt @@ -0,0 +1,53 @@ +package ml.adamsprogs.bimba.models.adapters + +import android.support.v7.widget.RecyclerView +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import android.widget.ImageView +import android.widget.TextView +import ml.adamsprogs.bimba.R +import ml.adamsprogs.bimba.collections.FavouriteStorage +import ml.adamsprogs.bimba.models.Favourite +import ml.adamsprogs.bimba.models.Plate +import ml.adamsprogs.bimba.models.Timetable + +class FavouriteEditRowAdapter(private var favourite: Favourite) : + RecyclerView.Adapter<FavouriteEditRowAdapter.ViewHolder>() { + override fun getItemCount(): Int { + return favourite.size + } + + override fun onBindViewHolder(holder: ViewHolder, position: Int) { + val timetable = Timetable.getTimetable() + val favourites = FavouriteStorage.getFavouriteStorage() + val id = favourite.segments.flatMap { it.plates!! }.sortedBy { "${it.line}${it.stop}"}[position] + val plate = Plate(id, null) + val favouriteElement = "${timetable.getStopName(plate.id.stop)} ( ${timetable.getStopCode(plate.id.stop)}):\n${plate.id.line} → ${plate.id.headsign}" + holder.rowTextView.text = favouriteElement +// holder?.splitButton?.setOnClickListener { +// favourites.detach(favourite.name, id, favouriteElement) +// favourite = favourites.favourites[favourite.name]!! +// notifyDataSetChanged() +// } + holder.deleteButton.setOnClickListener { + favourites.delete(favourite.name, id) + favourite = favourites.favourites[favourite.name]!! + notifyDataSetChanged() + } + } + + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder { + val context = parent.context + val inflater = LayoutInflater.from(context) + + val rowView = inflater.inflate(R.layout.row_favourite_edit, parent, false) + return ViewHolder(rowView) + } + + inner class ViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) { + val rowTextView:TextView = itemView.findViewById(R.id.favourite_edit_row) +// val splitButton:ImageView = itemView.findViewById(R.id.favourite_edit_split) + val deleteButton:ImageView = itemView.findViewById(R.id.favourite_edit_delete) + } +} \ No newline at end of file diff --git a/app/src/main/java/ml/adamsprogs/bimba/models/adapters/FavouritesAdapter.kt b/app/src/main/java/ml/adamsprogs/bimba/models/adapters/FavouritesAdapter.kt new file mode 100644 index 0000000000000000000000000000000000000000..6cef0867376e4bcf1b8aecf4df91fc3b683ede7f --- /dev/null +++ b/app/src/main/java/ml/adamsprogs/bimba/models/adapters/FavouritesAdapter.kt @@ -0,0 +1,145 @@ +package ml.adamsprogs.bimba.models.adapters + +import android.content.Context +import android.support.v4.content.res.ResourcesCompat +import android.support.v7.widget.* +import android.support.v7.widget.PopupMenu +import android.util.SparseBooleanArray +import android.view.* +import android.widget.* +import ml.adamsprogs.bimba.R +import android.view.LayoutInflater +import kotlinx.coroutines.experimental.CommonPool +import kotlinx.coroutines.experimental.android.UI +import kotlinx.coroutines.experimental.async +import kotlinx.coroutines.experimental.launch +import java.util.* +import ml.adamsprogs.bimba.Declinator +import ml.adamsprogs.bimba.collections.FavouriteStorage +import ml.adamsprogs.bimba.secondsAfterMidnight + + +class FavouritesAdapter(val appContext: Context, var favourites: FavouriteStorage, + private val onMenuItemClickListener: OnMenuItemClickListener, + private val onClickListener: ViewHolder.OnClickListener) : + RecyclerView.Adapter<FavouritesAdapter.ViewHolder>() { + + private val selectedItems = SparseBooleanArray() + + private fun isSelected(position: Int) = getSelectedItems().contains(position) + + fun toggleSelection(position: Int) { + if (selectedItems.get(position, false)) { + selectedItems.delete(position) + } else { + selectedItems.put(position, true) + } + notifyItemChanged(position) + } + + fun clearSelection() { + val selection = getSelectedItems() + selectedItems.clear() + for (i in selection) { + notifyItemChanged(i) + } + } + + fun getSelectedItemCount() = selectedItems.size() + + fun getSelectedItems(): List<Int> { + val items = ArrayList<Int>(selectedItems.size()) + (0 until selectedItems.size()).mapTo(items) { selectedItems.keyAt(it) } + return items + } + + override fun getItemCount() = favourites.size + + override fun onBindViewHolder(holder: ViewHolder, position: Int) { + launch(UI) { + val favourite = favourites[position]!! + holder.nameTextView.text = favourite.name + + holder.selectedOverlay.visibility = if (isSelected(position)) View.VISIBLE else View.INVISIBLE + holder.moreButton.setOnClickListener { + val popup = PopupMenu(appContext, it) + val inflater = popup.menuInflater + popup.setOnMenuItemClickListener { + when (it.itemId) { + R.id.favourite_edit -> onMenuItemClickListener.edit(favourite.name) + R.id.favourite_delete -> onMenuItemClickListener.delete(favourite.name) + else -> false + } + } + inflater.inflate(R.menu.favourite_actions, popup.menu) + popup.show() + } + + val nextDeparture = async(CommonPool) { + favourite.nextDeparture() + }.await() + + val nextDepartureText: String + val nextDepartureLineText: String + if (nextDeparture != null) { + val interval = nextDeparture.timeTill(Calendar.getInstance().secondsAfterMidnight()) + nextDepartureLineText = appContext.getString(R.string.departure_to_line, nextDeparture.line, nextDeparture.headsign) + nextDepartureText = if (interval < 0) + appContext.getString(R.string.just_departed) + else + appContext.getString(Declinator.decline(interval), interval.toString()) + } else { + nextDepartureText = appContext.getString(R.string.no_next_departure) + nextDepartureLineText = "" + } + holder.timeTextView.text = nextDepartureText + holder.lineTextView.text = nextDepartureLineText + if (nextDeparture != null) { + if (nextDeparture.vm) + holder.typeIcon.setImageDrawable(ResourcesCompat.getDrawable(appContext.resources, R.drawable.ic_departure_vm, appContext.theme)) + else + holder.typeIcon.setImageDrawable(ResourcesCompat.getDrawable(appContext.resources, R.drawable.ic_departure_timetable, appContext.theme)) + } + } + } + + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder { + val context = parent.context + val inflater = LayoutInflater.from(context) + + val rowView = inflater.inflate(R.layout.row_favourite, parent, false) + return ViewHolder(rowView, onClickListener) + } + + class ViewHolder(itemView: View, private val listener: OnClickListener) : RecyclerView.ViewHolder(itemView), View.OnClickListener, View.OnLongClickListener { + override fun onLongClick(v: View?): Boolean { + return listener.onItemLongClicked(adapterPosition) + } + + override fun onClick(v: View?) { + listener.onItemClicked(adapterPosition) + } + + val selectedOverlay: View = itemView.findViewById(R.id.selected_overlay) + val nameTextView: TextView = itemView.findViewById(R.id.favourite_name) + val timeTextView: TextView = itemView.findViewById(R.id.favourite_time) + val lineTextView: TextView = itemView.findViewById(R.id.favourite_line) + val moreButton: ImageView = itemView.findViewById(R.id.favourite_more_button) + val typeIcon: ImageView = itemView.findViewById(R.id.departureTypeIcon) + + init { + itemView.setOnClickListener(this) + itemView.setOnLongClickListener(this) + } + + interface OnClickListener { + fun onItemClicked(position: Int) + fun onItemLongClicked(position: Int): Boolean + } + } + + interface OnMenuItemClickListener { + fun edit(name: String): Boolean + fun delete(name: String): Boolean + } +} \ 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 42ffd2a2b02df0482f2b00767b902ea66d809e34..6b7c47a92bd4100cf4aa28d34f91a7987cb34c66 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -15,6 +15,7 @@no database background <string name="no_db_connect">Connect to the Internet to download the timetable</string> <string name="no_db_downloading">Timetable is being downloaded…</string> <string name="timetable_downloading">Downloading timetable</string> + <string name="timetable_uncompressing">%1$d/%2$d, uncompressing: %3$f%</string> <string name="search_placeholder">Stop or line…</string> <string name="no_connectivity">No connectivity – can’t update timetable</string> <string name="timetable_up_to_date">Timetable is up-to-date</string> @@ -71,7 +72,6 @@Valid till %1$s <string name="departure_floor" translatable="false">departure floor type (lowFloor)</string> <string name="departure_info" translatable="false">departure info icon</string> <string name="refreshing_cache">Refreshing cache. May take some time…</string> - <string name="timetable_converting">Converting timetable…</string> <string name="today">Today</string> <string name="no_departures">No departures</string> diff --git a/app/src/main/res/values-de/strings.xml b/app/src/main/res/values-de/strings.xml index e9534bf4467d28c55290d78ef6b12726781c273c..744bb00f8113915c6bcdf0bd600b32e4f788bb62 100644 --- a/app/src/main/res/values-de/strings.xml +++ b/app/src/main/res/values-de/strings.xml @@ -59,7 +59,6 @@ "heute zeigen (wenn es Dienstag ist, es wird auf „Arbeitstage“ Karte sein).\n" "Sei sicher, die Nachrichten auf\nhttps://www.ztm.poznan.pl/en\nkonsultieren.\n\n" </string> <string name="refreshing_cache">Cache wird aktualisiert. Es kann einige Zeit dauern…</string> - <string name="timetable_converting">Fahrplan wird umgewandelt…</string> <string name="today">Heute</string> <string name="no_departures">Keine Abfahrten</string> <string name="tab_text_line_to">Hin</string> @@ -68,4 +67,5 @@Fahrplan gilt nur bis heute. <string name="timetable_validity_finished">Die Gültigkeit des Zeitplans ist beendet. Verbind mit dem Internet, um eine neue herunterzuladen und um fortzufahren.</string> <string name="timetable_validity_tomorrow">Fahrplan gilt nur bis morgen.</string> <string name="just_departed">Gerade gegangen</string> + <string name="timetable_uncompressing">%1$d/%2$d, dekompression: %3$f%</string> </resources> diff --git a/app/src/main/res/values-it/strings.xml b/app/src/main/res/values-it/strings.xml index dff70c86c95fb00f7f973232d2766e713e55a31c..d71472cece3d9dc77c59bf6043f2e5fec0f45f4c 100644 --- a/app/src/main/res/values-it/strings.xml +++ b/app/src/main/res/values-it/strings.xml @@ -57,7 +57,6 @@ "(se è martedì, sarà nella scheda «giorni di lavoro»).\n" "Assicurati di consultare le notifiche su\nhttps://www.ztm.poznan.pl/en.\n\n" </string> <string name="refreshing_cache">Cache sta essendo aggiornato. Può richiedere un certo tempo…</string> - <string name="timetable_converting">L’orario e stando convertito</string> <string name="today">Oggi</string> <string name="no_departures">Nessune partenze</string> <string name="tab_text_line_to">Avanti</string> @@ -66,4 +65,5 @@L’orario è valido solo fino ad oggi. <string name="timetable_validity_finished">"La validità dell’orario è terminata. Connetti a Internet per scaricarne uno nuovo e continuare. "</string> <string name="timetable_validity_tomorrow">L’orario è valido solo fino a domani.</string> <string name="just_departed">Appena partito</string> + <string name="timetable_uncompressing">%1$d/%2$d, decompressione: %3$f%</string> </resources> diff --git a/app/src/main/res/values-pl/strings.xml b/app/src/main/res/values-pl/strings.xml index 2871fa773db94ca328522d1a8b71a9e06e5c3921..90ab004f7fb7dbb77177895c181e8e4f29e7fe35 100644 --- a/app/src/main/res/values-pl/strings.xml +++ b/app/src/main/res/values-pl/strings.xml @@ -59,7 +59,6 @@Zbieranie odjazdów… <string name="valid_since">Ważny od %1$s</string> <string name="valid_till">Ważny do %1$s</string> <string name="refreshing_cache">Odświeżanie pamięci podręcznej. Może chwilę potrwać…</string> - <string name="timetable_converting">Konwertowanie rozkładu</string> <string name="today">Dzisiaj</string> <string name="no_departures">Brak odjazdów</string> <string name="tab_text_line_to">Tam</string> @@ -68,4 +67,5 @@Rozkład obowiązuje tylko do dzisiaj. <string name="timetable_validity_finished">Rozkład przestał obowiązywać. Połącz się z Internetem, aby pobrać nowy i kontynuować.</string> <string name="timetable_validity_tomorrow">Rozkład obowiązuje tylko do jutra.</string> <string name="just_departed">Właśnie odjechał</string> + <string name="timetable_uncompressing">%1$d/%2$d, dekompresja: %3$f%</string> </resources> \ No newline at end of file diff --git a/build.gradle b/build.gradle index e67481b9243b782c24e7bbe927900e7d970012f6..f7dc8977568630d236b494ed8f2f4f6e2b9efcd2 100644 --- a/build.gradle +++ b/build.gradle @@ -9,7 +9,7 @@ //maven { url 'https://dl.bintray.com/guardian/android' } // TooLargeTool google() } dependencies { - classpath 'com.android.tools.build:gradle:3.0.1' + classpath 'com.android.tools.build:gradle:3.1.0' classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" // NOTE: Do not place your application dependencies here; they belong