diff --git a/app/build.gradle b/app/build.gradle index 43a900f..dcd072c 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -9,8 +9,8 @@ android { applicationId "fnsb.macstransit" minSdk 27 targetSdk 36 - versionCode 33 - versionName 'Release 1.4.2' + versionCode 34 + versionName 'Release 1.5' testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" } buildTypes { @@ -62,13 +62,14 @@ dependencies { implementation 'com.android.volley:volley:1.2.1' implementation 'androidx.legacy:legacy-support-v4:1.0.0' implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.10.2' - implementation 'androidx.lifecycle:lifecycle-viewmodel-ktx:2.9.4' + implementation 'androidx.lifecycle:lifecycle-viewmodel-ktx:2.10.0' implementation 'com.google.maps.android:maps-ktx:5.2.1' implementation 'com.google.maps.android:maps-utils-ktx:5.2.1' - implementation 'com.google.maps.android:android-maps-utils:3.19.0' + implementation 'com.google.maps.android:android-maps-utils:3.19.1' implementation 'androidx.annotation:annotation-jvm:1.9.1' implementation 'androidx.constraintlayout:constraintlayout:2.2.1' implementation 'com.orhanobut:dialogplus:1.11@aar' + implementation 'com.google.android.material:material:1.13.0' testImplementation 'junit:junit:4.13.2' testImplementation 'org.json:json:20250517' androidTestImplementation 'androidx.test.ext:junit:1.3.0' diff --git a/app/src/main/java/fnsb/macstransit/activities/mapsactivity/MapsActivity.kt b/app/src/main/java/fnsb/macstransit/activities/mapsactivity/MapsActivity.kt index 2d285cc..afd22ab 100644 --- a/app/src/main/java/fnsb/macstransit/activities/mapsactivity/MapsActivity.kt +++ b/app/src/main/java/fnsb/macstransit/activities/mapsactivity/MapsActivity.kt @@ -5,7 +5,9 @@ import fnsb.macstransit.routematch.Route import fnsb.macstransit.settings.V2 import fnsb.macstransit.R import android.util.Log +import android.view.Gravity import android.view.Menu +import android.view.ViewGroup import androidx.core.view.ViewCompat import androidx.core.view.WindowInsetsCompat import androidx.core.view.updatePadding @@ -15,9 +17,11 @@ import androidx.lifecycle.ViewModelProvider import androidx.lifecycle.lifecycleScope import androidx.lifecycle.repeatOnLifecycle import com.google.android.gms.maps.SupportMapFragment +import com.orhanobut.dialogplus.DialogPlus import fnsb.macstransit.activities.LoadedRoutes import fnsb.macstransit.activities.SettingsActivity import fnsb.macstransit.activities.mapsactivity.mappopups.FarePopupWindow +import fnsb.macstransit.activities.mapsactivity.mappopups.RouteMenu import fnsb.macstransit.databinding.ActivityMapsBinding import fnsb.macstransit.routematch.Bus import fnsb.macstransit.routematch.SharedStop @@ -34,12 +38,14 @@ class MapsActivity: FragmentActivity() { * The view model for the maps activity. * This is usually where all the large functions and additional properties are. */ - private lateinit var viewModel: MapsViewModel + lateinit var viewModel: MapsViewModel /** * Create a variable to store our fare popup window instance. */ - private lateinit var farePopupWindow: FarePopupWindow + lateinit var farePopupWindow: FarePopupWindow + + lateinit var routeMenu: DialogPlus override fun onCreate(savedInstanceState: Bundle?) { Log.v("onCreate", "onCreate has been called!") @@ -52,6 +58,7 @@ class MapsActivity: FragmentActivity() { val binding: ActivityMapsBinding = ActivityMapsBinding.inflate(layoutInflater) binding.viewmodel = viewModel binding.lifecycleOwner = this + binding.activity = this // Set the activity view to the map activity layout. setContentView(binding.root) @@ -149,139 +156,6 @@ class MapsActivity: FragmentActivity() { Log.v("onDestroy", "Finished onDestroy") } - override fun onCreateOptionsMenu(menu: Menu): Boolean { - Log.v("onCreateOptionsMenu", "onCreateOptionsMenu has been called!") - - // Setup the inflater. - menuInflater.inflate(R.menu.menu, menu) - - // Create the menu item that corresponds to the route object. - for (name in LoadedRoutes.routes.keys) { - - // Make sure the item is checkable. - menu.add(R.id.routes, name.hashCode(), Menu.NONE, name).isCheckable = true - } - - // Return what ever the default behaviour would be when calling this method if it were not overridden. - return super.onCreateOptionsMenu(menu) - } - - override fun onPrepareOptionsMenu(menu: Menu): Boolean { - Log.v("onPrepareOptionsMenu", "onPrepareOptionsMenu has been called!") - - // Iterate through all the routes that can be tracked (if allRoutes isn't null). - for ((name, route) in LoadedRoutes.routes) { - - // Determine whether or not the menu item should be checked before hand. - val checked: Boolean = route.enabled - - // Set the menu item to be checked if the route it corresponds to is enabled. - Log.d("onPrepareOptionsMenu", "Setting $name to be enabled: $checked") - menu.findItem(name.hashCode()).isChecked = checked - } - - // Check if night mode should be enabled by default, and set the checkbox to that value. - menu.findItem(R.id.night_mode).isChecked = (CurrentSettings.settingsImplementation as V2).darktheme - - // Return what ever the default behaviour would be when calling this method if it were not overridden. - return super.onPrepareOptionsMenu(menu) - } - - override fun onOptionsItemSelected(item: android.view.MenuItem): Boolean { - Log.v("onOptionsItemSelected", "onOptionsItemSelected has been called!") - - // Identify which method to call based on the item ID. - // Check if the item that was selected belongs to the other group - when (item.groupId) { - R.id.other -> { - - // Identify what action to execute based on the item ID. - when (item.itemId) { - - // Check if the item that was selected was the night mode toggle. - R.id.night_mode -> { - if (viewModel.map == null) { - return false - } - - Log.d("onOptionsItemSelected", "Toggling night mode...") - - // Create a boolean to store the resulting value of the menu item. - val enabled = !item.isChecked - - // Toggle night mode - MapsViewModel.toggleNightMode(viewModel.map!!, this, enabled) - - // Set the menu item's checked value to that of the enabled value. - item.isChecked = enabled - } - - // Check if the item that was selected was the settings button. - R.id.settings -> { - - // Create the intent to launch the settings activity. - val settingsIntent = android.content.Intent(this, SettingsActivity::class.java) - - // Start the settings activity. - startActivity(settingsIntent) - } - - // Check if the item that was selected was the fares button. - R.id.fares -> farePopupWindow.showFarePopupWindow() - - // Since the item's ID was not part of anything accounted for (uh oh), log it as a warning! - else -> Log.w("onOptionsItemSelected", "Unaccounted menu item in the other group was checked!") - } - } - - // Check if the item that was selected belongs to the routes group. - R.id.routes -> { - - // Create a boolean to store the resulting value of the menu item. - val enabled = !item.isChecked - - // Get the route that was selected. - val route: Route = LoadedRoutes.routes[item.title] ?: return super.onOptionsItemSelected(item) - - // Set the route to enabled. - route.enabled = enabled - Log.d("onOptionsItemSelected", "Selected route ${route.name}") - - // If the map is null at this point just return early (skip redrawing). - if (viewModel.map == null) { - return super.onOptionsItemSelected(item) - } - - // Try to (re)draw the buses onto the map. - // Because we are iterating a static variable that is modified on a different thread - // there is a possibility of a concurrent modification. - try { - viewModel.drawBuses() - } catch (e: ConcurrentModificationException) { - Log.e("onOptionsItemSelected", - "Unable to redraw all buses due to concurrent modification", e) - } - - // (Re) draw the stops onto the map. - viewModel.drawStops() - - // (Re) draw the routes onto the map (if enabled). - if ((CurrentSettings.settingsImplementation as V2).polylines) { - viewModel.drawRoutes() - } - - // Set the menu item's checked value to that of the enabled value - item.isChecked = enabled - } - - // Since the item's ID and group was not part of anything accounted for (uh oh), log it as a warning! - else -> Log.w("onOptionsItemSelected", "Unaccounted menu item was checked!") - } - - // Return what ever the default behaviour would be when calling this method if it were not overridden. - return super.onOptionsItemSelected(item) - } - override fun onResume() { Log.v("onResume", "onResume has been called!") super.onResume() @@ -305,6 +179,32 @@ class MapsActivity: FragmentActivity() { } } + fun showRoutesMenu() { + + // Constantly recreating the dialog window is wasteful, + // but it fixes a bug where after the menu was dismissed its height would be crushed back down to minimums + routeMenu = DialogPlus.newDialog(this) + .setAdapter(RouteMenu(this)) + .setContentBackgroundResource(R.color.colorPrimaryDark) + .setExpanded(false) + .setGravity(Gravity.CENTER) + .setContentWidth(ViewGroup.LayoutParams.MATCH_PARENT) + .setContentHeight(ViewGroup.LayoutParams.WRAP_CONTENT) + .setOnDismissListener { dialog -> RouteMenu.onDismissListener(this, dialog) } + .create() + + routeMenu.show() + } + + fun launchSettingsActivity() { + + // Create the intent to launch the settings activity. + val settingsIntent = android.content.Intent(this, SettingsActivity::class.java) + + // Start the settings activity. + startActivity(settingsIntent) + } + companion object { /** diff --git a/app/src/main/java/fnsb/macstransit/activities/mapsactivity/mappopups/RouteMenu.kt b/app/src/main/java/fnsb/macstransit/activities/mapsactivity/mappopups/RouteMenu.kt new file mode 100644 index 0000000..66a0dc2 --- /dev/null +++ b/app/src/main/java/fnsb/macstransit/activities/mapsactivity/mappopups/RouteMenu.kt @@ -0,0 +1,152 @@ +package fnsb.macstransit.activities.mapsactivity.mappopups + +import android.content.Context +import android.content.res.ColorStateList +import android.util.Log +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import android.widget.BaseAdapter +import android.widget.Button +import android.widget.LinearLayout +import androidx.core.graphics.drawable.DrawableCompat +import androidx.core.view.children +import com.orhanobut.dialogplus.DialogPlus +import fnsb.macstransit.R +import fnsb.macstransit.activities.LoadedRoutes +import fnsb.macstransit.activities.mapsactivity.MapsActivity +import fnsb.macstransit.routematch.Route +import fnsb.macstransit.settings.CurrentSettings +import fnsb.macstransit.settings.V2 +import org.json.JSONException +import kotlin.math.PI + +class RouteMenu(private val mapsActivity: MapsActivity): BaseAdapter() { + + private var setup = false + + override fun getCount(): Int { + return 1 + } + + override fun getItem(position: Int): Any { + return position // Todo properly implement me? + } + + override fun getItemId(position: Int): Long { + return position.toLong() // Todo properly implement me? + } + + override fun getView(position: Int, convertView: View?, parent: ViewGroup?): View? { + val layoutInflater: LayoutInflater = LayoutInflater.from(mapsActivity) + val view = convertView ?: layoutInflater.inflate(R.layout.route_menu, parent, + false) + val routesContainer: LinearLayout = view.findViewById(R.id.routes) + val closeButton: Button = view.findViewById(R.id.close) + + if (setup) { + return view + } + Log.d("RouteMenu", "Setting up menu for first time") + + val settings = CurrentSettings.settingsImplementation as V2 + + val routesWithBuses = mutableListOf() + for (bus in mapsActivity.viewModel.buses) { + if (!routesWithBuses.contains(bus.route.name)) { + routesWithBuses.add(bus.route.name) + } + } + + for ((name, route) in LoadedRoutes.routes) { + val selectableRoute = SelectableRoute(mapsActivity) + + // If the route has buses broadcasting then set the icon appropriately + if (routesWithBuses.contains(name)) { + selectableRoute.broadcastingIcon.setImageResource(R.drawable.wifi_icon) + } else { + selectableRoute.broadcastingIcon.setImageResource(R.drawable.not_broadcasting) + } + + selectableRoute.routeName.text = name + selectableRoute.routeName.backgroundTintList = ColorStateList.valueOf(route.color) + + selectableRoute.selected.isChecked = route.enabled + + // Clicking the route name also checks the box + selectableRoute.routeName.setOnClickListener { + selectableRoute.selected.isChecked = !selectableRoute.selected.isChecked + } + + selectableRoute.favoriteRoute.isChecked = settings.favoriteRouteNames.contains(name) + + routesContainer.addView(selectableRoute) + } + + closeButton.setOnClickListener { + mapsActivity.routeMenu.dismiss() + } + + setup = true + + return view + } + + companion object { + + fun onDismissListener(mapsActivity: MapsActivity, dialog: DialogPlus) { + val routesContainer: LinearLayout = dialog.findViewById(R.id.routes) as LinearLayout + + val settings = CurrentSettings.settingsImplementation as V2 + + val favoritedRoutes: MutableList = mutableListOf() + for (selectableRoute in routesContainer.children) { + if (selectableRoute !is SelectableRoute) { + Log.w("onDismissListener", "Entry in routes container isn't selectable route") + return + } + + LoadedRoutes.routes[selectableRoute.routeName.text]?.enabled = selectableRoute.selected.isChecked + + if (selectableRoute.favoriteRoute.isChecked) { + val route = LoadedRoutes.routes[selectableRoute.routeName.text] + favoritedRoutes.add(route!!) + } + } + + val settingsJson = try { + settings.formatSettingsToJsonString( + settings.traffic, + settings.darktheme, settings.polylines, + settings.streetView, settings.maptype, + *favoritedRoutes.toTypedArray() + ) + } catch (e: JSONException) { + Log.e("RouteMenu", "Json error when writing settings", e) + return + } + settings.writeSettingsToFile(settingsJson.toString(), mapsActivity) + + // Reload the settings + settings.parseSettings(settingsJson) + + // Try to (re)draw the buses onto the map. + // Because we are iterating a static variable that is modified on a different thread + // there is a possibility of a concurrent modification. + try { + mapsActivity.viewModel.drawBuses() + } catch (e: ConcurrentModificationException) { + Log.e("onOptionsItemSelected", + "Unable to redraw all buses due to concurrent modification", e) + } + + // (Re) draw the stops onto the map. + mapsActivity.viewModel.drawStops() + + // (Re) draw the routes onto the map (if enabled). + if (settings.polylines) { + mapsActivity.viewModel.drawRoutes() + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/fnsb/macstransit/activities/mapsactivity/mappopups/SelectableRoute.kt b/app/src/main/java/fnsb/macstransit/activities/mapsactivity/mappopups/SelectableRoute.kt new file mode 100644 index 0000000..aa32532 --- /dev/null +++ b/app/src/main/java/fnsb/macstransit/activities/mapsactivity/mappopups/SelectableRoute.kt @@ -0,0 +1,37 @@ +package fnsb.macstransit.activities.mapsactivity.mappopups + +import android.content.Context +import android.util.AttributeSet +import android.widget.CheckBox +import android.widget.ImageView +import android.widget.LinearLayout +import android.widget.TextView +import fnsb.macstransit.R + +class SelectableRoute : LinearLayout { + + val selected: CheckBox + + val broadcastingIcon: ImageView + + val routeName: TextView + + // val routeFrequency: TextView + + val favoriteRoute: CheckBox + + + constructor(context: Context): this(context, null) + constructor(context: Context, attributeSet: AttributeSet?): this(context, attributeSet, 0) + constructor(context: Context, attributeSet: AttributeSet?, defStyleAttr: Int) : super(context, + attributeSet, defStyleAttr) { + inflate(context, R.layout.selectable_route, this) + + selected = findViewById(R.id.selected) + broadcastingIcon = findViewById(R.id.broadcasting_icon) + routeName = findViewById(R.id.route_name) + // routeFrequency = findViewById(R.id.route_frequency) + favoriteRoute = findViewById(R.id.favorite_route) + } + +} \ No newline at end of file diff --git a/app/src/main/java/fnsb/macstransit/activities/mapsactivity/mappopups/StopDialog.kt b/app/src/main/java/fnsb/macstransit/activities/mapsactivity/mappopups/StopDialog.kt index 37abcdb..89a03cc 100644 --- a/app/src/main/java/fnsb/macstransit/activities/mapsactivity/mappopups/StopDialog.kt +++ b/app/src/main/java/fnsb/macstransit/activities/mapsactivity/mappopups/StopDialog.kt @@ -9,7 +9,6 @@ import android.widget.BaseAdapter import android.widget.LinearLayout import android.widget.ProgressBar import android.widget.TextView -import androidx.lifecycle.lifecycleScope import com.android.volley.VolleyError import fnsb.macstransit.R import fnsb.macstransit.activities.mapsactivity.MapsActivity @@ -17,9 +16,6 @@ import fnsb.macstransit.activities.mapsactivity.MapsViewModel.Companion.formatTi import fnsb.macstransit.activities.mapsactivity.MapsViewModel.Companion.getTime import fnsb.macstransit.routematch.Route import fnsb.macstransit.routematch.RouteMatch -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.async -import kotlinx.coroutines.launch import org.json.JSONArray import org.json.JSONException import org.json.JSONObject diff --git a/app/src/main/res/drawable/fares.xml b/app/src/main/res/drawable/fares.xml new file mode 100644 index 0000000..d6896de --- /dev/null +++ b/app/src/main/res/drawable/fares.xml @@ -0,0 +1,15 @@ + + + + + diff --git a/app/src/main/res/drawable/invisible_divider.xml b/app/src/main/res/drawable/invisible_divider.xml new file mode 100644 index 0000000..c572471 --- /dev/null +++ b/app/src/main/res/drawable/invisible_divider.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/not_broadcasting.xml b/app/src/main/res/drawable/not_broadcasting.xml new file mode 100644 index 0000000..93af1a1 --- /dev/null +++ b/app/src/main/res/drawable/not_broadcasting.xml @@ -0,0 +1,18 @@ + + + + + + diff --git a/app/src/main/res/drawable/routes_icon.xml b/app/src/main/res/drawable/routes_icon.xml new file mode 100644 index 0000000..d1ad171 --- /dev/null +++ b/app/src/main/res/drawable/routes_icon.xml @@ -0,0 +1,24 @@ + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/settings.xml b/app/src/main/res/drawable/settings.xml new file mode 100644 index 0000000..c7ca159 --- /dev/null +++ b/app/src/main/res/drawable/settings.xml @@ -0,0 +1,10 @@ + + + + diff --git a/app/src/main/res/drawable/wifi_icon.xml b/app/src/main/res/drawable/wifi_icon.xml new file mode 100644 index 0000000..f098339 --- /dev/null +++ b/app/src/main/res/drawable/wifi_icon.xml @@ -0,0 +1,18 @@ + + + + + + diff --git a/app/src/main/res/layout/activity_maps.xml b/app/src/main/res/layout/activity_maps.xml index c7bb128..b9b788a 100644 --- a/app/src/main/res/layout/activity_maps.xml +++ b/app/src/main/res/layout/activity_maps.xml @@ -1,19 +1,71 @@ + xmlns:tools="http://schemas.android.com/tools" + xmlns:app="http://schemas.android.com/apk/res-auto"> - + - + android:layout_height="match_parent"> + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/route_menu.xml b/app/src/main/res/layout/route_menu.xml new file mode 100644 index 0000000..b385e89 --- /dev/null +++ b/app/src/main/res/layout/route_menu.xml @@ -0,0 +1,38 @@ + + + + + + + + + +