commit a330488f6982f82c80ec616756a94135042691c1 Author: Fabio Mazza Date: Wed Apr 10 22:36:52 2024 +0200 Re-enable foreground service with special notification channel, upgrade ACRA version Summary: Insert back the foreground service notification (and permission), complying with Play Store Also, avoid non-stop request of trip data that simply fails to download, giving rise to less notifications on devices that run Android < 12. Test Plan: Check nothing is broken Reviewers: valerio.bozzolan Reviewed By: valerio.bozzolan Subscribers: valerio.bozzolan, #libre_busto Tags: #libre_busto Differential Revision: https://gitpull.it/D161 diff --git a/app/build.gradle b/app/build.gradle index 8ec0138..c3d1a89 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -12,8 +12,8 @@ android { minSdkVersion 21 targetSdkVersion 34 buildToolsVersion = '34.0.0' - versionCode 57 - versionName "2.2.0" + versionCode 58 + versionName "2.2.1" vectorDrawables.useSupportLibrary = true multiDexEnabled true javaCompileOptions { @@ -70,6 +70,9 @@ dependencies { 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' + + // Guava implementation for DBUpdateWorker + implementation 'com.google.guava:guava:29.0-android' implementation "androidx.fragment:fragment-ktx:$fragment_version" implementation "androidx.activity:activity:$activity_version" @@ -83,7 +86,7 @@ dependencies { implementation "androidx.work:work-runtime-ktx:$work_version" - implementation "com.google.android.material:material:1.9.0" + implementation "com.google.android.material:material:1.11.0" implementation 'androidx.constraintlayout:constraintlayout:2.1.4' implementation "androidx.coordinatorlayout:coordinatorlayout:1.2.0" @@ -102,11 +105,9 @@ dependencies { 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.2' + implementation 'com.github.hannesa2:paho.mqtt.android:4.2.4' //implementation 'com.github.fabmazz:paho.mqtt.android:v0.0.1' - - // ViewModel implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:$lifecycle_version" // LiveData diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index d2811d9..c7f53e0 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -12,9 +12,10 @@ - + --> @@ -133,6 +134,8 @@ android:name="android.support.PARENT_ACTIVITY" android:value="it.reyboz.bustorino.ActivityPrincipal"/> + + \ No newline at end of file diff --git a/app/src/main/java/it/reyboz/bustorino/BustoApp.java b/app/src/main/java/it/reyboz/bustorino/BustoApp.java index a80c054..d9e2916 100644 --- a/app/src/main/java/it/reyboz/bustorino/BustoApp.java +++ b/app/src/main/java/it/reyboz/bustorino/BustoApp.java @@ -28,29 +28,44 @@ import org.acra.config.DialogConfigurationBuilder; import org.acra.config.MailSenderConfigurationBuilder; import org.acra.data.StringFormat; +import java.lang.reflect.Array; +import java.util.ArrayList; +import java.util.List; + import static org.acra.ReportField.*; public class BustoApp extends MultiDexApplication { - private static final ReportField[] REPORT_FIELDS = {REPORT_ID, APP_VERSION_CODE, APP_VERSION_NAME, + private static final List REPORT_FIELDS = List.of(REPORT_ID, APP_VERSION_CODE, APP_VERSION_NAME, PACKAGE_NAME, PHONE_MODEL, BRAND, PRODUCT, ANDROID_VERSION, BUILD_CONFIG, CUSTOM_DATA, IS_SILENT, STACK_TRACE, INITIAL_CONFIGURATION, CRASH_CONFIGURATION, DISPLAY, USER_COMMENT, - USER_APP_START_DATE, USER_CRASH_DATE, LOGCAT, SHARED_PREFERENCES}; + USER_APP_START_DATE, USER_CRASH_DATE, LOGCAT, SHARED_PREFERENCES); @Override protected void attachBaseContext(Context base) { super.attachBaseContext(base); - CoreConfigurationBuilder builder = new CoreConfigurationBuilder(this); - builder.setBuildConfigClass(BuildConfig.class).setReportFormat(StringFormat.JSON) - .setDeleteUnapprovedReportsOnApplicationStart(true); - builder.getPluginConfigurationBuilder(MailSenderConfigurationBuilder.class).setMailTo("gtt@succhia.cz") - .setReportFileName(it.reyboz.bustorino.BuildConfig.VERSION_NAME +"_report.json") - .setResBody(R.string.acra_email_message) - .setEnabled(true); - builder.getPluginConfigurationBuilder(DialogConfigurationBuilder.class).setResText(R.string.message_crash) - .setResTheme(R.style.AppTheme) + CoreConfigurationBuilder builder = new CoreConfigurationBuilder(); + // mail stuff + MailSenderConfigurationBuilder mailConfig = new MailSenderConfigurationBuilder(); + mailConfig.withMailTo("gtt@succhia.cz") + .withReportFileName(it.reyboz.bustorino.BuildConfig.VERSION_NAME +"_report.json") + .withBody(getString(R.string.acra_email_message)) .setEnabled(true); + //dialog stuff + DialogConfigurationBuilder dialogBuild = new DialogConfigurationBuilder(); + dialogBuild.withText(getString(R.string.message_crash)) + .withResTheme(R.style.AppTheme).setEnabled(true); + //Set options + builder.withBuildConfigClass(BuildConfig.class) + .withReportFormat(StringFormat.JSON) + .withDeleteUnapprovedReportsOnApplicationStart(true); + //Add plugins + builder.withPluginConfigurations( + mailConfig.build(), dialogBuild.build() + ); + + builder.setReportContent(REPORT_FIELDS); if (!it.reyboz.bustorino.BuildConfig.DEBUG) ACRA.init(this, builder); 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 2fd2ec3..207d897 100644 --- a/app/src/main/java/it/reyboz/bustorino/backend/Notifications.java +++ b/app/src/main/java/it/reyboz/bustorino/backend/Notifications.java @@ -11,6 +11,7 @@ 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"; + public static final String MATO_LIVE_POSITIONS_CHANNEL="Live Positions"; //match this value to the one used by the MQTTAndroidClient MANUALLY @@ -64,12 +65,27 @@ public class Notifications { .setContentText(title) .build(); } + + public static Notification makeLivePositionsNotification(Context context,String title){ + return new NotificationCompat.Builder(context, Notifications.MATO_LIVE_POSITIONS_CHANNEL) + //.setContentIntent(PendingIntent.getActivity(context, 0, Intent(context, MainActivity::class.java), Constants.PENDING_INTENT_FLAG_IMMUTABLE)) + .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)); + return makeLivePositionsNotification(context, context.getString(R.string.mqtt_notification_text)); } public static void cancelNotification(Context context, int notificationID){ @@ -88,4 +104,18 @@ public class Notifications { notificationManager.createNotificationChannel(channel); } } + + public static void createLivePositionsChannel(Context context){ + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + NotificationChannel channel = new NotificationChannel( + Notifications.MATO_LIVE_POSITIONS_CHANNEL, + context.getString(R.string.live_positions_notification_channel), + NotificationManager.IMPORTANCE_MIN + ); + channel.setDescription(context.getString(R.string.live_positions_notification_channel_desc)); + + 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 7df1b0c..e15a1b1 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 @@ -48,12 +48,15 @@ class MQTTMatoClient(): MqttCallbackExtended{ 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){ + // update, 2024-04: Google Play doesn't understand our needs, so we put back the notification + // and add a video of it working as Google wants + if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.O){ //we need a notification + Notifications.createLivePositionsChannel(context) val notific = Notifications.makeMQTTServiceNotification(context) - client.setForegroundService(notific) + client!!.setForegroundService(notific) notification=notific - }*/ + } val options = MqttConnectOptions() 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 9ad512e..81866e9 100644 --- a/app/src/main/java/it/reyboz/bustorino/data/DBUpdateWorker.java +++ b/app/src/main/java/it/reyboz/bustorino/data/DBUpdateWorker.java @@ -34,6 +34,7 @@ import java.util.concurrent.atomic.AtomicReference; import static android.content.Context.MODE_PRIVATE; +//TODO: Move to code to Kotlin public class DBUpdateWorker extends Worker{ diff --git a/app/src/main/java/it/reyboz/bustorino/data/MatoTripsDownloadWorker.kt b/app/src/main/java/it/reyboz/bustorino/data/MatoTripsDownloadWorker.kt index 5e391ef..0f03a28 100644 --- a/app/src/main/java/it/reyboz/bustorino/data/MatoTripsDownloadWorker.kt +++ b/app/src/main/java/it/reyboz/bustorino/data/MatoTripsDownloadWorker.kt @@ -82,15 +82,20 @@ class MatoTripsDownloadWorker(appContext: Context, workerParams: WorkerParameter } requestCountDown.await() val tripsIDsCompleted = downloadedMatoTrips.map { trip-> trip.tripID } - val doInsert = (queriedMatoTrips subtract failedMatoTripsDownload).containsAll(tripsIDsCompleted) - Log.i(DEBUG_TAG, "Inserting missing GtfsTrips in the database, should insert $doInsert") - if(doInsert){ + if (tripsIDsCompleted.isEmpty()){ + Log.d(DEBUG_TAG, "No trips have been downloaded, set work to fail") + return Result.failure() + } else { + val doInsert = (queriedMatoTrips subtract failedMatoTripsDownload).containsAll(tripsIDsCompleted) + Log.i(DEBUG_TAG, "Inserting missing GtfsTrips in the database, should insert $doInsert") + if (doInsert) { - gtfsRepository.gtfsDao.insertTrips(downloadedMatoTrips) + gtfsRepository.gtfsDao.insertTrips(downloadedMatoTrips) - } + } - return Result.success() + return Result.success() + } } override suspend fun getForegroundInfo(): ForegroundInfo { val notificationManager = @@ -109,8 +114,8 @@ class MatoTripsDownloadWorker(appContext: Context, workerParams: WorkerParameter const val TAG_TRIPS ="gtfsTripsDownload" - fun downloadTripsFromMato(trips: List, context: Context, debugTag: String): Boolean{ - if (trips.isEmpty()) return false + fun requestMatoTripsDownload(trips: List, context: Context, debugTag: String): OneTimeWorkRequest? { + if (trips.isEmpty()) return null val workManager = WorkManager.getInstance(context) val info = workManager.getWorkInfosForUniqueWork(TAG_TRIPS).get() @@ -128,8 +133,8 @@ class MatoTripsDownloadWorker(appContext: Context, workerParams: WorkerParameter .addTag(TAG_TRIPS) .build() workManager.enqueueUniqueWork(TAG_TRIPS, ExistingWorkPolicy.KEEP, requ) - } - return true + return requ + } else return null; } } } diff --git a/app/src/main/java/it/reyboz/bustorino/fragments/LinesDetailFragment.kt b/app/src/main/java/it/reyboz/bustorino/fragments/LinesDetailFragment.kt index 5a15293..98cf68c 100644 --- a/app/src/main/java/it/reyboz/bustorino/fragments/LinesDetailFragment.kt +++ b/app/src/main/java/it/reyboz/bustorino/fragments/LinesDetailFragment.kt @@ -366,7 +366,7 @@ class LinesDetailFragment() : ScreenBaseFragment() { //download missing tripIDs liveBusViewModel.tripsGtfsIDsToQuery.observe(viewLifecycleOwner){ //gtfsPosViewModel.downloadTripsFromMato(dat); - MatoTripsDownloadWorker.downloadTripsFromMato( + MatoTripsDownloadWorker.requestMatoTripsDownload( it, requireContext().applicationContext, "BusTO-MatoTripDownload" ) diff --git a/app/src/main/java/it/reyboz/bustorino/fragments/MapFragment.java b/app/src/main/java/it/reyboz/bustorino/fragments/MapFragment.java index 0035c62..6e3992e 100644 --- a/app/src/main/java/it/reyboz/bustorino/fragments/MapFragment.java +++ b/app/src/main/java/it/reyboz/bustorino/fragments/MapFragment.java @@ -46,7 +46,6 @@ import androidx.preference.PreferenceManager; import it.reyboz.bustorino.backend.gtfs.LivePositionUpdate; import it.reyboz.bustorino.backend.mato.MQTTMatoClient; import it.reyboz.bustorino.backend.utils; -import it.reyboz.bustorino.data.MatoTripsDownloadWorker; import it.reyboz.bustorino.data.gtfs.MatoPattern; import it.reyboz.bustorino.data.gtfs.TripAndPatternWithStops; import it.reyboz.bustorino.map.*; @@ -374,12 +373,16 @@ public class MapFragment extends ScreenBaseFragment { else livePositionsViewModel.requestGTFSUpdates(); //mapViewModel.testCascade(); + livePositionsViewModel.isLastWorkResultGood().observe(this, d -> + Log.d(DEBUG_TAG, "Last trip download result is "+d)); livePositionsViewModel.getTripsGtfsIDsToQuery().observe(this, dat -> { Log.i(DEBUG_TAG, "Have these trips IDs missing from the DB, to be queried: "+dat); - //gtfsPosViewModel.downloadTripsFromMato(dat); - MatoTripsDownloadWorker.Companion.downloadTripsFromMato(dat, + livePositionsViewModel.downloadTripsFromMato(dat); + /*MatoTripsDownloadWorker.Companion.requestMatoTripsDownload(dat, requireContext().getApplicationContext(), "BusTO-MatoTripDownload"); + + */ }); } /*else if(gtfsPosViewModel!=null){ gtfsPosViewModel.requestUpdates(); diff --git a/app/src/main/java/it/reyboz/bustorino/viewmodels/GtfsPositionsViewModel.kt b/app/src/main/java/it/reyboz/bustorino/viewmodels/GtfsPositionsViewModel.kt index 5b1e26b..aa195cf 100644 --- a/app/src/main/java/it/reyboz/bustorino/viewmodels/GtfsPositionsViewModel.kt +++ b/app/src/main/java/it/reyboz/bustorino/viewmodels/GtfsPositionsViewModel.kt @@ -20,6 +20,7 @@ package it.reyboz.bustorino.viewmodels import android.app.Application import android.util.Log import androidx.lifecycle.* +import androidx.work.OneTimeWorkRequest import com.android.volley.Response import it.reyboz.bustorino.backend.NetworkVolleyManager import it.reyboz.bustorino.backend.gtfs.LivePositionUpdate @@ -166,8 +167,8 @@ class GtfsPositionsViewModel(application: Application): AndroidViewModel(applica Keep in mind that trips DO CHANGE often, and so do the Patterns */ - fun downloadTripsFromMato(trips: List): Boolean{ - return MatoTripsDownloadWorker.downloadTripsFromMato(trips,getApplication(), DEBUG_TI) + fun downloadTripsFromMato(trips: List): OneTimeWorkRequest?{ + return MatoTripsDownloadWorker.requestMatoTripsDownload(trips,getApplication(), "BusTO-MatoTripsDown") } private fun downloadMissingPatterns(routeIds: List): Boolean{ return MatoPatternsDownloadWorker.downloadPatternsForRoutes(routeIds, getApplication()) 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 53c6e51..27f3df6 100644 --- a/app/src/main/java/it/reyboz/bustorino/viewmodels/LivePositionsViewModel.kt +++ b/app/src/main/java/it/reyboz/bustorino/viewmodels/LivePositionsViewModel.kt @@ -20,6 +20,8 @@ package it.reyboz.bustorino.viewmodels import android.app.Application import android.util.Log import androidx.lifecycle.* +import androidx.work.WorkInfo +import androidx.work.WorkManager import com.android.volley.DefaultRetryPolicy import com.android.volley.Response import it.reyboz.bustorino.backend.NetworkVolleyManager @@ -28,9 +30,14 @@ 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.MatoTripsDownloadWorker import it.reyboz.bustorino.data.gtfs.TripAndPatternWithStops import kotlinx.coroutines.delay import kotlinx.coroutines.launch +import java.util.* +import kotlin.collections.ArrayList +import kotlin.collections.HashMap +import kotlin.collections.HashSet class LivePositionsViewModel(application: Application): AndroidViewModel(application) { @@ -49,6 +56,27 @@ class LivePositionsViewModel(application: Application): AndroidViewModel(applica private val gtfsRtRequestRunning = MutableLiveData(false) + private val lastFailedTripsRequest = HashMap() + private val workManager = WorkManager.getInstance(application) + + private var lastRequestedDownloadTrips = MutableLiveData>() + + var isLastWorkResultGood = workManager + .getWorkInfosForUniqueWorkLiveData(MatoTripsDownloadWorker.TAG_TRIPS).map { it -> + if (it.isEmpty()) return@map false + var res = true + if(it[0].state == WorkInfo.State.FAILED){ + val currDate = Date() + res = false + lastRequestedDownloadTrips.value?.let { trips-> + for(tr in trips){ + lastFailedTripsRequest[tr] = currDate + } + } + + } + return@map res + } /** * Responder to the MQTT Client */ @@ -218,8 +246,42 @@ class LivePositionsViewModel(application: Application): AndroidViewModel(applica mqttClient.disconnect() super.onCleared() } + //Request trips download + fun downloadTripsFromMato(trips: List): Boolean{ + if(trips.isEmpty()) + return false + var shouldContinue = false + val currentDateTime = Date().time + + for (tr in trips){ + if (!lastFailedTripsRequest.containsKey(tr)){ + shouldContinue = true + break + } else{ + //Log.i(DEBUG_TI, "Last time the trip has failed is ${lastFailedTripsRequest[tr]}") + if ((lastFailedTripsRequest[tr]!!.time - currentDateTime) > MAX_TIME_RETRY){ + shouldContinue =true + break + } + } + } + if (shouldContinue) { + //if one trip + val workRequ =MatoTripsDownloadWorker.requestMatoTripsDownload(trips, getApplication(), "BusTO-MatoTripsDown") + workRequ?.let { req -> + Log.d(DEBUG_TI, "Enqueueing new work, saving work info") + lastRequestedDownloadTrips.postValue(trips) + //isLastWorkResultGood = + } + } else{ + Log.w(DEBUG_TI, "Requested to fetch data for ${trips.size} trips but they all have failed before in the last $MAX_MINUTES_RETRY mins") + } + return shouldContinue + } companion object{ private const val DEBUG_TI = "BusTO-LivePosViewModel" + private const val MAX_MINUTES_RETRY = 3 + private const val MAX_TIME_RETRY = MAX_MINUTES_RETRY*60*1000 //3 minutes (in milliseconds) } } \ No newline at end of file diff --git a/app/src/main/res/values-it/strings.xml b/app/src/main/res/values-it/strings.xml index 63cf4f2..f8e5690 100644 --- a/app/src/main/res/values-it/strings.xml +++ b/app/src/main/res/values-it/strings.xml @@ -177,7 +177,12 @@ Canale default delle notifiche Operazioni sul database Informazioni sul database (aggiornamento) - Servizio posizioni in tempo reale attivo + BusTO - posizioni in tempo reale + + Posizioni in tempo reale + Attività del servizio delle posizioni in tempo reale + + Servizio posizioni MaTO in tempo reale attivo Downloading trips from MaTO server diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 7d74a62..7ee94cf 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -203,7 +203,10 @@ Default channel for notifications Database operations Updates of the app database - Bus live positions service is running + BusTO - live position service + Live positions + Showing activity related to the live positions service + MaTO live bus positions service is running Downloading trips from MaTO server diff --git a/build.gradle b/build.gradle index 7e05677..1b79b23 100644 --- a/build.gradle +++ b/build.gradle @@ -11,7 +11,7 @@ buildscript { } //kotlin ext.kotlin_version = '1.9.0' - ext.coroutines_version = "1.7.3" + ext.coroutines_version = "1.8.0" dependencies { classpath 'com.android.tools.build:gradle:8.1.4' @@ -31,8 +31,8 @@ ext { preference_version = "1.2.1" work_version = "2.9.0" - acra_version = "5.7.0" - lifecycle_version = "2.4.1" + acra_version = "5.11.3" + lifecycle_version = "2.7.0" arch_version = "2.1.0" room_version = "2.5.2"