diff --git a/app/build.gradle b/app/build.gradle index 43a900f..37bf1d7 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 35 + versionName 'Release 1.5.1' 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/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 0c136f1..ce4ee6f 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -38,14 +38,14 @@ android:label="@string/settings" android:launchMode="standard" android:screenOrientation="portrait" - android:theme="@style/AppThemeNoActionBar" + android:theme="@style/AppTheme" tools:ignore="LockedOrientationActivity" /> 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..76b3c75 100644 --- a/app/src/main/java/fnsb/macstransit/activities/mapsactivity/MapsActivity.kt +++ b/app/src/main/java/fnsb/macstransit/activities/mapsactivity/MapsActivity.kt @@ -1,23 +1,28 @@ package fnsb.macstransit.activities.mapsactivity +import android.graphics.Color +import androidx.core.graphics.Insets import android.os.Bundle import fnsb.macstransit.routematch.Route -import fnsb.macstransit.settings.V2 import fnsb.macstransit.R import android.util.Log -import android.view.Menu +import android.view.Gravity +import android.view.ViewGroup +import androidx.activity.SystemBarStyle +import androidx.activity.enableEdgeToEdge import androidx.core.view.ViewCompat import androidx.core.view.WindowInsetsCompat -import androidx.core.view.updatePadding import androidx.fragment.app.FragmentActivity import androidx.lifecycle.Lifecycle 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,24 +39,37 @@ 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 + + lateinit var binding: ActivityMapsBinding + + var bars: Insets? = null override fun onCreate(savedInstanceState: Bundle?) { Log.v("onCreate", "onCreate has been called!") + + // https://stackoverflow.com/questions/76960994/enableedgetoedge-navigation-system-bar-is-not-fully-transparent + enableEdgeToEdge( + statusBarStyle = SystemBarStyle.auto(Color.TRANSPARENT, Color.TRANSPARENT), + navigationBarStyle = SystemBarStyle.light(Color.TRANSPARENT, Color.TRANSPARENT) // light causes internally enforce the navigation bar to be fully transparent + ) super.onCreate(savedInstanceState) // Setup view model. viewModel = ViewModelProvider(this)[MapsViewModel::class.java] // Setup binding. - val binding: ActivityMapsBinding = ActivityMapsBinding.inflate(layoutInflater) + binding = ActivityMapsBinding.inflate(layoutInflater) binding.viewmodel = viewModel binding.lifecycleOwner = this + binding.activity = this // Set the activity view to the map activity layout. setContentView(binding.root) @@ -83,18 +101,14 @@ class MapsActivity: FragmentActivity() { Log.v("onCreate", "Setting up fare window") farePopupWindow = FarePopupWindow(this) - ViewCompat.setOnApplyWindowInsetsListener(binding.root) { view, insets -> + ViewCompat.setOnApplyWindowInsetsListener(binding.root) { _, insets -> val bars = insets.getInsets( WindowInsetsCompat.Type.systemBars() or WindowInsetsCompat.Type.displayCutout() ) - view.updatePadding( - left = bars.left, - top = bars.top, - right = bars.right, - bottom = bars.bottom - ) + binding.buttons.setPadding(bars.left,bars.top, bars.right, bars.bottom) + this.bars = bars WindowInsetsCompat.CONSUMED } @@ -149,139 +163,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 +186,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 { /** @@ -312,6 +219,5 @@ class MapsActivity: FragmentActivity() { * This will be set to true coming out of SplashActivity */ var firstRun: Boolean = true - } } diff --git a/app/src/main/java/fnsb/macstransit/activities/mapsactivity/MapsViewModel.kt b/app/src/main/java/fnsb/macstransit/activities/mapsactivity/MapsViewModel.kt index 4e2613d..6836676 100644 --- a/app/src/main/java/fnsb/macstransit/activities/mapsactivity/MapsViewModel.kt +++ b/app/src/main/java/fnsb/macstransit/activities/mapsactivity/MapsViewModel.kt @@ -149,6 +149,11 @@ class MapsViewModel(application: Application): androidx.lifecycle.AndroidViewMod Log.v("MapCoroutine", "Awaiting map...") val map = supportFragment.awaitMap() + val insets = activity.bars + if (insets != null) { + map.setPadding(insets.left,insets.top, insets.right, insets.bottom) + } + // Move the camera to the 'home' position. Log.v("MapCoroutine", "Moving camera to home position") map.moveCamera(com.google.android.gms.maps.CameraUpdateFactory.newLatLngZoom( 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..9265bff --- /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..29e2888 100644 --- a/app/src/main/res/layout/activity_maps.xml +++ b/app/src/main/res/layout/activity_maps.xml @@ -1,19 +1,73 @@ + 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 @@ + + + + + + + + + +