Skip to content
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
13 changes: 2 additions & 11 deletions app/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -45,24 +45,18 @@ android {
defaultConfig {
applicationId = "edu.rpi.shuttletracker"
minSdk = 26
targetSdk = 36
targetSdk = 37
versionCode = 17
versionName = "2.5.0"

testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
vectorDrawables {
useSupportLibrary = true
}
}

buildTypes {
release {
isMinifyEnabled = true
isShrinkResources = true
proguardFiles(
getDefaultProguardFile("proguard-android-optimize.txt"),
"proguard-rules.pro",
)
proguardFiles(getDefaultProguardFile("proguard-android-optimize.txt"))
}

debug {
Expand Down Expand Up @@ -101,15 +95,13 @@ dependencies {
androidTestImplementation(libs.androidx.espresso.core)
androidTestImplementation(platform(libs.androidx.compose.bom))
androidTestImplementation(libs.androidx.junit4)
debugImplementation(libs.androidx.ui.tooling.preview)
debugImplementation(libs.androidx.ui.test.manifest)
debugImplementation(libs.androidx.ui.tooling)

// hilt
implementation(libs.hilt.android)
ksp(libs.hilt.compiler)
implementation(libs.androidx.hilt.lifecycle.viewmodel.compose)
ksp(libs.androidx.hilt.compiler)

// retrofit
implementation(libs.retrofit)
Expand All @@ -119,7 +111,6 @@ dependencies {
implementation(libs.logging.interceptor)

// compose
implementation(libs.androidx.lifecycle.viewmodel.compose)
implementation(libs.androidx.lifecycle.viewmodel.navigation3)
implementation(libs.androidx.lifecycle.runtime.compose)

Expand Down
21 changes: 0 additions & 21 deletions app/proguard-rules.pro

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@ import androidx.compose.ui.test.performClick
import androidx.test.ext.junit.runners.AndroidJUnit4
import edu.rpi.shuttletracker.core.network.NetworkResult
import edu.rpi.shuttletracker.core.ui.theme.ShuttleTrackerTheme
import edu.rpi.shuttletracker.feature.etas.EtasViewModel
import edu.rpi.shuttletracker.feature.schedule.ScheduleViewModel
import edu.rpi.shuttletracker.testing.fakes.FakeShuttleRepository
import edu.rpi.shuttletracker.testing.fakes.FakeUserPreferences
Expand Down Expand Up @@ -82,15 +81,13 @@ class MapsScreenNavigationTest {
val preferences = FakeUserPreferences()
val viewModel = MapsViewModel(repository, preferences)
val scheduleViewModel = ScheduleViewModel(repository)
val etasViewModel = EtasViewModel(repository, preferences)

composeRule.setContent {
ShuttleTrackerTheme(dynamicColor = false) {
MapsScreen(
onOpenSettings = {},
viewModel = viewModel,
scheduleViewModel = scheduleViewModel,
etasViewModel = etasViewModel,
)
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,6 @@ class AboutScreenTest {
ShuttleTrackerTheme(dynamicColor = false) {
AboutScreen(
onBack = {},
onOpenLibraries = {},
viewModel = remember { AboutViewModel(preferences) },
)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,15 +22,13 @@ class FakeShuttleRepository : ShuttleRepository {
val announcements = MutableSharedFlow<NetworkResult<List<Announcement>>>(replay = 1)

var routesResult: NetworkResult<Map<String, Route>> = NetworkResult.Success(emptyMap())
var announcementsResult: NetworkResult<List<Announcement>> = NetworkResult.Success(emptyList())
var scheduleResult: NetworkResult<Schedule>? = null

var observeLocationsCalls = 0
var observeEtasCalls = 0
var observeVelocitiesCalls = 0
var observeAnnouncementsCalls = 0
var routesCalls = 0
var announcementsCalls = 0
var scheduleCalls = 0

override fun observeVehicleLocations(pollMs: Long): Flow<NetworkResult<Map<String, VehicleLocation>>> {
Expand Down Expand Up @@ -58,11 +56,6 @@ class FakeShuttleRepository : ShuttleRepository {
return routesResult
}

override suspend fun getAnnouncements(): NetworkResult<List<Announcement>> {
announcementsCalls++
return announcementsResult
}

override suspend fun getSchedule(): NetworkResult<Schedule> {
scheduleCalls++
return checkNotNull(scheduleResult) { "Set scheduleResult before creating a view model" }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,6 @@ fun testRoute() =
Route(
color = "#D32F2F",
stops = listOf("union", "academy"),
polylineStops = emptyList(),
coordinates = listOf(listOf(listOf(42.730, -73.680), listOf(42.731, -73.679))),
stopDetails =
mapOf(
Expand Down
4 changes: 2 additions & 2 deletions app/src/main/java/edu/rpi/shuttletracker/app/MainActivity.kt
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
package edu.rpi.shuttletracker.app

import android.content.Intent
import android.net.Uri
import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
Expand All @@ -14,6 +13,7 @@ import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
import androidx.core.net.toUri
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import dagger.hilt.android.AndroidEntryPoint
import edu.rpi.shuttletracker.app.navigation.AppNavigation
Expand Down Expand Up @@ -98,7 +98,7 @@ class MainActivity : ComponentActivity() {

when (val destination = resolveNotificationTapDestination(url)) {
is NotificationTapDestination.ExternalUrl ->
startActivity(Intent(Intent.ACTION_VIEW, Uri.parse(destination.url)))
startActivity(Intent(Intent.ACTION_VIEW, destination.url.toUri()))
NotificationTapDestination.Map -> Unit
}
}
Expand Down
26 changes: 9 additions & 17 deletions app/src/main/java/edu/rpi/shuttletracker/app/di/NetworkModule.kt
Original file line number Diff line number Diff line change
Expand Up @@ -64,23 +64,15 @@ object NetworkModule {
val cacheSize = (5 * 1024 * 1024).toLong()
val myCache = Cache(context.cacheDir, cacheSize)

return if (BuildConfig.DEBUG) {
val loggingInterceptor = HttpLoggingInterceptor()

loggingInterceptor.level = HttpLoggingInterceptor.Level.BODY
OkHttpClient
.Builder()
.cache(myCache)
.addInterceptor(cacheInterceptor)
.addInterceptor(loggingInterceptor)
.build()
} else {
OkHttpClient
.Builder()
.cache(myCache)
.addInterceptor(cacheInterceptor)
.build()
}
return OkHttpClient
.Builder()
.cache(myCache)
.addInterceptor(cacheInterceptor)
.apply {
if (BuildConfig.DEBUG) {
addInterceptor(HttpLoggingInterceptor().setLevel(HttpLoggingInterceptor.Level.BODY))
}
}.build()
}

@Provides
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,6 @@ import androidx.navigation3.ui.NavDisplay
import edu.rpi.shuttletracker.feature.map.MapsScreen
import edu.rpi.shuttletracker.feature.settings.SettingsScreen
import edu.rpi.shuttletracker.feature.settings.about.AboutScreen
import edu.rpi.shuttletracker.feature.settings.about.LibrariesScreen
import edu.rpi.shuttletracker.feature.settings.developerMenu.DevMenuScreen
import edu.rpi.shuttletracker.feature.setup.SetupScreen
import kotlinx.serialization.Serializable
Expand All @@ -27,9 +26,6 @@ private data object SettingsRoute : NavKey
@Serializable
private data object AboutRoute : NavKey

@Serializable
private data object LibrariesRoute : NavKey

@Serializable
private data object DeveloperOptionsRoute : NavKey

Expand Down Expand Up @@ -90,13 +86,7 @@ fun AppNavigation(setupCompleted: Boolean) {
)
}
entry<AboutRoute> {
AboutScreen(
onBack = ::navigateBack,
onOpenLibraries = { navigateTo(LibrariesRoute) },
)
}
entry<LibrariesRoute> {
LibrariesScreen(onOpened = ::navigateBack)
AboutScreen(onBack = ::navigateBack)
}
entry<DeveloperOptionsRoute> {
DevMenuScreen(onBack = ::navigateBack)
Expand Down
48 changes: 19 additions & 29 deletions app/src/main/java/edu/rpi/shuttletracker/core/ui/Errors.kt
Original file line number Diff line number Diff line change
Expand Up @@ -17,55 +17,45 @@ import edu.rpi.shuttletracker.core.network.NetworkError
* time), and calls back so the ViewModel can clear or retry. See `MapsScreen`/`ScheduleScreen` for
* how features wire this up.
*
* @param networkError: a network error, null if none
* @param serverError: a server error, null if none
* @param unknownError: an unknown error, null if none
* @param error a network error, null if none
*
* @param ignoreErrorRequest: what happens when error is ignored
* @param retryErrorRequest: what happens when you want to retry what caused the error
* */
@Composable
fun CheckResponseError(
networkError: NetworkError.Connectivity? = null,
serverError: NetworkError.Http? = null,
unknownError: NetworkError.Unknown? = null,
error: NetworkError? = null,
ignoreErrorRequest: () -> Unit = {},
retryErrorRequest: () -> Unit = {},
) {
val networkMessage = stringResource(R.string.error_network)
val serverMessage = stringResource(R.string.error_server)
val unknownMessage = stringResource(R.string.error_unknown)
val activeError =
when {
networkError != null ->
ErrorContent(
networkError,
networkMessage,
when (networkError) {
is NetworkError.NoConnection -> networkError.cause?.message.orEmpty()
is NetworkError.Timeout -> networkError.cause?.message.orEmpty()
},
)
serverError != null -> ErrorContent(serverError, serverMessage, serverError.displayMessage)
unknownError != null -> ErrorContent(unknownError, unknownMessage, unknownError.displayMessage)
else -> null
val errorType =
when (error) {
is NetworkError.Connectivity -> networkMessage
is NetworkError.Http -> serverMessage
is NetworkError.Unknown -> unknownMessage
null -> ""
}
val errorBody =
when (error) {
is NetworkError.NoConnection -> error.cause?.message.orEmpty()
is NetworkError.Timeout -> error.cause?.message.orEmpty()
is NetworkError.Http -> error.displayMessage
is NetworkError.Unknown -> error.displayMessage
null -> ""
}

Error(
error = activeError?.error,
error = error,
onPrimaryRequest = retryErrorRequest,
onDismissRequest = ignoreErrorRequest,
errorType = activeError?.type.orEmpty(),
errorBody = activeError?.body.orEmpty(),
errorType = errorType,
errorBody = errorBody,
)
}

private data class ErrorContent(
val error: Any,
val type: String,
val body: String,
)

/**
* @param error: the error you want to display
* @param onPrimaryRequest: what happens when you want to retry what caused the error
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -114,7 +114,6 @@ fun ShuttleTrackerTheme(

MaterialTheme(
colorScheme = colorScheme,
typography = Typography,
content = content,
)
}

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,13 @@ import java.net.URI
import java.net.URISyntaxException

/**
* Only `http`/`https` URLs with a host are safe to open; anything else (custom schemes, relative
* Only `https` URLs with a host are safe to open; anything else (unencrypted/custom schemes, relative
* paths, malformed URIs) is rejected rather than crashing or launching an unintended target.
* */
fun isSafeHttpUrl(url: String): Boolean =
try {
val uri = URI(url)
uri.scheme?.lowercase() in setOf("http", "https") && !uri.host.isNullOrBlank()
uri.scheme.equals("https", ignoreCase = true) && !uri.host.isNullOrBlank()
} catch (_: URISyntaxException) {
false
} catch (_: IllegalArgumentException) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ fun VehicleLocationDto.toModel(): VehicleLocation {
return VehicleLocation(name, latitude, longitude, speedMph, timestamp, headingDegrees)
}

fun VehicleStopEtaDto.toModel() = VehicleStopEta(stopTimes, timestamp)
fun VehicleStopEtaDto.toModel() = VehicleStopEta(stopTimes)

fun VehicleVelocitiesDto.toModel() = VehicleVelocities(routeName, isAtStop, currentStop)

Expand All @@ -41,7 +41,7 @@ fun StopDto.toModel(): Stop {
return Stop(coordinates, offset, name)
}

fun RouteDto.toModel() = Route(color, stops, polylineStops, coordinates, stopDetails.mapValues { it.value.toModel() })
fun RouteDto.toModel() = Route(color, stops, coordinates, stopDetails.mapValues { it.value.toModel() })

fun AnnouncementDto.toModel(): Announcement =
Announcement(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,6 @@ import com.google.android.gms.maps.model.LatLng
data class Route(
val color: String,
val stops: List<String>,
val polylineStops: List<String>,
val coordinates: List<List<List<Double>>>,
val stopDetails: Map<String, Stop>,
) {
Expand Down
24 changes: 1 addition & 23 deletions app/src/main/java/edu/rpi/shuttletracker/data/models/Schedule.kt
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
package edu.rpi.shuttletracker.data.models

import java.util.Calendar
import java.time.DayOfWeek

/**
* The weekly shuttle schedule. Each day of the week names a schedule *type* ("weekday",
Expand Down Expand Up @@ -41,25 +41,3 @@ data class Schedule(
else -> emptyMap()
}
}

/** Wraps [java.util.Calendar]'s day constants so the rest of the app never has to touch them directly. */
enum class DayOfWeek(
val displayName: String,
val calendarConst: Int,
) {
MONDAY("Mon", Calendar.MONDAY),
TUESDAY("Tue", Calendar.TUESDAY),
WEDNESDAY("Wed", Calendar.WEDNESDAY),
THURSDAY("Thu", Calendar.THURSDAY),
FRIDAY("Fri", Calendar.FRIDAY),
SATURDAY("Sat", Calendar.SATURDAY),
SUNDAY("Sun", Calendar.SUNDAY),
;

companion object {
fun fromToday(): DayOfWeek {
val today = Calendar.getInstance().get(Calendar.DAY_OF_WEEK)
return entries.firstOrNull { it.calendarConst == today } ?: MONDAY
}
}
}
Loading