diff --git a/app/build.gradle b/app/build.gradle index a72930c..deb0ae7 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -1,141 +1,143 @@ apply plugin: 'kotlin-kapt' apply plugin: 'kotlin-android' apply plugin: 'com.android.application' android { compileSdk 34 namespace "it.reyboz.bustorino" defaultConfig { applicationId "it.reyboz.bustorino" minSdkVersion 21 targetSdkVersion 34 buildToolsVersion = '34.0.0' versionCode 55 versionName "2.1.5" vectorDrawables.useSupportLibrary = true multiDexEnabled true javaCompileOptions { annotationProcessorOptions { arguments = ["room.schemaLocation": "$projectDir/assets/schemas/".toString()] } } testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" } testOptions { unitTests.returnDefaultValues = true } sourceSets { androidTest.assets.srcDirs += files("$projectDir/assets/schemas/".toString()) } buildTypes { debug { applicationIdSuffix ".debug" versionNameSuffix "-dev" } gitpull{ applicationIdSuffix ".gitdev" versionNameSuffix "-gitdev" } } repositories { mavenCentral() mavenLocal() } dependencies { //new libraries } compileOptions { sourceCompatibility JavaVersion.VERSION_17 targetCompatibility JavaVersion.VERSION_17 } kotlin { jvmToolchain 17 } lint { abortOnError false } } dependencies { implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version" api "org.jetbrains.kotlinx:kotlinx-coroutines-core:$coroutines_version" api "org.jetbrains.kotlinx:kotlinx-coroutines-android:$coroutines_version" implementation 'androidx.legacy:legacy-support-v4:1.0.0' implementation "androidx.fragment:fragment-ktx:$fragment_version" implementation "androidx.activity:activity:$activity_version" implementation "androidx.annotation:annotation:1.6.0" implementation "androidx.swiperefreshlayout:swiperefreshlayout:1.1.0" implementation "androidx.appcompat:appcompat:$appcompat_version" implementation "androidx.appcompat:appcompat-resources:$appcompat_version" implementation "androidx.preference:preference:$preference_version" implementation "androidx.work:work-runtime:$work_version" implementation "androidx.work:work-runtime-ktx:$work_version" implementation "com.google.android.material:material:1.9.0" implementation 'androidx.constraintlayout:constraintlayout:2.1.4' implementation "androidx.coordinatorlayout:coordinatorlayout:1.2.0" implementation 'org.jsoup:jsoup:1.15.3' implementation 'com.readystatesoftware.sqliteasset:sqliteassethelper:2.0.1' implementation 'com.android.volley:volley:1.2.1' implementation 'org.osmdroid:osmdroid-android:6.1.10' // remember to enable maven repo jitpack.io when wanting to use osmbonuspack //implementation 'com.github.MKergall:osmbonuspack:6.9.0' // ACRA implementation "ch.acra:acra-mail:$acra_version" implementation "ch.acra:acra-dialog:$acra_version" // google transit realtime implementation 'com.google.protobuf:protobuf-java:3.19.6' // mqtt library implementation 'org.eclipse.paho:org.eclipse.paho.client.mqttv3:1.2.5' - implementation 'com.github.hannesa2:paho.mqtt.android:4.1' + implementation 'com.github.hannesa2:paho.mqtt.android:4.2' + //implementation 'com.github.fabmazz:paho.mqtt.android:v0.0.1' + // ViewModel implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:$lifecycle_version" // LiveData implementation "androidx.lifecycle:lifecycle-livedata-ktx:$lifecycle_version" // Lifecycles only (without ViewModel or LiveData) implementation "androidx.lifecycle:lifecycle-runtime-ktx:$lifecycle_version" // Legacy implementation 'androidx.legacy:legacy-support-v4:1.0.0' // Room components implementation "androidx.room:room-runtime:$room_version" implementation "androidx.room:room-ktx:$room_version" kapt "androidx.room:room-compiler:$room_version" //multidex - we need this to build the app implementation "androidx.multidex:multidex:$multidex_version" implementation 'de.siegmar:fastcsv:2.0.0' testImplementation 'junit:junit:4.12' implementation 'junit:junit:4.12' implementation "androidx.test.ext:junit:1.1.5" implementation "androidx.test:core:$androidXTestVersion" implementation "androidx.test:runner:$androidXTestVersion" implementation "androidx.room:room-testing:$room_version" androidTestImplementation "androidx.test.ext:junit:1.1.5" androidTestImplementation "androidx.test:core:$androidXTestVersion" androidTestImplementation "androidx.test:runner:$androidXTestVersion" androidTestImplementation "androidx.test:rules:$androidXTestVersion" androidTestImplementation "androidx.room:room-testing:$room_version" } diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 53f3581..58dffc4 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -1,127 +1,130 @@ - + + + \ No newline at end of file diff --git a/app/src/main/java/it/reyboz/bustorino/backend/Notifications.java b/app/src/main/java/it/reyboz/bustorino/backend/Notifications.java index 9d3e8ca..2fd2ec3 100644 --- a/app/src/main/java/it/reyboz/bustorino/backend/Notifications.java +++ b/app/src/main/java/it/reyboz/bustorino/backend/Notifications.java @@ -1,80 +1,91 @@ package it.reyboz.bustorino.backend; import android.app.Notification; import android.app.NotificationChannel; import android.app.NotificationManager; import android.content.Context; import android.os.Build; import androidx.core.app.NotificationCompat; import it.reyboz.bustorino.R; public class Notifications { public static final String DEFAULT_CHANNEL_ID ="Default"; public static final String DB_UPDATE_CHANNELS_ID ="Database Update"; + //match this value to the one used by the MQTTAndroidClient MANUALLY + public static void createDefaultNotificationChannel(Context context) { // Create the NotificationChannel, but only on API 26+ because // the NotificationChannel class is new and not in the support library if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { CharSequence name = context.getString(R.string.default_notification_channel); String description = context.getString(R.string.default_notification_channel_description); int importance = NotificationManager.IMPORTANCE_DEFAULT; NotificationChannel channel = new NotificationChannel(DEFAULT_CHANNEL_ID, name, importance); channel.setDescription(description); // Register the channel with the system; you can't change the importance // or other notification behaviors after this NotificationManager notificationManager = context.getSystemService(NotificationManager.class); notificationManager.createNotificationChannel(channel); } } /** * Register a notification channel on Android Oreo and above * @param con a Context * @param name channel name * @param description channel description * @param importance channel importance (from NotificationManager) * @param ID channel ID */ public static void createNotificationChannel(Context con, String name, String description, int importance, String ID){ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { NotificationChannel channel = new NotificationChannel(ID, name, importance); channel.setDescription(description); // Register the channel with the system; you can't change the importance // or other notification behaviors after this NotificationManager notificationManager = con.getSystemService(NotificationManager.class); notificationManager.createNotificationChannel(channel); } } public static Notification makeMatoDownloadNotification(Context context,String title){ return new NotificationCompat.Builder(context, Notifications.DB_UPDATE_CHANNELS_ID) //.setContentIntent(PendingIntent.getActivity(context, 0, Intent(context, MainActivity::class.java), Constants.PENDING_INTENT_FLAG_IMMUTABLE)) - .setSmallIcon(R.drawable.bus) + .setSmallIcon(R.drawable.ic_bus_stilized_transparent) .setOngoing(true) .setAutoCancel(true) .setOnlyAlertOnce(true) .setPriority(NotificationCompat.PRIORITY_MIN) .setContentTitle(context.getString(R.string.app_name)) .setLocalOnly(true) .setVisibility(NotificationCompat.VISIBILITY_SECRET) .setContentText(title) .build(); } public static Notification makeMatoDownloadNotification(Context context){ return makeMatoDownloadNotification(context, context.getString(R.string.downloading_data_mato)); } + public static Notification makeMQTTServiceNotification(Context context){ + return makeMatoDownloadNotification(context, context.getString(R.string.mqtt_notification_text)); + } + + public static void cancelNotification(Context context, int notificationID){ + NotificationManager manager = (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE); + manager.cancel(notificationID); + } + public static void createDBNotificationChannel(Context context){ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { NotificationChannel channel = new NotificationChannel( Notifications.DB_UPDATE_CHANNELS_ID, context.getString(R.string.database_notification_channel), NotificationManager.IMPORTANCE_MIN ); NotificationManager notificationManager = context.getSystemService(NotificationManager.class); notificationManager.createNotificationChannel(channel); } } } diff --git a/app/src/main/java/it/reyboz/bustorino/backend/mato/MQTTMatoClient.kt b/app/src/main/java/it/reyboz/bustorino/backend/mato/MQTTMatoClient.kt index 0057e91..afc4048 100644 --- a/app/src/main/java/it/reyboz/bustorino/backend/mato/MQTTMatoClient.kt +++ b/app/src/main/java/it/reyboz/bustorino/backend/mato/MQTTMatoClient.kt @@ -1,360 +1,404 @@ package it.reyboz.bustorino.backend.mato +import android.app.Notification +import android.app.NotificationManager import android.content.Context +import android.os.Build import android.util.Log import androidx.lifecycle.LifecycleOwner import info.mqtt.android.service.Ack -import it.reyboz.bustorino.backend.gtfs.LivePositionUpdate -import org.eclipse.paho.client.mqttv3.* import info.mqtt.android.service.MqttAndroidClient import info.mqtt.android.service.QoS - +import it.reyboz.bustorino.backend.Notifications +import it.reyboz.bustorino.backend.gtfs.LivePositionUpdate +import org.eclipse.paho.client.mqttv3.* import org.json.JSONArray import org.json.JSONException import java.lang.ref.WeakReference -import java.util.ArrayList -import java.util.Properties +import java.util.* typealias PositionsMap = HashMap > -class MQTTMatoClient private constructor(): MqttCallbackExtended{ +class MQTTMatoClient(): MqttCallbackExtended{ private var isStarted = false private var subscribedToAll = false private lateinit var client: MqttAndroidClient //private var clientID = "" private val respondersMap = HashMap>>() private val currentPositions = PositionsMap() private lateinit var lifecycle: LifecycleOwner + //TODO: remove class reference to context (always require context in all methods) private var context: Context?= null private var connectionTrials = 0 + private var notification: Notification? = null + + //private lateinit var notification: Notification private fun connect(context: Context, iMqttActionListener: IMqttActionListener?){ val clientID = "mqtt-explorer-${getRandomString(8)}"//"mqttjs_${getRandomString(8)}" + //notification = Notifications.makeMQTTServiceNotification(context) + client = MqttAndroidClient(context,SERVER_ADDR,clientID,Ack.AUTO_ACK) + // WE DO NOT WANT A FOREGROUND SERVICE -> it's only more mayhem + // (and the positions need to be downloaded only when the app is shown) + /*if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.O){ + //we need a notification + val notific = Notifications.makeMQTTServiceNotification(context) + client.setForegroundService(notific) + notification=notific + }*/ + val options = MqttConnectOptions() //options.sslProperties = options.isCleanSession = true val headersPars = Properties() headersPars.setProperty("Origin","https://mato.muoversiatorino.it") headersPars.setProperty("Host","mapi.5t.torino.it") options.customWebSocketHeaders = headersPars Log.d(DEBUG_TAG,"client name: $clientID") //actually connect client.connect(options,null, iMqttActionListener) isStarted = true client.setCallback(this) if (this.context ==null) this.context = context.applicationContext } override fun connectComplete(reconnect: Boolean, serverURI: String?) { Log.d(DEBUG_TAG, "Connected to server, reconnect: $reconnect") Log.d(DEBUG_TAG, "Have listeners: $respondersMap") } private fun connectTopic(topic: String){ if(context==null){ Log.e(DEBUG_TAG, "Trying to connect but context is null") return } connectionTrials += 1 connect(context!!, object : IMqttActionListener{ override fun onSuccess(asyncActionToken: IMqttToken?) { val disconnectedBufferOptions = DisconnectedBufferOptions() disconnectedBufferOptions.isBufferEnabled = true disconnectedBufferOptions.bufferSize = 100 disconnectedBufferOptions.isPersistBuffer = false disconnectedBufferOptions.isDeleteOldestMessages = false client.setBufferOpts(disconnectedBufferOptions) client.subscribe(topic, QoS.AtMostOnce.value) isStarted = true } override fun onFailure(asyncActionToken: IMqttToken?, exception: Throwable?) { Log.e(DEBUG_TAG, "FAILED To connect to the server",exception) if (connectionTrials < 10) { Log.d(DEBUG_TAG, "Reconnecting") connectTopic(topic) } else { //reset connection trials connectionTrials = 0 } } }) } fun startAndSubscribe(lineId: String, responder: MQTTMatoListener, context: Context): Boolean{ //start the client, and then subscribe to the topic val topic = mapTopic(lineId) this.context = context.applicationContext synchronized(this) { if(!isStarted){ connectTopic(topic) //wait for connection } else { client.subscribe(topic, QoS.AtMostOnce.value) } } synchronized(this){ if (!respondersMap.contains(lineId)) respondersMap[lineId] = ArrayList() respondersMap[lineId]!!.add(WeakReference(responder)) Log.d(DEBUG_TAG, "Add MQTT Listener for line $lineId, topic $topic") } return true } - fun desubscribe(responder: MQTTMatoListener){ + fun stopMatoRequests(responder: MQTTMatoListener){ var removed = false for ((line,v)in respondersMap.entries){ var done = false for (el in v){ if (el.get()==null){ v.remove(el) } else if(el.get() == responder){ v.remove(el) done = true } if (done) break } if(done) Log.d(DEBUG_TAG, "Removed one listener for line $line, listeners: $v") //if (done) break if (v.isEmpty()){ //actually unsubscribe client.unsubscribe( mapTopic(line)) } removed = done || removed } - // remove lines that have no responders + // check responders map, remove lines that have no responders + for(line in respondersMap.keys){ if(respondersMap[line]?.isEmpty() == true){ respondersMap.remove(line) } } Log.d(DEBUG_TAG, "Removed: $removed, respondersMap: $respondersMap") } fun getPositions(): PositionsMap{ return currentPositions } + /** + * Cancel the notification + */ + fun removeNotification(context: Context){ + val notifManager = context.applicationContext.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager + + notifManager.cancel(MQTT_NOTIFICATION_ID) + } + private fun sendUpdateToResponders(responders: ArrayList>): Int{ //var sent = false var count = 0 for (wrD in responders) { if (wrD.get() == null) { Log.d(DEBUG_TAG, "Removing weak reference") responders.remove(wrD) } else { wrD.get()!!.onUpdateReceived(currentPositions) //sent = true count++ } } return count } override fun connectionLost(cause: Throwable?) { + var doReconnect = false + for ((line,elms) in respondersMap.entries){ + if(!elms.isEmpty()){ + doReconnect = true + break + } + } + if (!doReconnect){ + Log.d(DEBUG_TAG, "Disconnected, but no responders to give the positions, avoid reconnecting") + //finish here + return + } Log.w(DEBUG_TAG, "Lost connection in MQTT Mato Client") - synchronized(this){ // isStarted = false //var i = 0 // while(i < 20 && !isStarted) { connect(context!!, object: IMqttActionListener{ override fun onSuccess(asyncActionToken: IMqttToken?) { //relisten to messages for ((line,elms) in respondersMap.entries){ val topic = mapTopic(line) if(elms.isEmpty()) respondersMap.remove(line) else { client.subscribe(topic, QoS.AtMostOnce.value, null, null) Log.d(DEBUG_TAG, "Resubscribed with topic $topic") } } Log.d(DEBUG_TAG, "Reconnected to MQTT Mato Client") } override fun onFailure(asyncActionToken: IMqttToken?, exception: Throwable?) { Log.w(DEBUG_TAG, "Failed to reconnect to MQTT server") } }) } } override fun messageArrived(topic: String?, message: MqttMessage?) { if (topic==null || message==null) return //Log.d(DEBUG_TAG,"Arrived message on topic $topic, ${String(message.payload)}") parseMessageAndAddToList(topic, message) //GlobalScope.launch { } } private fun parseMessageAndAddToList(topic: String, message: MqttMessage){ val vals = topic.split("/") val lineId = vals[1] val vehicleId = vals[2] val timestamp = (System.currentTimeMillis() / 1000 ) as Long val messString = String(message.payload) try { val jsonList = JSONArray(messString) - //val full = if(jsonList.length()>7) { - // if (jsonList.get(7).equals(null)) null else jsonList.getInt(7) - //}else null + /*val posUpdate = MQTTPositionUpdate(lineId+"U", vehicleId, jsonList.getDouble(0), jsonList.getDouble(1), if(jsonList.get(2).equals(null)) null else jsonList.getInt(2), if(jsonList.get(3).equals(null)) null else jsonList.getInt(3), if(jsonList.get(4).equals(null)) null else jsonList.getString(4)+"U", if(jsonList.get(5).equals(null)) null else jsonList.getInt(5), if(jsonList.get(6).equals(null)) null else jsonList.getInt(6), //full ) - */ if(jsonList.get(4)==null){ Log.d(DEBUG_TAG, "We have null tripId: line $lineId veh $vehicleId: $jsonList") return } val posUpdate = LivePositionUpdate( jsonList.getString(4)+"U", null, null, lineId+"U", vehicleId, jsonList.getDouble(0), //latitude jsonList.getDouble(1), //longitude if(jsonList.get(2).equals(null)) null else jsonList.getInt(2).toFloat(), //"heading" (same as bearing?) timestamp, if(jsonList.get(6).equals(null)) null else jsonList.getInt(6).toString() //nextStop ) //add update var valid = false if(!currentPositions.contains(lineId)) currentPositions[lineId] = HashMap() currentPositions[lineId]?.let{ it[vehicleId] = posUpdate valid = true } //sending //Log.d(DEBUG_TAG, "Parsed update on topic $topic, line $lineId, responders $respondersMap") var cc = 0 if (LINES_ALL in respondersMap.keys) { val count = sendUpdateToResponders(respondersMap[LINES_ALL]!!) cc +=count } if(lineId in respondersMap.keys){ cc += sendUpdateToResponders(respondersMap[lineId]!!) } //Log.d(DEBUG_TAG, "Sent to $cc responders, have $respondersMap") if(cc==0){ Log.w(DEBUG_TAG, "We have received an update but apparently there is no one to send it") var emptyResp = true for(en in respondersMap.values){ if(!en.isEmpty()){ emptyResp=false break } } //try unsubscribing to all if(emptyResp) { Log.d(DEBUG_TAG, "Unsubscribe all") client.unsubscribe(LINES_ALL) } } //Log.d(DEBUG_TAG, "We have update on line $lineId, vehicle $vehicleId") } catch (e: JSONException){ Log.w(DEBUG_TAG,"Cannot decipher message on topic $topic, line $lineId, veh $vehicleId (bad JSON)") } catch (e: Exception){ Log.e(DEBUG_TAG, "Exception occurred", e) } } override fun deliveryComplete(token: IMqttDeliveryToken?) { //NOT USED (we're not sending any messages) } + /*/** + * Stop the service forever. Client has not to be used again!! + */ + fun closeClientForever(){ + client.disconnect() + client.close() + }*/ + + fun disconnect(){ + client.disconnect() + } + companion object{ const val SERVER_ADDR="wss://mapi.5t.torino.it:443/scre" const val LINES_ALL="ALL" private const val DEBUG_TAG="BusTO-MatoMQTT" - @Volatile - private var instance: MQTTMatoClient? = null + //this has to match the value in MQTT library (MQTTAndroidClient) + const val MQTT_NOTIFICATION_ID: Int = 77 - fun getInstance() = instance?: synchronized(this){ - instance?: MQTTMatoClient().also { instance= it } - } @JvmStatic fun mapTopic(lineId: String): String{ return if(lineId== LINES_ALL || lineId == "#") "#" else{ "/${lineId}/#" } } fun getRandomString(length: Int) : String { val allowedChars = ('a'..'f') + ('0'..'9') return (1..length) .map { allowedChars.random() } .joinToString("") } fun interface MQTTMatoListener{ //positionsMap is a dict with line -> vehicle -> Update fun onUpdateReceived(posUpdates: PositionsMap) } } } data class MQTTPositionUpdate( val lineId: String, val vehicleId: String, val latitude: Double, val longitude: Double, val heading: Int?, val speed: Int?, val tripId: String?, val direct: Int?, val nextStop: Int?, //val full: Int? ) \ No newline at end of file diff --git a/app/src/main/java/it/reyboz/bustorino/data/DBUpdateWorker.java b/app/src/main/java/it/reyboz/bustorino/data/DBUpdateWorker.java index 5af122e..8acd918 100644 --- a/app/src/main/java/it/reyboz/bustorino/data/DBUpdateWorker.java +++ b/app/src/main/java/it/reyboz/bustorino/data/DBUpdateWorker.java @@ -1,188 +1,188 @@ /* BusTO - Data components Copyright (C) 2021 Fabio Mazza This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . */ package it.reyboz.bustorino.data; import android.annotation.SuppressLint; import android.content.Context; import android.content.SharedPreferences; import android.util.Log; import androidx.annotation.NonNull; import androidx.core.app.NotificationCompat; import androidx.core.app.NotificationManagerCompat; import androidx.work.*; import it.reyboz.bustorino.R; import it.reyboz.bustorino.backend.Fetcher; import it.reyboz.bustorino.backend.Notifications; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicReference; import static android.content.Context.MODE_PRIVATE; public class DBUpdateWorker extends Worker{ public static final String ERROR_CODE_KEY ="Error_Code"; public static final String ERROR_REASON_KEY = "ERROR_REASON"; public static final int ERROR_FETCHING_VERSION = 4; public static final int ERROR_DOWNLOADING_STOPS = 5; public static final int ERROR_DOWNLOADING_LINES = 6; public static final int ERROR_CODE_DB_CLOSED=-2; public static final String SUCCESS_REASON_KEY = "SUCCESS_REASON"; public static final int SUCCESS_NO_ACTION_NEEDED = 9; public static final int SUCCESS_UPDATE_DONE = 1; private final static int NOTIFIC_ID =32198; public static final String FORCED_UPDATE = "FORCED-UPDATE"; public static final String DEBUG_TAG = "Busto-UpdateWorker"; private static final long UPDATE_MIN_DELAY= 9*24*3600; //9 days public DBUpdateWorker(@NonNull Context context, @NonNull WorkerParameters workerParams) { super(context, workerParams); } @SuppressLint("RestrictedApi") @NonNull @Override public Result doWork() { //register Notification channel final Context con = getApplicationContext(); //Notifications.createDefaultNotificationChannel(con); //Use the new notification channels Notifications.createNotificationChannel(con,con.getString(R.string.database_notification_channel), con.getString(R.string.database_notification_channel_desc), NotificationManagerCompat.IMPORTANCE_LOW, Notifications.DB_UPDATE_CHANNELS_ID ); final NotificationManagerCompat notificationManager = NotificationManagerCompat.from(getApplicationContext()); final int notification_ID = 32198; final SharedPreferences shPr = con.getSharedPreferences(con.getString(R.string.mainSharedPreferences),MODE_PRIVATE); final int current_DB_version = shPr.getInt(DatabaseUpdate.DB_VERSION_KEY,-10); final int new_DB_version = DatabaseUpdate.getNewVersion(); final boolean isUpdateCompulsory = getInputData().getBoolean(FORCED_UPDATE,false); final long lastDBUpdateTime = shPr.getLong(DatabaseUpdate.DB_LAST_UPDATE_KEY, 0); long currentTime = System.currentTimeMillis()/1000; //showNotification(notificationManager, notification_ID); final NotificationCompat.Builder builder = new NotificationCompat.Builder(con, Notifications.DB_UPDATE_CHANNELS_ID) .setContentTitle(con.getString(R.string.database_update_msg_notif)) .setProgress(0,0,true) .setPriority(NotificationCompat.PRIORITY_LOW); - builder.setSmallIcon(R.drawable.ic_bus_orange); + builder.setSmallIcon(R.drawable.ic_bus_stilized); notificationManager.notify(notification_ID,builder.build()); Log.d(DEBUG_TAG, "Have previous version: "+current_DB_version +" and new version "+new_DB_version); Log.d(DEBUG_TAG, "Update compulsory: "+isUpdateCompulsory); /* SKIP CHECK (Reason: The Old API might fail at any moment) if (new_DB_version < 0){ //there has been an error final Data out = new Data.Builder().putInt(ERROR_REASON_KEY, ERROR_FETCHING_VERSION) .putInt(ERROR_CODE_KEY,new_DB_version).build(); cancelNotification(notificationID); return ListenableWorker.Result.failure(out); } */ //we got a good version if (!(current_DB_version < new_DB_version || currentTime > lastDBUpdateTime + UPDATE_MIN_DELAY ) && !isUpdateCompulsory) { //don't need to update cancelNotification(notification_ID); return ListenableWorker.Result.success(new Data.Builder(). putInt(SUCCESS_REASON_KEY, SUCCESS_NO_ACTION_NEEDED).build()); } //start the real update AtomicReference resultAtomicReference = new AtomicReference<>(); DatabaseUpdate.setDBUpdatingFlag(con, shPr,true); final DatabaseUpdate.Result resultUpdate = DatabaseUpdate.performDBUpdate(con,resultAtomicReference); DatabaseUpdate.setDBUpdatingFlag(con, shPr,false); if (resultUpdate != DatabaseUpdate.Result.DONE){ //Fetcher.Result result = resultAtomicReference.get(); final Data.Builder dataBuilder = new Data.Builder(); switch (resultUpdate){ case ERROR_STOPS_DOWNLOAD: dataBuilder.put(ERROR_REASON_KEY, ERROR_DOWNLOADING_STOPS); break; case ERROR_LINES_DOWNLOAD: dataBuilder.put(ERROR_REASON_KEY, ERROR_DOWNLOADING_LINES); break; case DB_CLOSED: dataBuilder.put(ERROR_REASON_KEY, ERROR_CODE_DB_CLOSED); break; } cancelNotification(notification_ID); return ListenableWorker.Result.failure(dataBuilder.build()); } Log.d(DEBUG_TAG, "Update finished successfully!"); //update the version in the shared preference final SharedPreferences.Editor editor = shPr.edit(); editor.putInt(DatabaseUpdate.DB_VERSION_KEY, new_DB_version); currentTime = System.currentTimeMillis()/1000; editor.putLong(DatabaseUpdate.DB_LAST_UPDATE_KEY, currentTime); editor.apply(); cancelNotification(notification_ID); return ListenableWorker.Result.success(new Data.Builder().putInt(SUCCESS_REASON_KEY, SUCCESS_UPDATE_DONE).build()); } public static Constraints getWorkConstraints(){ return new Constraints.Builder().setRequiredNetworkType(NetworkType.CONNECTED) .setRequiresCharging(false).build(); } public static WorkRequest newFirstTimeWorkRequest(){ return new OneTimeWorkRequest.Builder(DBUpdateWorker.class) .setBackoffCriteria(BackoffPolicy.LINEAR, 15, TimeUnit.SECONDS) //.setInputData(new Data.Builder().putBoolean()) .build(); } /* private int showNotification(@NonNull final NotificationManagerCompat notificManager, final int notification_ID, final String channel_ID){ final NotificationCompat.Builder builder = new NotificationCompat.Builder(getApplicationContext(), channel_ID) .setContentTitle("Libre BusTO - Updating Database") .setProgress(0,0,true) .setPriority(NotificationCompat.PRIORITY_LOW); builder.setSmallIcon(R.drawable.ic_bus_orange); notificManager.notify(notification_ID,builder.build()); return notification_ID; } */ private void cancelNotification(int notificationID){ final NotificationManagerCompat notificationManager = NotificationManagerCompat.from(getApplicationContext()); notificationManager.cancel(notificationID); } } diff --git a/app/src/main/java/it/reyboz/bustorino/viewmodels/LivePositionsViewModel.kt b/app/src/main/java/it/reyboz/bustorino/viewmodels/LivePositionsViewModel.kt index 92abb79..53c6e51 100644 --- a/app/src/main/java/it/reyboz/bustorino/viewmodels/LivePositionsViewModel.kt +++ b/app/src/main/java/it/reyboz/bustorino/viewmodels/LivePositionsViewModel.kt @@ -1,218 +1,225 @@ /* BusTO - ViewModel components Copyright (C) 2023 Fabio Mazza This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . */ package it.reyboz.bustorino.viewmodels import android.app.Application import android.util.Log import androidx.lifecycle.* import com.android.volley.DefaultRetryPolicy import com.android.volley.Response import it.reyboz.bustorino.backend.NetworkVolleyManager import it.reyboz.bustorino.backend.gtfs.GtfsRtPositionsRequest import it.reyboz.bustorino.backend.gtfs.LivePositionUpdate import it.reyboz.bustorino.backend.mato.MQTTMatoClient import it.reyboz.bustorino.data.GtfsRepository import it.reyboz.bustorino.data.MatoPatternsDownloadWorker import it.reyboz.bustorino.data.gtfs.TripAndPatternWithStops import kotlinx.coroutines.delay import kotlinx.coroutines.launch class LivePositionsViewModel(application: Application): AndroidViewModel(application) { private val gtfsRepo = GtfsRepository(application) //private val updates = UpdatesMap() private val updatesLiveData = MutableLiveData>() private val netVolleyManager = NetworkVolleyManager.getInstance(application) - private var mqttClient = MQTTMatoClient.getInstance() + private var mqttClient = MQTTMatoClient() private var lineListening = "" private var lastTimeReceived: Long = 0 private val gtfsRtRequestRunning = MutableLiveData(false) /** * Responder to the MQTT Client */ private val matoPositionListener = MQTTMatoClient.Companion.MQTTMatoListener{ val mupds = ArrayList() if(lineListening==MQTTMatoClient.LINES_ALL){ for(sdic in it.values){ for(update in sdic.values){ mupds.add(update) } } } else{ //we're listening to one if (it.containsKey(lineListening.trim()) ){ for(up in it[lineListening]?.values!!){ mupds.add(up) } } } val time = System.currentTimeMillis() if(lastTimeReceived == (0.toLong()) || (time-lastTimeReceived)>500){ updatesLiveData.postValue(mupds) lastTimeReceived = time } } //find the trip IDs in the updates private val tripsIDsInUpdates = updatesLiveData.map { it -> //Log.d(DEBUG_TI, "Updates map has keys ${upMap.keys}") it.map { pos -> "gtt:"+pos.tripID } } // get the trip IDs in the DB private val gtfsTripsPatternsInDB = tripsIDsInUpdates.switchMap { //Log.i(DEBUG_TI, "tripsIds in updates: ${it.size}") gtfsRepo.gtfsDao.getTripPatternStops(it) } //trip IDs to query, which are not present in the DB //REMEMBER TO OBSERVE THIS IN THE MAP val tripsGtfsIDsToQuery: LiveData> = gtfsTripsPatternsInDB.map { tripswithPatterns -> val tripNames=tripswithPatterns.map { twp-> twp.trip.tripID } Log.i(DEBUG_TI, "Have ${tripswithPatterns.size} trips in the DB") if (tripsIDsInUpdates.value!=null) return@map tripsIDsInUpdates.value!!.filter { !(tripNames.contains(it) || it.contains("null"))} else { Log.e(DEBUG_TI,"Got results for gtfsTripsInDB but not tripsIDsInUpdates??") return@map ArrayList() } } // unify trips with updates val updatesWithTripAndPatterns = gtfsTripsPatternsInDB.map { tripPatterns-> Log.i(DEBUG_TI, "Mapping trips and patterns") val mdict = HashMap>() //missing patterns val routesToDownload = HashSet() if(updatesLiveData.value!=null) for(update in updatesLiveData.value!!){ val trID:String = update.tripID var found = false for(trip in tripPatterns){ if (trip.pattern == null){ //pattern is null, which means we have to download // the pattern data from MaTO routesToDownload.add(trip.trip.routeID) } if (trip.trip.tripID == "gtt:$trID"){ found = true //insert directly mdict[trID] = Pair(update,trip) break } } if (!found){ //Log.d(DEBUG_TI, "Cannot find pattern ${tr}") //give the update anyway mdict[trID] = Pair(update,null) } } //have to request download of missing Patterns if (routesToDownload.size > 0){ Log.d(DEBUG_TI, "Have ${routesToDownload.size} missing patterns from the DB: $routesToDownload") //downloadMissingPatterns (ArrayList(routesToDownload)) MatoPatternsDownloadWorker.downloadPatternsForRoutes(routesToDownload.toList(), getApplication()) } return@map mdict } fun requestMatoPosUpdates(line: String){ lineListening = line viewModelScope.launch { mqttClient.startAndSubscribe(line,matoPositionListener, getApplication()) } //updatePositions(1000) } fun stopMatoUpdates(){ viewModelScope.launch { val tt = System.currentTimeMillis() - mqttClient.desubscribe(matoPositionListener) + mqttClient.stopMatoRequests(matoPositionListener) val time = System.currentTimeMillis() -tt Log.d(DEBUG_TI, "Took $time ms to unsubscribe") } } fun retriggerPositionUpdate(){ if(updatesLiveData.value!=null){ updatesLiveData.postValue(updatesLiveData.value) } } //Gtfs Real time private val gtfsPositionsReqListener = object: GtfsRtPositionsRequest.Companion.RequestListener{ override fun onResponse(response: ArrayList?) { Log.i(DEBUG_TI,"Got response from the GTFS RT server") response?.let {it:ArrayList -> if (it.size == 0) { Log.w(DEBUG_TI,"No position updates from the GTFS RT server") return } else { //Log.i(DEBUG_TI, "Posting value to positionsLiveData") viewModelScope.launch { updatesLiveData.postValue(it) } } } gtfsRtRequestRunning.postValue(false) } } private val positionRequestErrorListener = Response.ErrorListener { Log.e(DEBUG_TI, "Could not download the update", it) gtfsRtRequestRunning.postValue(false) } fun requestGTFSUpdates(){ if(gtfsRtRequestRunning.value == null || !gtfsRtRequestRunning.value!!) { val request = GtfsRtPositionsRequest(positionRequestErrorListener, gtfsPositionsReqListener) request.setRetryPolicy( DefaultRetryPolicy(1000,10,DefaultRetryPolicy.DEFAULT_BACKOFF_MULT) ) netVolleyManager.requestQueue.add(request) Log.i(DEBUG_TI, "Requested GTFS realtime position updates") gtfsRtRequestRunning.value = true } } fun requestDelayedGTFSUpdates(timems: Long){ viewModelScope.launch { delay(timems) requestGTFSUpdates() } } + override fun onCleared() { + //stop the MQTT Service + Log.d(DEBUG_TI, "Clearing the live positions view model, stopping the mqttClient") + mqttClient.disconnect() + super.onCleared() + } + companion object{ private const val DEBUG_TI = "BusTO-LivePosViewModel" } } \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_bus_stilized.xml b/app/src/main/res/drawable/ic_bus_stilized.xml new file mode 100644 index 0000000..5cd6688 --- /dev/null +++ b/app/src/main/res/drawable/ic_bus_stilized.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/ic_bus_stilized_transparent.xml b/app/src/main/res/drawable/ic_bus_stilized_transparent.xml new file mode 100644 index 0000000..ab8d6da --- /dev/null +++ b/app/src/main/res/drawable/ic_bus_stilized_transparent.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/values-it/strings.xml b/app/src/main/res/values-it/strings.xml index 6aec75d..48de94a 100644 --- a/app/src/main/res/values-it/strings.xml +++ b/app/src/main/res/values-it/strings.xml @@ -1,277 +1,281 @@ Stai utilizzando l\'ultimo ritrovato in materia di rispetto della tua privacy. Cerca QR Code Si No Prossimo Precedente Installare Barcode Scanner? Questa azione richiede un\'altra app per scansionare i codici QR. Vuoi installare Barcode Scanner? Numero fermata Nome fermata Inserisci il numero della fermata Inserisci il nome della fermata Verifica l\'accesso ad Internet! Sembra che nessuna fermata abbia questo nome Nessun passaggio trovato alla fermata Errore di lettura del sito 5T/GTT (dannato sito!) Fermata: %1$s Linea Linee Linee urbane Linee extraurbane Linee turistiche Direzione: Linea: %1$s Linee: %1$s Scegli la fermata… Nessun passaggio Nessun QR code trovato, prova ad usare un\'altra app Preferiti Aiuto Informazioni Più informazioni Contribuisci https://gitpull.it/w/librebusto/it/ Codice sorgente Licenza Incontra l\'autore Fermata aggiunta ai preferiti Impossibile aggiungere ai preferiti (memoria piena o database corrotto?)! Preferiti Mappa Nessun preferito? Arghh!\nSchiaccia sulla stella di una fermata per aggiungerla a questa lista! Rimuovi Rinomina Rinomina fermata Reset Informazioni Tocca la stella per aggiungere la fermata ai preferiti\n\nCome leggere gli orari: \n   12:56* Orario in tempo reale\n   12:56   Orario programmato\n\nTrascina giù per aggiornare l\'orario. \nTocca a lungo su Fonte Orari per cambiare sorgente degli orari di arrivo. OK! Benvenuto!

Grazie per aver scelto BusTO, un\'app indipendente da GTT/5T, per spostarsi a Torino attraverso software libero:


Perché usare BusTO?

- Non sei monitorato
- Non ci sono pubblicità
- La tua privacy è al sicuro
- Inoltre l\'app è molto leggera!


Come funziona?

Quest\'app ottiene i passaggi dei bus, le fermate e altre informazioni utili unendo dati forniti dal sito www.gtt.to.it, www.5t.torino.it, muoversiatorino.it "per uso personale" e altre fonti Open Data (aperto.comune.torino.it).


Ingredienti:
- Fabio Mazza attuale rockstar developer anziano.
- Andrea Ugo attuale rockstar developer in formazione.
- Silviu Chiriac designer del logo 2021.
- Marco M formidabile tester e cacciatore di bug.
- Ludovico Pavesi ex rockstar developer anziano asd.
- Valerio Bozzolan attuale manutentore.
- Marco Gagino apprezzato ex collaboratore, ideatore icona e grafica.
- JSoup libreria per "web scaping".
- Google icone e libreria di supporto per il Material Design.
- Tutti i contributori e i beta tester!


Licenze

L\'app e il relativo codice sorgente sono distribuiti sotto la licenza GNU General Public License v3+. Ciò significa che puoi usare, studiare, migliorare e ricondividere quest\'app con qualunque mezzo e per qualsiasi scopo: a patto di mantenere sempre questi diritti a tua volta e di dare credito a Valerio Bozzolan.


Note

Quest\'applicazione è rilasciata nella speranza che sia utile a tutti ma senza NESSUNA garanzia sul suo funzionamento attuale e/o futuro.

Tutti i dati utilizzati dall\'app provengono direttamente da GTT o da simili agenzie pubbliche: se trovi che sono inesatti per qualche motivo, ti invitiamo a rivolgerti a loro.

Buon utilizzo! :)

]]>
Nome troppo corto, digita più caratteri e riprova %1$s verso %2$s %s (destinazione sconosciuta) Errore interno inaspettato, impossibile estrarre dati dal sito GTT/5T Visualizza sulla mappa Non trovo un\'applicazione dove mostrarla Posizione della fermata non trovata Fermate vicine Ricerca della posizione Nessuna fermata nei dintorni Preferenze Aggiornamento del database… Aggiornamento del database Aggiornamento database forzato Tocca per aggiornare ora il database Numero minimo di fermate Il numero di fermate da ricercare non è valido Valore errato, inserisci un numero Impostazioni Distanza massima di ricerca (m) Funzionalità sperimentali Impostazioni Generali Fermate recenti Impostazioni generali Gestione del database Comincia aggiornamento manuale del database Consenti l\'accesso alla posizione per mostrarla sulla mappa + Consenti l\'accesso alla posizione per mostrare le fermate vicine + Abilitare il GPS arriva alle alla fermata Mostra arrivi Mostra fermate Arrivi qui vicino Fermata rimossa dai preferiti Canale Telegram La mia posizione Segui posizione Attiva o disattiva posizione Posizione attivata Posizione disattivata La posizione è disabilitata sul dispositivo Fonte orari: %1$s App GTT Sito GTT Sito 5T Torino App Muoversi a Torino Sconosciuta Fonti orari di arrivo Scegli le fonti di orari da usare Cambiamento sorgente orari… Premi a lungo per cambiare la sorgente degli orari Nessun passaggio per le linee: Canale default delle notifiche Operazioni sul database Informazioni sul database (aggiornamento) + Servizio posizioni in tempo reale attivo + Downloading trips from MaTO server Chiesto troppe volte per il permesso %1$s Non si può usare questa funzionalità senza il permesso di archivio di archivio Un bug ha fatto crashare l\'app! \nPremi \"OK\" per inviare il report agli sviluppatori via email, così potranno scovare e risolvere il tuo bug! \nIl report contiene piccole informazioni non sensibili sulla configurazione del tuo telefono e sullo stato dell\'app al momento del crash. L\'applicazione è crashata, e il crash report è stato messo negli allegati. Se vuoi, descrivi cosa stavi facendo prima che si interrompesse: \n Arrivi Mappa Preferiti Apri drawer Chiudi drawer Esperimenti Offrici un caffè Mappa Ricerca fermate Versione app Orari di arrivo Richiesto aggiornamento del database Download dati dal server MaTO Mostra direzioni in maiuscolo Non cambiare Tutto in maiuscolo Solo la prima lettera maiuscola Mostra arrivi quando tocchi una fermata Abilita esperimenti Schermata da mostrare all\'avvio Tocca per cambiare Fonte posizioni in tempo reale di bus e tram MaTO (aggiornate più spesso, può non funzionare) GTFS RT (più stabile) Linea aggiunta ai preferiti Linea rimossa dai preferiti Preferite Tocca a lungo la fermata per le opzioni Rimuovi i dati dei trip (libera spazio) Tutti i trip GTFS sono rimossi dal database Mostra introduzione open source per il trasporto pubblico di Torino. Stai usando un\'app indipendente, senza pubblicità e senza nessun tracciamento. ]]> Se ti trovi a una fermata, puoi scansionare il codice QR presente sulla palina toccando l\'icona a sinistra della barra di ricerca.]]> preferiti toccando la stella a fianco del nome.]]> fermate più vicine a te direttamente nella schermata principale...]]> posizioni in tempo reale dei bus e tram (in blu)]]> Guarda nelle Impostazioni per personalizzare l\'app come preferisci, e su Informazioni per sapere di più sull\'app e il team di sviluppo.]]> Capito, chiudi introduzione Chiudi introduzione Abilita accesso alla posizione Accesso alla posizione abilitato Accesso alla posizione non consentito dall\'utente Abilita notifiche Notifiche abilitate
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 4697434..2e10797 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -1,308 +1,309 @@ BusTO Libre BusTO BusTO dev BusTO git You\'re using the latest in technology when it comes to respecting your privacy. Search Scan QR Code Yes No Next Previous Install Barcode Scanner? This application requires an app to scan the QR codes. Would you like to install Barcode Scanner now? Bus stop number Bus stop name Insert bus stop number Insert bus stop name %1$s towards %2$s %s (unknown destination) Verify your Internet connection! Seems that no bus stop have this name No arrivals found for this stop Error parsing the 5T/GTT website (damn site!) Name too short, type more characters and retry Arrivals at: %1$s Choose the bus stop… Line Lines Urban lines Extra urban lines Tourist lines Destination: Lines: %1$s Line: %1$s No timetable found No QR code found, try using another app to scan Unexpected internal error, cannot extract data from GTT/5T website Help About the app More about Contribute https://gitpull.it/w/librebusto/en/ Source code Licence11 Meet the author Bus stop is now in your favorites Bus stop removed from your favorites Added line to favorites Remove line from favorites Favorites Favorites Favorites Map No favorites? Arghh! Press on a bus stop star to populate this list! Delete Rename Rename the bus stop Reset About the app Tap the star to add the bus stop to the favourites\n\nHow to read timelines:\n   12:56* Real-time arrivals\n   12:56   Scheduled arrivals\n\nPull down to refresh the timetable \n Long press on Arrivals source to change the source of the arrival times GOT IT! Arrival times No arrivals found for lines: Welcome!

