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: Integrate Recent Stops Data into External Surveys #1234

Merged
merged 3 commits into from
Sep 3, 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
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,10 @@ package org.onebusaway.android.database

import androidx.room.Database
import androidx.room.RoomDatabase
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.ui.survey.dao.StudiesDao
import org.onebusaway.android.ui.survey.dao.SurveysDao
import org.onebusaway.android.ui.survey.entity.Study
Expand All @@ -12,8 +16,12 @@ 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], version = 1)
@Database(entities = [Study::class, Survey::class,RegionEntity::class, StopEntity::class], version = 1)
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
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
package org.onebusaway.android.database

import android.content.Context
import androidx.room.Room

/**
* Provides a singleton instance of the Room database (`AppDatabase`).
* Ensures that only one instance of the database is created and used throughout the application.
*/
object DatabaseProvider {
private var INSTANCE: AppDatabase? = null

/**
* Retrieves the singleton instance of the Room database.
* If the instance does not exist, it creates and initializes it.
*
* @return The singleton `AppDatabase` instance.
*/
fun getDatabase(context: Context): AppDatabase {
return INSTANCE ?: synchronized(this) {
val instance = Room.databaseBuilder(
context.applicationContext,
AppDatabase::class.java,
"app_database"
).build()
INSTANCE = instance
instance
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
package org.onebusaway.android.database.recentStops

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.app.Application
import org.onebusaway.android.database.DatabaseProvider
import org.onebusaway.android.database.recentStops.entity.RegionEntity
import org.onebusaway.android.database.recentStops.entity.StopEntity
import org.onebusaway.android.io.elements.ObaStop

/**
* Manages recent stops data by interacting with the database.
* Handles saving new stops, and retrieving recent stops.
*/
object RecentStopsManager {

// Maximum stops count to save
private var MAX_STOP_COUNT = 5
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍


/**
* Inserts a region into the database if it does not already exist.
*
* @param regionId The ID of the region to insert.
*/
private suspend fun insertRegion(context: Context, regionId: Int) {
val db = DatabaseProvider.getDatabase(context)
val regionDao = db.regionDao()

withContext(Dispatchers.IO) {
val existingRegion = regionDao.getRegionByID(regionId)
if (existingRegion == null) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

what happens when properties of a region change? (e.g. the server URL changes or the support email address changes?)

Copy link
Member Author

@amrhossamdev amrhossamdev Sep 2, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't save any of them I only save the region ID, currently, this table is customized for the survey feature only and can be extended in the future.

regionDao.insertRegion(RegionEntity(regionId))
}
}
}

/**
* Saves a new stop to the database. If the maximum number of stops is exceeded, deletes the oldest stop.
*
* @param stop The `ObaStop` object to save.
*/
@JvmStatic
fun saveStop(context: Context, stop: ObaStop) {
val db = DatabaseProvider.getDatabase(context)
val stopDao = db.stopDao()

CoroutineScope(Dispatchers.IO).launch {
val regionId = Application.get().currentRegion.id.toInt()
val currentTime = System.currentTimeMillis()
val stopCount = stopDao.getStopCount(regionId)

if (stopCount >= MAX_STOP_COUNT) {
// Delete the oldest stop and insert the new one
stopDao.deleteOldestStop(regionId)
}

insertRegion(context, regionId)
stopDao.insertStop(StopEntity(stop.id, stop.name, regionId, currentTime))
}
}

/**
* Retrieves a list of recent stop IDs for the current region.
*
* @return A list of recent stop IDs or an empty list if none are found.
*/
fun getRecentStops(context: Context): List<String> {
return runBlocking {
val db = DatabaseProvider.getDatabase(context)
val stopDao = db.stopDao()

withContext(Dispatchers.IO) {
val regionId = Application.get().currentRegion.id.toInt()
val stops = stopDao.getRecentStopsForRegion(regionId)
stops.map { it.stop_id }
}
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
package org.onebusaway.android.database.recentStops.dao

import androidx.room.Dao
import androidx.room.Insert
import androidx.room.OnConflictStrategy
import androidx.room.Query
import org.onebusaway.android.database.recentStops.entity.RegionEntity

/**
* DAO interface for accessing and modifying the `regions` table in the database.
*/

@Dao
interface RegionDao {
@Insert(onConflict = OnConflictStrategy.REPLACE)
suspend fun insertRegion(region: RegionEntity): Long

@Query("SELECT * FROM regions WHERE :regionId = regionId")
suspend fun getRegionByID(regionId:Int):Int?
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
package org.onebusaway.android.database.recentStops.dao

import androidx.room.Dao
import androidx.room.Insert
import androidx.room.OnConflictStrategy
import androidx.room.Query
import org.onebusaway.android.database.recentStops.entity.StopEntity
/**
* DAO interface for accessing and managing the `stops` table in the database.
*/
@Dao
interface StopDao {
@Insert(onConflict = OnConflictStrategy.REPLACE)
suspend fun insertStop(stop: StopEntity): Long

@Query("SELECT * FROM stops WHERE regionId = :regionId")
suspend fun getRecentStopsForRegion(regionId: Int): List<StopEntity>

@Query("SELECT COUNT(*) FROM stops WHERE regionId = :regionId")
suspend fun getStopCount(regionId: Int): Int

@Query("DELETE FROM stops WHERE stop_id = (SELECT stop_id FROM stops WHERE regionId = :regionId ORDER BY timestamp ASC LIMIT 1)")
suspend fun deleteOldestStop(regionId: Int)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
package org.onebusaway.android.database.recentStops.entity

import androidx.room.Entity
import androidx.room.PrimaryKey

/**
* Entity class representing a region in the `regions` table.
* Contains a primary key `regionId` for identifying regions.
*/

@Entity(tableName = "regions")
data class RegionEntity(
@PrimaryKey val regionId:Int
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
package org.onebusaway.android.database.recentStops.entity

import androidx.room.Entity
import androidx.room.ForeignKey
import androidx.room.PrimaryKey

/**
* Entity class representing a stop in the `stops` table.
* Includes a primary key `stop_id`, foreign key `regionId`, stop name, and timestamp.
*/

@Entity(
tableName = "stops",
foreignKeys = [
ForeignKey(
entity = RegionEntity::class,
parentColumns = ["regionId"],
childColumns = ["regionId"],
onDelete = ForeignKey.CASCADE
)
]
)
data class StopEntity(
@PrimaryKey val stop_id: String,
val name: String,
val regionId: Int,
val timestamp: Long
)
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@

import org.onebusaway.android.R;
import org.onebusaway.android.app.Application;
import org.onebusaway.android.database.recentStops.RecentStopsManager;
import org.onebusaway.android.io.ObaAnalytics;
import org.onebusaway.android.io.ObaApi;
import org.onebusaway.android.io.elements.ObaArrivalInfo;
Expand Down Expand Up @@ -231,6 +232,7 @@ public IntentBuilder(Context context, String stopId) {
public IntentBuilder(Context context, ObaStop stop, HashMap<String, ObaRoute> routes) {
mIntent = new Intent(context, ArrivalsListFragment.class);
mIntent.setData(Uri.withAppendedPath(ObaContract.Stops.CONTENT_URI, stop.getId()));
RecentStopsManager.saveStop(context,stop);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

<3

setStopName(stop.getName());
setStopCode(stop.getStopCode());
setStopDirection(stop.getDirection());
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
package org.onebusaway.android.ui.survey.activities

import android.annotation.SuppressLint
import android.content.pm.PackageManager
import android.os.Build
import android.os.Bundle
import android.util.Log
import android.view.MenuItem
Expand All @@ -15,6 +17,7 @@ import com.google.android.gms.common.GoogleApiAvailability
import com.google.android.gms.common.api.GoogleApiClient
import org.onebusaway.android.R
import org.onebusaway.android.app.Application
import org.onebusaway.android.database.recentStops.RecentStopsManager
import org.onebusaway.android.ui.survey.SurveyPreferences
import org.onebusaway.android.ui.survey.utils.SurveyUtils
import org.onebusaway.android.util.LocationUtils
Expand Down Expand Up @@ -117,6 +120,9 @@ class SurveyWebViewActivity : AppCompatActivity() {
newUrl += getEmbeddedDataValue(embeddedValueList[index])
if (index + 1 != size) newUrl += "&"
}
newUrl+="&os=android"
newUrl+=getAppVersionCode()
newUrl+=getAndroidVersionCode()
return newUrl
}

Expand All @@ -140,6 +146,7 @@ class SurveyWebViewActivity : AppCompatActivity() {
SurveyUtils.ROUTE_ID -> getRouteID()
SurveyUtils.STOP_ID -> getStopID()
SurveyUtils.CURRENT_LOCATION -> getCurrentLocation()
SurveyUtils.RECENT_STOP_IDS -> getRecentStops()
else -> ""
}
}
Expand Down Expand Up @@ -182,4 +189,53 @@ class SurveyWebViewActivity : AppCompatActivity() {
}


/**
* Retrieves a comma-separated string of recent stop IDs.
*
* If the list is not empty, it concatenates the stop IDs into a single string with each ID separated by a comma.
* If the list is empty, it returns "NA" as a placeholder.
*
* @return A comma-separated string of recent stop IDs if available, otherwise "NA".
*/

private fun getRecentStops(): String {
val recentStops = RecentStopsManager.getRecentStops(this)
return recentStops.takeIf { it.isNotEmpty() }
?.joinToString(separator = ",")
?: "NA"
}

// Return app version code
private fun getAppVersionCode(): String {
val packageInfo = try {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
packageManager.getPackageInfo(packageName, PackageManager.PackageInfoFlags.of(0))
} else {
packageManager.getPackageInfo(packageName, 0)
}
} catch (e: PackageManager.NameNotFoundException) {
e.printStackTrace()
return "&app_version=NA"
}

val versionCode = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
packageInfo.longVersionCode
} else {
@Suppress("DEPRECATION")
packageInfo.versionCode.toLong()
}

return "&app_version=$versionCode"
}


// Return the version as a string, e.g., "9.1"
private fun getAndroidVersionCode():String{
val androidVersion = Build.VERSION.RELEASE
val versionInfo = "&os_version=$androidVersion"
return versionInfo
}



}
Loading