Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feat: Support region-wide alerts #1274

Merged
merged 6 commits into from
Oct 28, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 15 additions & 2 deletions onebusaway-android/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -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()]
}
}
}

/**
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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'
Expand Down
Original file line number Diff line number Diff line change
@@ -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')"
]
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -82,6 +83,8 @@ public class Application extends MultiDexApplication {

private DonationsManager mDonationsManager;

private GtfsAlerts mGtfsAlerts;

private static Application mApp;

/**
Expand Down Expand Up @@ -121,6 +124,8 @@ public void onCreate() {
initOneSignal();

mDonationsManager = new DonationsManager(mPrefs, mFirebaseAnalytics, getResources(), getAppLaunchCount());

mGtfsAlerts = new GtfsAlerts(getApplicationContext());
}

/**
Expand All @@ -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() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
}
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ object DatabaseProvider {
context.applicationContext,
AppDatabase::class.java,
"app_database"
).build()
).addMigrations(MIGRATION_1_2).build()
INSTANCE = instance
instance
}
Expand Down
Original file line number Diff line number Diff line change
@@ -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`))")
}
}
Original file line number Diff line number Diff line change
@@ -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)
}
}
}
Original file line number Diff line number Diff line change
@@ -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<AlertEntity>
}
Loading
Loading