Thanks for using BusTO, a "politically" independent app useful to move around Torino using a Free/Libre software.


Why use this app?

- You\'ll never be tracked
- You\'ll never see boring ads
- We\'ll always respect your privacy
- Moreover, it\'s lightweight!


How does it work?

This app is able to do all the amazing things it does by pulling data from www.gtt.to.it, www.5t.torino.it or muoversiatorino.it "for personal use", along with open data from the AperTO (aperto.comune.torino.it) website.


The work of several people is behind this app, in particular:
- Fabio Mazza, current senior rockstar developer.
- Andrea Ugo, current junior rockstar developer.
- Silviu Chiriac, designer of the 2021 logo.
- Marco M, rockstar tester and bug hunter.
- Ludovico Pavesi, previous senior rockstar developer (asd).
- Valerio Bozzolan, maintainer and infrastructure (sponsor).
- Marco Gagino, contributor and first icon creator.
- JSoup web scraper library.
- makovkastar floating buttons.
- Google Material Design icons and Volley framework.
- Android app components.
- All the contributors, and the beta testers, too!


Licenses

The app and the related source code are released by Valerio Bozzolan and the other authors under the terms of the GNU General Public License v3+). So everyone is allowed to use, to study, to improve and to share this app by any kind of means and for any purpose: under the conditions of maintaining this rights and of attributing the original work to Valerio Bozzolan.


