diff --git a/onebusaway-android/build.gradle b/onebusaway-android/build.gradle index 7b70d6e4c..f02fe5da5 100644 --- a/onebusaway-android/build.gradle +++ b/onebusaway-android/build.gradle @@ -49,6 +49,13 @@ android { // This enables us to tell when we're running unit tests on CI (#1010 for Travis, #1072 for GitHub) buildConfigField("String", "CI", "\"" + System.getenv('CI') + "\"") + + // Configure Java compile options to specify the schema location for Room database to keep track of schema versions + javaCompileOptions { + annotationProcessorOptions { + arguments += ["room.schemaLocation": "$projectDir/schemas".toString()] + } + } } /** @@ -320,7 +327,12 @@ dependencies { implementation 'com.google.firebase:firebase-core:21.1.1' implementation 'com.google.firebase:firebase-analytics:22.1.2' // Cloud Firestore (for storing destination alert test data) - implementation 'com.google.firebase:firebase-firestore:25.1.0' + implementation('com.google.firebase:firebase-firestore:25.1.0') { + // Exclude protobuf-lite and protolite-well-known-types to avoid conflicts with GTFS-realtime bindings + // See https://github.com/firebase/firebase-android-sdk/issues/5997 + exclude group: 'com.google.firebase', module: 'protolite-well-known-types' + exclude group: 'com.google.protobuf', module: 'protobuf-javalite' + } implementation 'com.google.firebase:firebase-auth:21.0.5' implementation 'com.google.firebase:firebase-storage:21.0.1' // Firebase Crashlytics @@ -381,7 +393,8 @@ dependencies { implementation "androidx.room:room-runtime:2.6.1" kapt "androidx.room:room-compiler:2.6.1" implementation "androidx.room:room-ktx:2.6.1" - + // GTFS Realtime bindings for parsing GTFS-realtime data + implementation group: 'org.mobilitydata', name: 'gtfs-realtime-bindings', version: '0.0.8' } apply plugin:'com.google.gms.google-services' diff --git a/onebusaway-android/schemas/org.onebusaway.android.database.AppDatabase/2.json b/onebusaway-android/schemas/org.onebusaway.android.database.AppDatabase/2.json new file mode 100644 index 000000000..372f3ad5c --- /dev/null +++ b/onebusaway-android/schemas/org.onebusaway.android.database.AppDatabase/2.json @@ -0,0 +1,192 @@ +{ + "formatVersion": 1, + "database": { + "version": 2, + "identityHash": "4db170986b81dc2388f0e2a0f3aee2f0", + "entities": [ + { + "tableName": "studies", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`study_id` INTEGER NOT NULL, `name` TEXT NOT NULL, `description` TEXT NOT NULL, `is_subscribed` INTEGER NOT NULL, PRIMARY KEY(`study_id`))", + "fields": [ + { + "fieldPath": "study_id", + "columnName": "study_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "description", + "columnName": "description", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "is_subscribed", + "columnName": "is_subscribed", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "study_id" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "surveys", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`survey_id` INTEGER NOT NULL, `study_id` INTEGER NOT NULL, `name` TEXT NOT NULL, `state` INTEGER NOT NULL, PRIMARY KEY(`survey_id`), FOREIGN KEY(`study_id`) REFERENCES `studies`(`study_id`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "survey_id", + "columnName": "survey_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "study_id", + "columnName": "study_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "state", + "columnName": "state", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "survey_id" + ] + }, + "indices": [], + "foreignKeys": [ + { + "table": "studies", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "study_id" + ], + "referencedColumns": [ + "study_id" + ] + } + ] + }, + { + "tableName": "regions", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`regionId` INTEGER NOT NULL, PRIMARY KEY(`regionId`))", + "fields": [ + { + "fieldPath": "regionId", + "columnName": "regionId", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "regionId" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "stops", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`stop_id` TEXT NOT NULL, `name` TEXT NOT NULL, `regionId` INTEGER NOT NULL, `timestamp` INTEGER NOT NULL, PRIMARY KEY(`stop_id`), FOREIGN KEY(`regionId`) REFERENCES `regions`(`regionId`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "stop_id", + "columnName": "stop_id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "regionId", + "columnName": "regionId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "timestamp", + "columnName": "timestamp", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "stop_id" + ] + }, + "indices": [], + "foreignKeys": [ + { + "table": "regions", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "regionId" + ], + "referencedColumns": [ + "regionId" + ] + } + ] + }, + { + "tableName": "alerts", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, PRIMARY KEY(`id`))", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [] + } + ], + "views": [], + "setupQueries": [ + "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)", + "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '4db170986b81dc2388f0e2a0f3aee2f0')" + ] + } +} \ No newline at end of file diff --git a/onebusaway-android/src/main/java/org/onebusaway/android/app/Application.java b/onebusaway-android/src/main/java/org/onebusaway/android/app/Application.java index a8b2363d0..698fbfdb2 100644 --- a/onebusaway-android/src/main/java/org/onebusaway/android/app/Application.java +++ b/onebusaway-android/src/main/java/org/onebusaway/android/app/Application.java @@ -55,6 +55,7 @@ import org.onebusaway.android.util.LocationUtils; import org.onebusaway.android.util.PreferenceUtils; import org.onebusaway.android.util.ReminderUtils; +import org.onebusaway.android.widealerts.GtfsAlerts; import java.security.MessageDigest; import java.util.Iterator; @@ -82,6 +83,8 @@ public class Application extends MultiDexApplication { private DonationsManager mDonationsManager; + private GtfsAlerts mGtfsAlerts; + private static Application mApp; /** @@ -121,6 +124,8 @@ public void onCreate() { initOneSignal(); mDonationsManager = new DonationsManager(mPrefs, mFirebaseAnalytics, getResources(), getAppLaunchCount()); + + mGtfsAlerts = new GtfsAlerts(getApplicationContext()); } /** @@ -147,6 +152,10 @@ public static SharedPreferences getPrefs() { public static DonationsManager getDonationsManager() { return get().mDonationsManager; } + public static GtfsAlerts getGtfsAlerts() { + return get().mGtfsAlerts; + } + private static String appLaunchCountPreferencesKey = "appLaunchCountPreferencesKey"; private void incrementAppLaunchCount() { diff --git a/onebusaway-android/src/main/java/org/onebusaway/android/database/AppDatabase.kt b/onebusaway-android/src/main/java/org/onebusaway/android/database/AppDatabase.kt index a51972689..ab3ab81a3 100644 --- a/onebusaway-android/src/main/java/org/onebusaway/android/database/AppDatabase.kt +++ b/onebusaway-android/src/main/java/org/onebusaway/android/database/AppDatabase.kt @@ -6,6 +6,8 @@ import org.onebusaway.android.database.recentStops.dao.RegionDao import org.onebusaway.android.database.recentStops.dao.StopDao import org.onebusaway.android.database.recentStops.entity.RegionEntity import org.onebusaway.android.database.recentStops.entity.StopEntity +import org.onebusaway.android.database.widealerts.dao.AlertDao +import org.onebusaway.android.database.widealerts.entity.AlertEntity import org.onebusaway.android.ui.survey.dao.StudiesDao import org.onebusaway.android.ui.survey.dao.SurveysDao import org.onebusaway.android.ui.survey.entity.Study @@ -16,12 +18,22 @@ import org.onebusaway.android.ui.survey.entity.Survey * Provides abstract methods for accessing `StudiesDao` and `SurveysDao`. * The `@Database` annotation sets up Room with version 1 of the schema. */ -@Database(entities = [Study::class, Survey::class,RegionEntity::class, StopEntity::class], version = 1) + +// Beginning from version 2 we should support auto migration +@Database( + entities = [Study::class, Survey::class, RegionEntity::class, StopEntity::class, AlertEntity::class], + version = 2, + exportSchema = true, +) abstract class AppDatabase : RoomDatabase() { // Studies abstract fun studiesDao(): StudiesDao abstract fun surveysDao(): SurveysDao + // Recent stops for region abstract fun regionDao(): RegionDao abstract fun stopDao(): StopDao + + // Region wide alerts + abstract fun alertsDao(): AlertDao } diff --git a/onebusaway-android/src/main/java/org/onebusaway/android/database/DatabaseProvider.kt b/onebusaway-android/src/main/java/org/onebusaway/android/database/DatabaseProvider.kt index 8ff8b0a00..2300c3cfb 100644 --- a/onebusaway-android/src/main/java/org/onebusaway/android/database/DatabaseProvider.kt +++ b/onebusaway-android/src/main/java/org/onebusaway/android/database/DatabaseProvider.kt @@ -22,7 +22,7 @@ object DatabaseProvider { context.applicationContext, AppDatabase::class.java, "app_database" - ).build() + ).addMigrations(MIGRATION_1_2).build() INSTANCE = instance instance } diff --git a/onebusaway-android/src/main/java/org/onebusaway/android/database/Migrations.kt b/onebusaway-android/src/main/java/org/onebusaway/android/database/Migrations.kt new file mode 100644 index 000000000..db4157bd6 --- /dev/null +++ b/onebusaway-android/src/main/java/org/onebusaway/android/database/Migrations.kt @@ -0,0 +1,10 @@ +package org.onebusaway.android.database + +import androidx.room.migration.Migration +import androidx.sqlite.db.SupportSQLiteDatabase + +val MIGRATION_1_2 = object : Migration(1, 2) { + override fun migrate(db: SupportSQLiteDatabase) { + db.execSQL("CREATE TABLE IF NOT EXISTS `alerts` (`id` TEXT NOT NULL, PRIMARY KEY(`id`))") + } +} \ No newline at end of file diff --git a/onebusaway-android/src/main/java/org/onebusaway/android/database/widealerts/AlertsRepository.kt b/onebusaway-android/src/main/java/org/onebusaway/android/database/widealerts/AlertsRepository.kt new file mode 100644 index 000000000..53025e270 --- /dev/null +++ b/onebusaway-android/src/main/java/org/onebusaway/android/database/widealerts/AlertsRepository.kt @@ -0,0 +1,49 @@ +package org.onebusaway.android.database.widealerts + +import android.content.Context +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch +import kotlinx.coroutines.runBlocking +import kotlinx.coroutines.withContext +import org.onebusaway.android.database.DatabaseProvider +import org.onebusaway.android.database.widealerts.entity.AlertEntity + +/** Provides methods to interact with the alerts database. */ +object AlertsRepository { + + /** + * Checks if an alert exists in the database. + * + * @param context The context to access the database. + * @param alertId The ID of the alert to check. + * @return True if the alert exists, false otherwise. + */ + @JvmStatic + fun isAlertExists(context: Context, alertId: String): Boolean { + val db = DatabaseProvider.getDatabase(context) + val alertDao = db.alertsDao() + + return runBlocking { + withContext(Dispatchers.IO) { + alertDao.getAlertById(alertId) != null + } + } + } + + /** + * Inserts a new alert into the database. + * + * @param context The context to access the database. + * @param alert The `AlertEntity` object to insert. + */ + @JvmStatic + fun insertAlert(context: Context, alert: AlertEntity) { + val db = DatabaseProvider.getDatabase(context) + val alertDao = db.alertsDao() + + CoroutineScope(Dispatchers.IO).launch { + alertDao.insertAlert(alert) + } + } +} \ No newline at end of file diff --git a/onebusaway-android/src/main/java/org/onebusaway/android/database/widealerts/dao/AlertDao.kt b/onebusaway-android/src/main/java/org/onebusaway/android/database/widealerts/dao/AlertDao.kt new file mode 100644 index 000000000..d6dd95c31 --- /dev/null +++ b/onebusaway-android/src/main/java/org/onebusaway/android/database/widealerts/dao/AlertDao.kt @@ -0,0 +1,21 @@ +package org.onebusaway.android.database.widealerts.dao + +import androidx.room.Dao +import androidx.room.Insert +import androidx.room.OnConflictStrategy +import androidx.room.Query +import org.onebusaway.android.database.widealerts.entity.AlertEntity + +/** Data Access Object (DAO) for the `AlertEntity` class. */ + +@Dao +interface AlertDao { + @Insert(onConflict = OnConflictStrategy.REPLACE) + suspend fun insertAlert(alert: AlertEntity) + + @Query("SELECT * FROM alerts WHERE id = :alertId") + suspend fun getAlertById(alertId: String): AlertEntity? + + @Query("SELECT * FROM alerts") + suspend fun getAllAlerts(): List +} \ No newline at end of file diff --git a/onebusaway-android/src/main/java/org/onebusaway/android/database/widealerts/entity/AlertEntity.kt b/onebusaway-android/src/main/java/org/onebusaway/android/database/widealerts/entity/AlertEntity.kt new file mode 100644 index 000000000..d4cfec4ab --- /dev/null +++ b/onebusaway-android/src/main/java/org/onebusaway/android/database/widealerts/entity/AlertEntity.kt @@ -0,0 +1,10 @@ +package org.onebusaway.android.database.widealerts.entity + +import androidx.room.Entity +import androidx.room.PrimaryKey + +/** Represents an alert entity in the database. */ +@Entity(tableName = "alerts") +data class AlertEntity( + @PrimaryKey val id: String +) \ No newline at end of file diff --git a/onebusaway-android/src/main/java/org/onebusaway/android/ui/HomeActivity.java b/onebusaway-android/src/main/java/org/onebusaway/android/ui/HomeActivity.java index 79d1184ee..d683ae9f6 100644 --- a/onebusaway-android/src/main/java/org/onebusaway/android/ui/HomeActivity.java +++ b/onebusaway-android/src/main/java/org/onebusaway/android/ui/HomeActivity.java @@ -27,6 +27,7 @@ import org.onebusaway.android.BuildConfig; import org.onebusaway.android.R; +import org.onebusaway.android.widealerts.GtfsAlertCallBack; import org.onebusaway.android.app.Application; import org.onebusaway.android.donations.DonationsManager; import org.onebusaway.android.io.ObaAnalytics; @@ -60,6 +61,7 @@ import org.onebusaway.android.util.RegionUtils; import org.onebusaway.android.util.ShowcaseViewUtils; import org.onebusaway.android.util.UIUtils; +import org.onebusaway.android.widealerts.GtfsAlertsHelper; import org.opentripplanner.routing.bike_rental.BikeRentalStation; import android.Manifest; @@ -79,6 +81,7 @@ import android.os.Build; import android.os.Bundle; import android.os.Handler; +import android.os.Looper; import android.text.TextUtils; import android.util.Log; import android.view.LayoutInflater; @@ -2008,6 +2011,7 @@ private void showIgnoreBatteryOptimizationDialog() { public void onValidRegion(boolean isValid) { if(isValid){ makeWeatherRequest(); + getGtfsAlerts(); }else{ WeatherUtils.toggleWeatherViewVisibility(false,weatherView); weatherResponse = null; @@ -2185,4 +2189,17 @@ public void onCancelSurvey() { }); surveyManager.requestSurveyData(); } + + private void getGtfsAlerts() { + String regionId = String.valueOf(Application.get().getCurrentRegion().getId()); + Application.getGtfsAlerts().fetchAlerts(regionId, new GtfsAlertCallBack() { + @Override + public void onAlert(String title, String message, String url) { + new Handler(Looper.getMainLooper()).post(() -> { + GtfsAlertsHelper.showWideAlertDialog(HomeActivity.this, title, message, url); + }); + } + }); + } + } diff --git a/onebusaway-android/src/main/java/org/onebusaway/android/widealerts/GtfsAlertCallBack.java b/onebusaway-android/src/main/java/org/onebusaway/android/widealerts/GtfsAlertCallBack.java new file mode 100644 index 000000000..0fb36a4fb --- /dev/null +++ b/onebusaway-android/src/main/java/org/onebusaway/android/widealerts/GtfsAlertCallBack.java @@ -0,0 +1,6 @@ +package org.onebusaway.android.widealerts; + +/** Callback interface for GTFS alerts. */ +public interface GtfsAlertCallBack { + void onAlert(String title, String message, String url); +} diff --git a/onebusaway-android/src/main/java/org/onebusaway/android/widealerts/GtfsAlerts.java b/onebusaway-android/src/main/java/org/onebusaway/android/widealerts/GtfsAlerts.java new file mode 100644 index 000000000..0d717329d --- /dev/null +++ b/onebusaway-android/src/main/java/org/onebusaway/android/widealerts/GtfsAlerts.java @@ -0,0 +1,96 @@ +package org.onebusaway.android.widealerts; + +import com.google.transit.realtime.GtfsRealtime; + +import org.onebusaway.android.R; +import org.onebusaway.android.app.Application; + +import android.content.Context; +import android.content.SharedPreferences; +import android.util.Log; + +import java.net.URL; +import java.util.HashSet; +import java.util.List; +import java.util.Set; + +/** + * Fetches GTFS alerts and processes them. + */ +public class GtfsAlerts { + + private static final String TAG = "GtfsAlerts"; + private static final Set fetchedRegions = new HashSet<>(); + private final Context mContext; + + public GtfsAlerts(Context context) { + mContext = context; + } + + /** + * Fetches GTFS alerts from a specified URL and processes them. + * + * @param regionId The current region ID. + * @param callback The callback to handle the alert data. + */ + public void fetchAlerts(String regionId, GtfsAlertCallBack callback) { + if (fetchedRegions.contains(regionId)) { + Log.d(TAG, "Alerts already fetched for region: " + regionId); + return; + } + Log.d(TAG, "fetchAlerts for region: " + regionId); + new Thread(() -> { + try { + String pathUrl = getGtfsAlertsUrl(regionId); + URL url = new URL(pathUrl); + GtfsRealtime.FeedMessage feed = GtfsRealtime.FeedMessage.parseFrom(url.openStream()); + processAlerts(feed.getEntityList(), callback); + fetchedRegions.add(regionId); + } catch (Exception e) { + Log.e(TAG, "Error fetching GTFS alert data for region: " + regionId, e); + e.printStackTrace(); + } + }).start(); + } + + /** + * Processes the list of GTFS alerts and triggers the callback for one valid alert. + * + * @param alerts The list of GTFS alert entities. + * @param callback The callback to handle each alert. + */ + public void processAlerts(List alerts, GtfsAlertCallBack callback) { + for (GtfsRealtime.FeedEntity entity : alerts) { + if (!GtfsAlertsHelper.isValidEntity(mContext, entity)) { + continue; + } + GtfsRealtime.Alert alert = entity.getAlert(); + GtfsAlertsHelper.markAlertAsRead(Application.get().getApplicationContext() ,entity); + + String id = entity.getId(); + String title = GtfsAlertsHelper.getAlertTitle(alert); + String description = GtfsAlertsHelper.getAlertDescription(alert); + String url = GtfsAlertsHelper.getAlertUrl(alert); + + Log.d(TAG, "Alert: " + id + " - " + title + " - " + description + " - " + url); + callback.onAlert(title, description, url); + } + } + + /** + * Constructs the URL for fetching GTFS alerts for a given region. + * + * @param regionId The ID of the region for which to fetch alerts. + * @return The URL to fetch GTFS alerts. + */ + public String getGtfsAlertsUrl(String regionId) { + Application app = Application.get(); + SharedPreferences sharedPreferences = Application.getPrefs(); + + boolean isTestAlert = sharedPreferences.getBoolean(app.getString(R.string.preferences_display_test_alerts), false); + String url = "https://onebusaway.co/api/v1/regions/" + regionId + "/alerts.pb"; + if (isTestAlert) url += "?test=1"; + return url; + } + +} diff --git a/onebusaway-android/src/main/java/org/onebusaway/android/widealerts/GtfsAlertsHelper.java b/onebusaway-android/src/main/java/org/onebusaway/android/widealerts/GtfsAlertsHelper.java new file mode 100644 index 000000000..ee73046df --- /dev/null +++ b/onebusaway-android/src/main/java/org/onebusaway/android/widealerts/GtfsAlertsHelper.java @@ -0,0 +1,177 @@ +package org.onebusaway.android.widealerts; + +import com.google.transit.realtime.GtfsRealtime; + +import org.onebusaway.android.R; +import org.onebusaway.android.database.widealerts.AlertsRepository; +import org.onebusaway.android.database.widealerts.entity.AlertEntity; + +import android.content.Context; +import android.content.Intent; +import android.net.Uri; + +import java.util.Locale; + +import androidx.appcompat.app.AlertDialog; + +/** + * Helper class for GTFS alerts. + */ +public class GtfsAlertsHelper { + private static final String DEFAULT_LANGUAGE_CODE = "en"; + + /** + * Retrieves the title of the alert in the current app language or default language. + * + * @param alert The GTFS alert. + * @return The alert title. + */ + public static String getAlertTitle(GtfsRealtime.Alert alert) { + String currentLanguageCode = getCurrentAppLanguageCode(); + String title = ""; + + for (GtfsRealtime.TranslatedString.Translation translation : alert.getHeaderText().getTranslationList()) { + if (translation.hasLanguage()) { + if (translation.getLanguage().equals(currentLanguageCode)) { + return translation.getText(); + } else if (translation.getLanguage().equals(DEFAULT_LANGUAGE_CODE)) { + title = translation.getText(); + } + } + } + return title; + } + + /** + * Retrieves the description of the alert in the current app language or default language. + * + * @param alert The GTFS alert. + * @return The alert description. + */ + public static String getAlertDescription(GtfsRealtime.Alert alert) { + String currentLanguageCode = getCurrentAppLanguageCode(); + String description = ""; + + for (GtfsRealtime.TranslatedString.Translation translation : alert.getDescriptionText().getTranslationList()) { + if (translation.hasLanguage()) { + if (translation.getLanguage().equals(currentLanguageCode)) { + return translation.getText(); + } else if (translation.getLanguage().equals(DEFAULT_LANGUAGE_CODE)) { + description = translation.getText(); + } + } + } + return description; + } + + /** + * Retrieves the URL of the alert in the current app language or default language. + * + * @param alert The GTFS alert. + * @return The alert URL. + */ + public static String getAlertUrl(GtfsRealtime.Alert alert) { + String currentLanguageCode = getCurrentAppLanguageCode(); + String url = ""; + + for (GtfsRealtime.TranslatedString.Translation translation : alert.getUrl().getTranslationList()) { + if (translation.hasLanguage()) { + if (translation.getLanguage().equals(currentLanguageCode)) { + return translation.getText(); + } else if (translation.getLanguage().equals(DEFAULT_LANGUAGE_CODE)) { + url = translation.getText(); + } + } + } + return url; + } + + + /** + * Checks if the entity is valid based on agency-wide, severity, and start date criteria. + * + * @param entity The GTFS entity. + * @return True if the alert is valid, false otherwise. + */ + public static boolean isValidEntity(Context context, GtfsRealtime.FeedEntity entity) { + return isAgencyWideAlert(entity.getAlert()) && isHighSeverity(entity.getAlert()) && isStartDateWithin24Hours(entity.getAlert()) && !isAlertRead(context, entity); + } + + /** + * Checks if the alert is agency-wide. + * + * @param alert The GTFS alert. + * @return True if the alert is agency-wide, false otherwise. + */ + public static boolean isAgencyWideAlert(GtfsRealtime.Alert alert) { + for (GtfsRealtime.EntitySelector es : alert.getInformedEntityList()) { + if (es.hasAgencyId()) { + return true; + } + } + return false; + } + + /** + * Checks if the alert has high severity. + * + * @param alert The GTFS alert. + * @return True if the alert has high severity, false otherwise. + */ + public static boolean isHighSeverity(GtfsRealtime.Alert alert) { + return alert.hasSeverityLevel() && (alert.getSeverityLevel() == GtfsRealtime.Alert.SeverityLevel.SEVERE || alert.getSeverityLevel() == GtfsRealtime.Alert.SeverityLevel.WARNING); + } + + /** + * Checks if the alert start date is within the last 24 hours. + * + * @param alert The GTFS alert. + * @return True if the start date is within the last 24 hours, false otherwise. + */ + public static boolean isStartDateWithin24Hours(GtfsRealtime.Alert alert) { + long currentTime = System.currentTimeMillis(); + long startTime = alert.getActivePeriod(0).getStart() * 1000L; + return (currentTime - startTime) <= 24 * 60 * 60 * 1000L; + } + + /** + * Checks if the alert has already been read. + * + * @param context The context to access the database. + * @param entity The GTFS alert entity to check. + * @return True if the alert exists in the database, false otherwise. + */ + + public static boolean isAlertRead(Context context, GtfsRealtime.FeedEntity entity) { + return AlertsRepository.isAlertExists(context, entity.getId()); + } + + /** + * Marks the alert as read by inserting it into the database. + * + * @param context The context to access the database. + * @param entity The `GtfsRealtime.FeedEntity` object representing the alert. + */ + public static void markAlertAsRead(Context context, GtfsRealtime.FeedEntity entity) { + AlertsRepository.insertAlert(context, new AlertEntity(entity.getId())); + } + + public static String getCurrentAppLanguageCode() { + return Locale.getDefault().getLanguage(); + } + + public static void showWideAlertDialog(Context context, String title, String message, String url) { + AlertDialog.Builder builder = new AlertDialog.Builder(context); + builder.setTitle(title) + .setMessage(message) + .setIcon(R.drawable.baseline_warning_24) + .setCancelable(false) + .setPositiveButton(context.getString(R.string.more_info), (dialog, which) -> { + Intent browserIntent = new Intent(Intent.ACTION_VIEW, Uri.parse(url)); + context.startActivity(browserIntent); + }) + .setNegativeButton(R.string.dismiss, (dialog, which) -> dialog.dismiss()) + .create() + .show(); + } +} diff --git a/onebusaway-android/src/main/res/drawable/baseline_warning_24.xml b/onebusaway-android/src/main/res/drawable/baseline_warning_24.xml new file mode 100644 index 000000000..38a45b364 --- /dev/null +++ b/onebusaway-android/src/main/res/drawable/baseline_warning_24.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/onebusaway-android/src/main/res/values-es/strings.xml b/onebusaway-android/src/main/res/values-es/strings.xml index 115ec0520..4c1e1b0de 100644 --- a/onebusaway-android/src/main/res/values-es/strings.xml +++ b/onebusaway-android/src/main/res/values-es/strings.xml @@ -1125,4 +1125,8 @@ Parada eliminada de favoritos Ruta añadida a favoritos Ruta eliminada de favoritos + Descartar + Más información + Mostrar alertas de prueba + Mostrar alertas de prueba amplias para regiones diff --git a/onebusaway-android/src/main/res/values-fi/strings.xml b/onebusaway-android/src/main/res/values-fi/strings.xml index 345bc6436..ef6ca33ed 100644 --- a/onebusaway-android/src/main/res/values-fi/strings.xml +++ b/onebusaway-android/src/main/res/values-fi/strings.xml @@ -722,4 +722,8 @@ Pysäkki poistettu suosikeista Reitti lisätty suosikkeihin Reitti poistettu suosikeista + Hylkää + Lisätietoja + Näytä testihälytykset + Näytä laajat testihälytykset alueille diff --git a/onebusaway-android/src/main/res/values-it/strings.xml b/onebusaway-android/src/main/res/values-it/strings.xml index a3591ed9c..a47204def 100644 --- a/onebusaway-android/src/main/res/values-it/strings.xml +++ b/onebusaway-android/src/main/res/values-it/strings.xml @@ -1036,4 +1036,8 @@ Fermata rimossa dai preferiti Percorso aggiunto ai preferiti Percorso rimosso dai preferiti + Ignora + Maggiori informazioni + Mostra avvisi di prova + Mostra avvisi di prova per regioni \ No newline at end of file diff --git a/onebusaway-android/src/main/res/values-pl/strings.xml b/onebusaway-android/src/main/res/values-pl/strings.xml index c6c535694..6b74be819 100644 --- a/onebusaway-android/src/main/res/values-pl/strings.xml +++ b/onebusaway-android/src/main/res/values-pl/strings.xml @@ -742,4 +742,8 @@ Przystanek usunięty z ulubionych Trasa dodana do ulubionych Trasa usunięta z ulubionych + Odrzuć + Więcej informacji + Wyświetl alerty testowe + Wyświetl szerokie alerty testowe dla regionów \ No newline at end of file diff --git a/onebusaway-android/src/main/res/values/donottranslate.xml b/onebusaway-android/src/main/res/values/donottranslate.xml index 875ee7a90..1b28cb9b2 100644 --- a/onebusaway-android/src/main/res/values/donottranslate.xml +++ b/onebusaway-android/src/main/res/values/donottranslate.xml @@ -72,7 +72,7 @@ preference_travel_behavior preferences_key_user_denied_location_permissions preference_key_push_firebase_data - + preferences_display_test_alerts https://regions.onebusaway.org/regions-v3.json diff --git a/onebusaway-android/src/main/res/values/strings.xml b/onebusaway-android/src/main/res/values/strings.xml index 70ae7c6b3..dfc8cdfbc 100644 --- a/onebusaway-android/src/main/res/values/strings.xml +++ b/onebusaway-android/src/main/res/values/strings.xml @@ -1274,4 +1274,8 @@ Stop removed from favorites Route added to favorites Route removed from favorites + Dismiss + More Info + Display test alerts + Display test-wide alerts for regions diff --git a/onebusaway-android/src/main/res/xml/preferences.xml b/onebusaway-android/src/main/res/xml/preferences.xml index 8debfd76d..9791c567e 100644 --- a/onebusaway-android/src/main/res/xml/preferences.xml +++ b/onebusaway-android/src/main/res/xml/preferences.xml @@ -146,6 +146,11 @@ android:key="@string/preference_key_experimental_regions" android:summary="@string/preferences_experimental_regions_summary" android:title="@string/preferences_experimental_regions_title" /> +