Notes

This app has been developed with the hope to be useful to everyone, but comes without ANY warranty of any kind.

The data used by the app comes directly from GTT and other public agencies: if you find any errors, please take it up to them, not to us.

This translation is kindly provided by Riccardo Caniato, Marco Gagino and Fabio Mazza.

Now you can hack public transport, too! :)

]]>
Cannot add to favorites (storage full or corrupted database?)! View on a map Cannot find any application to show it in Cannot find the position of the stop ListFragment - BusTO it.reyboz.bustorino.preferences db_is_updating Nearby stops Nearby connections App version The number of stops to show in the recent stops is invalid Invalid value, put a valid number Finding location No stops nearby Minimum number of stops Preferences Settings Settings General Experimental features Maximum distance (meters) Recent stops General settings Database management Launch manual database update Allow access to location to show it on the map Allow access to location to show stops nearby Please enable location on the device Database update in progress… Updating the database Force database update Touch to update the app database now is arriving at at the stop %1$s - %2$s Show arrivals Show stops Join Telegram channel Center on my location Follow me Enable or disable location Location enabled Location disabled Location is disabled on device Arrivals source: %1$s GTT App GTT Website 5T Torino website Muoversi a Torino app Undetermined Changing arrival times source… Long press to change the source of arrivals @string/source_mato @string/fivetapifetcher @string/gttjsonfetcher @string/fivetscraper Sources of arrival times Select which sources of arrival times to use Default Default channel for notifications Database operations Updates of the app database + Bus live positions service is running Downloading trips from MaTO server Asked for %1$s permission too many times Cannot use the map with the storage permission! storage The application has crashed because you encountered a bug. \nIf you want, you can help the developers by sending the crash report via email. \nNote that no sensitive data is contained in the report, just small bits of info on your phone and app configuration/state. The application crashed and the crash report is in the attachments. Please describe what you were doing before the crash: \n Arrivals Map Favorites Open navigation drawer Close navigation drawer Experiments Buy us a coffee Map Search by stop Launching database update Downloading data from MaTO server Capitalize directions Do not change arrivals directions Capitalize everything Capitalize only first letter KEEP CAPITALIZE_ALL CAPITALIZE_FIRST Section to show on startup Touch to change it Show arrivals touching on stop Enable experiments Long press the stop for options @string/nav_arrivals_text @string/nav_favorites_text @string/nav_map_text @string/lines Source of real time positions for buses and trams MaTO (updated more frequently, might be offline) GTFS RT (more stable, less frequently updated) Remove trips data (free up space) All GTFS trips have been removed from the database Show tutorial open source app for Turin public transport. This is an independent app, with no ads and no tracking whatsoever.]]> favorites by touching the star next to its name]]> blue)]]> Settings to customize the app behaviour, and in the About the app section if you want to know more about the app and the developers.]]> Notifications permission to show the information about background processing. Press the button below to grant it]]> Grant location permission Location permission granted Location permission has not been granted OK, close the tutorial Close the tutorial Enable notifications Notifications enabled
diff --git a/build.gradle b/build.gradle index 834bb8b..7e05677 100644 --- a/build.gradle +++ b/build.gradle @@ -1,49 +1,49 @@ // Top-level build file where you can add configuration options common to all sub-projects/modules. buildscript { repositories { mavenCentral() maven { url 'https://maven.google.com' } google() maven { url 'https://jitpack.io' } } //kotlin ext.kotlin_version = '1.9.0' ext.coroutines_version = "1.7.3" dependencies { classpath 'com.android.tools.build:gradle:8.1.4' classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" } } ext { androidXTestVersion = "1.5.0" //multidex multidex_version = "2.0.1" //libraries versions fragment_version = "1.6.1" activity_version = "1.7.2" appcompat_version = "1.6.1" preference_version = "1.2.1" - work_version = "2.8.1" + work_version = "2.9.0" acra_version = "5.7.0" lifecycle_version = "2.4.1" arch_version = "2.1.0" room_version = "2.5.2" } allprojects { repositories { maven { url 'https://maven.google.com' } google() mavenCentral() maven { url "https://jitpack.io" } } }