Compare commits

...

13 Commits

33 changed files with 552 additions and 122 deletions

27
README.md Normal file
View File

@@ -0,0 +1,27 @@
# Fenceless Grazing System App
This is the repository for the mobile application part of the Fenceless
Grazing System. The app is a rather standard Android application,
written in the Kotlin programming language. To build the application,
it is sufficient to run:
```Bash
./gradlew assembleDebug
```
This will produce an APK file which can then be loaded onto
an Android device or emulator.
Note that the application makes HTTP requests to a predefined
API server URL, currently `dev.danilafe.com`. This is not suitable
for testing if you do not have access to the domain. To
reconfigure the application to use a different API
URL, change the `apiUrl` entry in the application's `strings.xml` file:
```XML
<string name="apiUrl">http://your.domain </string>
```
The application will not be of much use if it is unable
to reach the Fenceless Grazing API server. The server's
repository can be found [here](https://dev.danilafe.com/CS-46X/server).

10
REVIEW.md Normal file
View File

@@ -0,0 +1,10 @@
# Code Review Changes
https://docs.google.com/document/d/19d5U-ieQyGVVNQjj0rUouzIqF53evEP6yss-wyqNXiQ/edit?usp=sharing
|Comment|Adjustment|
|-------|----------|
|The package `com.danilafe.fencelessgrazing` can be changed to something more relevant.| Package name follows standard conventions of reverse domain name. No changes made.|
| (General remark) commenting is sparse.| Every function and field in the codebase documented using KDoc; documentation intermittently updated on [student website](https://web.engr.oregonstate.edu/~fedorind/CS46X/app/index.html).|
| No unit tests| Not worth it to do UI tests; [cow description language](https://dev.danilafe.com/CS-46x/cdl) developed for testing databases elsewhere.|
| Missing README. | README added with compilation instructions; gradle wrapper adjusted to work out of the box.|

View File

@@ -13,10 +13,11 @@
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="@style/AppTheme">
<activity android:name=".StatisticsActivity"/>
<activity android:name=".CollarDetailActivity" />
<activity android:name=".CollarListActivity" />
<activity android:name=".MainActivity">
<activity android:name=".ui.activities.BoundaryEditorActivity"></activity>
<activity android:name=".ui.activities.StatisticsActivity" />
<activity android:name=".ui.activities.CollarDetailActivity" />
<activity android:name=".ui.activities.CollarListActivity" />
<activity android:name=".ui.activities.MainActivity">
<intent-filter>
<action android:name="android.intent.action.MAIN" />

View File

@@ -6,5 +6,6 @@ package com.danilafe.fencelessgrazing.model
* @param id the collar's internal unique identifier.
* @param name the collar's current designation in the system.
* @param stimulus the number of stimulus activation reports in the last 24 hours.
* @param boundary the collar's current valid grazing boundary.
*/
data class CollarDetails(val id: Int, val name: String, val stimulus: Int)
data class CollarDetails(val id: Int, val name: String, val stimulus: Int, val boundary: List<CollarPos>)

View File

@@ -1,9 +1,17 @@
package com.danilafe.fencelessgrazing.model
import android.os.Parcelable
import kotlinx.android.parcel.Parcelize
import org.osmdroid.util.GeoPoint
/**
* GPS coordinate returned by many of the project's API endpoints.
*
* @param longitude the longitude of the GPS point.
* @param latitude the latitude of the GPS point.
*/
data class CollarPos(val longitude: String, val latitude: String)
@Parcelize
data class CollarPos(val longitude: String, val latitude: String) : Parcelable {
fun toGeoPoint(): GeoPoint = GeoPoint(latitude.toDouble(), longitude.toDouble())
}

View File

@@ -1,4 +1,4 @@
package com.danilafe.fencelessgrazing.requests
package com.danilafe.fencelessgrazing.requests.authenticated
import com.android.volley.Response
import com.android.volley.toolbox.StringRequest
@@ -7,7 +7,7 @@ import com.android.volley.toolbox.StringRequest
* General request aimed at a protected endpoint of the API. The token can be retrieved
* via a [LoginRequest], or, if you've made it past the login screen, via Shared Preferences.
*
* The [AuthenticatedRequest] expects to receive a string from the API endpoint; how this string
* The [AuthenticatedGetRequest] expects to receive a string from the API endpoint; how this string
* is handled is determined by the [Response.Listener] object. One may use
* [Response.Listener.toGsonListener] to create a JSON deserializer that expects a [String].
*
@@ -17,10 +17,10 @@ import com.android.volley.toolbox.StringRequest
* @param listener the listener object called if a valid response is received.
* @param error the error listener for the request.
*/
open class AuthenticatedRequest(
open class AuthenticatedGetRequest(
baseUrl: String,
apiEndpoint: String,
private val token: String,
val token: String,
listener: Response.Listener<String>,
error: Response.ErrorListener
) : StringRequest(

View File

@@ -0,0 +1,42 @@
package com.danilafe.fencelessgrazing.requests.authenticated
import com.android.volley.Response
import com.android.volley.toolbox.JsonObjectRequest
import com.android.volley.toolbox.StringRequest
import com.google.gson.Gson
import com.google.gson.JsonObject
import org.json.JSONObject
/**
* General POST request aimed at a protected endpoint of the API. The token can be retrieved
* via a [com.danilafe.fencelessgrazing.requests.LoginRequest], or, if you've made it past the login
* screen, via Shared Preferences.
*
* @param value the value to send as JSON to the API.
* @param baseUrl the base URL of the API.
* @param apiEndpoint the API endpoint to send the request to.
* @param token the token to use to authenticate with the API.
* @param listener the listener to be called when a response is received.
* @param error the error handler to be called if a request fails.
*/
open class AuthenticatedPostRequest<T>(
private val value: T,
baseUrl: String,
apiEndpoint: String,
private val token: String,
listener: Response.Listener<String>,
error: Response.ErrorListener
) : StringRequest(Method.POST, "${baseUrl}${apiEndpoint}",
listener, error
) {
override fun getBody(): ByteArray = Gson().toJson(value).toByteArray()
override fun getBodyContentType(): String = "application/json"
override fun getHeaders(): MutableMap<String, String> {
val newMap = HashMap(super.getHeaders())
newMap["Authorization"] = "Bearer $token"
return newMap
}
}

View File

@@ -1,11 +1,8 @@
package com.danilafe.fencelessgrazing.requests
package com.danilafe.fencelessgrazing.requests.authenticated
import com.android.volley.Response
import com.android.volley.toolbox.StringRequest
import com.danilafe.fencelessgrazing.model.CollarDetails
import com.danilafe.fencelessgrazing.model.CollarPos
import com.danilafe.fencelessgrazing.model.CollarSummary
import com.google.gson.reflect.TypeToken
import com.danilafe.fencelessgrazing.requests.toGsonListener
/**
* Request to the `/collars/<id>/details` API endpoint. Retrieves detailed information
@@ -23,7 +20,7 @@ class CollarDetailRequest(
token : String,
listener: Response.Listener<CollarDetails>,
error: Response.ErrorListener
) : AuthenticatedRequest(
) : AuthenticatedGetRequest(
baseUrl, "/collars/$collarId/details", token,
listener.toGsonListener(), error
)

View File

@@ -1,10 +1,8 @@
package com.danilafe.fencelessgrazing.requests
package com.danilafe.fencelessgrazing.requests.authenticated
import com.android.volley.Response
import com.android.volley.toolbox.StringRequest
import com.danilafe.fencelessgrazing.model.CollarPos
import com.danilafe.fencelessgrazing.model.CollarSummary
import com.google.gson.reflect.TypeToken
import com.danilafe.fencelessgrazing.requests.toGsonListener
/**
* A request to the `/collars/<id>/history` API endpoint. Retrieves the entire list
@@ -23,7 +21,7 @@ class CollarHistoryRequest(
token : String,
listener: Response.Listener<List<CollarPos>>,
error: Response.ErrorListener
) : AuthenticatedRequest(
) : AuthenticatedGetRequest(
baseUrl, "/collars/$collarId/history", token,
listener.toGsonListener(), error
)

View File

@@ -1,9 +1,8 @@
package com.danilafe.fencelessgrazing.requests
package com.danilafe.fencelessgrazing.requests.authenticated
import com.android.volley.Response
import com.android.volley.toolbox.StringRequest
import com.danilafe.fencelessgrazing.model.CollarSummary
import com.google.gson.reflect.TypeToken
import com.danilafe.fencelessgrazing.requests.toGsonListener
/**
* A request to the `/collars` API endpoint. Retrieves a list of collar summaries, represented
@@ -19,7 +18,7 @@ class CollarRequest(
token : String,
listener: Response.Listener<List<CollarSummary>>,
error: Response.ErrorListener
) : AuthenticatedRequest(
) : AuthenticatedGetRequest(
baseUrl, "/collars", token,
listener.toGsonListener(), error
)

View File

@@ -1,9 +1,8 @@
package com.danilafe.fencelessgrazing.requests
package com.danilafe.fencelessgrazing.requests.authenticated
import com.danilafe.fencelessgrazing.model.CollarDistance
import com.android.volley.Response
import com.android.volley.toolbox.StringRequest
import com.google.gson.reflect.TypeToken
import com.danilafe.fencelessgrazing.requests.toGsonListener
/**
* A request to the `/collars/stats/distance` API endpoint. Retrieves
@@ -20,7 +19,7 @@ class DistanceTraveledRequest(
token : String,
listener: Response.Listener<List<CollarDistance>>,
error: Response.ErrorListener
) : AuthenticatedRequest(
) : AuthenticatedGetRequest(
baseUrl, "/collars/stats/distance", token,
listener.toGsonListener(), error
)

View File

@@ -0,0 +1,28 @@
package com.danilafe.fencelessgrazing.requests.authenticated
import com.android.volley.Response
import com.danilafe.fencelessgrazing.model.CollarPos
/**
* Request to the API's `/collar/<id>/boundary/set` endpoint, used to update a collar's
* valid grazing area. Note that although this request takes an arbitrary number of points,
* the collar itself only supports at most 10 vertices.
*
* @param baseUrl the base URl of the API.
* @param token the API token used for authentication.
* @param identifier the collar whose boundary is being set.
* @param coordinates the list of coordinates representing, in order, the new grazing boundary's vertices.
* @param listener the listener to be called when an OK response is received.
* @param error the error listener.
*/
class SetBoundaryRequest(
baseUrl: String,
token: String,
identifier: Int,
coordinates: List<CollarPos>,
listener: Response.Listener<String>,
error: Response.ErrorListener
) : AuthenticatedPostRequest<List<CollarPos>>(
coordinates, baseUrl, "/collars/${identifier}/boundary/set",
token, listener, error
)

View File

@@ -0,0 +1,168 @@
package com.danilafe.fencelessgrazing.ui.activities
import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle
import android.os.PersistableBundle
import android.view.View
import android.widget.Toast
import androidx.appcompat.app.AlertDialog
import com.android.volley.RequestQueue
import com.android.volley.Response
import com.android.volley.toolbox.Volley
import com.danilafe.fencelessgrazing.R
import com.danilafe.fencelessgrazing.model.CollarPos
import com.danilafe.fencelessgrazing.requests.authenticated.SetBoundaryRequest
import com.danilafe.fencelessgrazing.ui.components.GrazingPolygon
import org.osmdroid.util.GeoPoint
import org.osmdroid.views.MapView
import org.osmdroid.views.overlay.Marker
import org.osmdroid.views.overlay.Polygon
/**
* Activity used to update a grazing boundary for a particular collar.
*/
class BoundaryEditorActivity : AppCompatActivity(),
Marker.OnMarkerDragListener,
Marker.OnMarkerClickListener {
/**
* The map displaying the boundary vertices and polygon.
*/
private lateinit var map: MapView
/**
* The list of markers representing the new grazing boundary's edges.
*/
private val markers: MutableList<Marker> = mutableListOf()
/**
* The polygon used to display the grazing area on the [map].
*/
private lateinit var polygon: GrazingPolygon
/**
* The center around which the [map] was originally centered,
* used also for creating new vertices.
*/
private lateinit var center: GeoPoint
/**
* The identifier of the collar whose boundary is being changed.
*/
private var identifier: Int = -1
/**
* The Volley queue used for sending API requests.
*/
private lateinit var queue: RequestQueue
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_boundary_editor)
val requestedCenter = intent.getParcelableExtra<CollarPos>("center")!!
center = requestedCenter.toGeoPoint()
identifier = intent.getIntExtra("identifier", -1)
queue = Volley.newRequestQueue(applicationContext)
map = findViewById(R.id.editorMap)
polygon = GrazingPolygon(map)
map.overlays.add(polygon)
map.controller.setZoom(9.5)
map.controller.setCenter(center)
}
/**
* Removes a marker from the [map].
*/
private fun removeMarker(m: Marker) {
markers.remove(m)
map.overlays.remove(m)
updatePolygon()
map.invalidate()
}
/**
* Sends the current list of points to the API endpoint, closing
* the activity if successful.
*/
fun sendBoundary(v: View) {
val request = SetBoundaryRequest(
getString(R.string.apiUrl),
getSharedPreferences("FencelessGrazing",0).getString("token",null)!!,
identifier, markers.map {
CollarPos(it.position.longitude.toString(), it.position.latitude.toString())
},
Response.Listener<String> {
finish()
},
Response.ErrorListener {
Toast.makeText(
this,
"Failed to update grazing boundaries",
Toast.LENGTH_SHORT).show()
}
)
queue.add(request)
}
/**
* Adds a new marker to the [map], starting it at the previously-configured
* [center].
*/
fun addMarker(v : View) {
if(markers.size >= 10) {
Toast.makeText(
this,
"Cannot add more than ten vertices to grazing boundary",
Toast.LENGTH_SHORT).show()
return
}
val newMarker = Marker(map)
newMarker.isDraggable = true
newMarker.setOnMarkerClickListener(this)
newMarker.setOnMarkerDragListener(this)
newMarker.position = center
markers.add(newMarker)
map.overlays.add(newMarker)
updatePolygon()
map.invalidate()
}
/**
* Ensures that the polygon's vertices are synchronized
* with the user-placed markers, making it so that the
* grazing area is visible to the user.
*/
private fun updatePolygon() {
polygon.points.clear()
if(markers.isEmpty()) return
val newPoints = markers.map { it.position }
polygon.points = newPoints
polygon.points.add(newPoints.first())
}
override fun onMarkerDragEnd(marker: Marker?) {
updatePolygon()
map.invalidate()
}
override fun onMarkerDragStart(marker: Marker?) {
}
override fun onMarkerDrag(marker: Marker?) {
}
override fun onMarkerClick(m: Marker, mv: MapView): Boolean {
AlertDialog.Builder(this)
.setTitle("Delete Marker")
.setMessage("Do you want to delete this marker?")
.setPositiveButton("Delete") { _, _ -> removeMarker(m) }
.setNegativeButton("Cancel") { d, _ -> d.dismiss() }
.show()
return true
}
}

View File

@@ -1,16 +1,22 @@
package com.danilafe.fencelessgrazing
package com.danilafe.fencelessgrazing.ui.activities
import android.content.Intent
import android.graphics.Color
import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle
import android.util.Log
import android.view.View
import android.widget.TextView
import android.widget.Toast
import com.android.volley.RequestQueue
import com.android.volley.Response
import com.android.volley.toolbox.Volley
import com.danilafe.fencelessgrazing.R
import com.danilafe.fencelessgrazing.model.CollarPos
import com.danilafe.fencelessgrazing.model.Polygon
import com.danilafe.fencelessgrazing.requests.CollarDetailRequest
import com.danilafe.fencelessgrazing.requests.CollarHistoryRequest
import com.danilafe.fencelessgrazing.requests.authenticated.CollarDetailRequest
import com.danilafe.fencelessgrazing.requests.authenticated.CollarHistoryRequest
import com.danilafe.fencelessgrazing.ui.components.GrazingPolygon
import org.osmdroid.util.GeoPoint
import org.osmdroid.views.MapView
import org.osmdroid.views.overlay.Marker
@@ -80,7 +86,7 @@ class CollarDetailActivity : AppCompatActivity() {
* to display the valid grazing area for the collar; its vertices
* are updated whenever the [boundingBox] property is changed.
*/
private lateinit var mapPolygon: org.osmdroid.views.overlay.Polygon
private lateinit var mapPolygon: GrazingPolygon
/**
* The [Polygon] that represents the collar's valid grazing area.
@@ -132,10 +138,9 @@ class CollarDetailActivity : AppCompatActivity() {
map = findViewById(R.id.detailMap)
map.controller.setZoom(9.5)
mapPolygon = org.osmdroid.views.overlay.Polygon(map)
mapPolygon = GrazingPolygon(map)
mapMarker = Marker(map)
mapPolyline = Polyline(map)
configureMapPolygon()
}
override fun onPause() {
@@ -166,36 +171,48 @@ class CollarDetailActivity : AppCompatActivity() {
* Sends API requests that retrieve updated information about the collar.
*/
private fun triggerRefresh() {
val historyRequest = CollarHistoryRequest(getString(R.string.apiUrl), collarId, token,
Response.Listener {
dataPoints = it.map { p -> GeoPoint(p.longitude.toDouble(), p.latitude.toDouble()) }
},
Response.ErrorListener {
Toast.makeText(this, "Failed to retrieve history of collar", Toast.LENGTH_SHORT).show()
}
)
val detailRequest = CollarDetailRequest(getString(R.string.apiUrl), collarId, token,
Response.Listener {
collarName.text = it.name
collarStimulus.text = getString(R.string.collarSummaryStimulus, it.stimulus)
},
Response.ErrorListener {
Toast.makeText(this, "Failed to retrieve details of collar", Toast.LENGTH_SHORT).show()
}
)
val historyRequest =
CollarHistoryRequest(getString(R.string.apiUrl),
collarId,
token,
Response.Listener {
dataPoints =
it.map { p -> p.toGeoPoint() }
},
Response.ErrorListener {
Toast.makeText(this, "Failed to retrieve history of collar", Toast.LENGTH_SHORT)
.show()
}
)
val detailRequest =
CollarDetailRequest(getString(R.string.apiUrl),
collarId,
token,
Response.Listener {
collarName.text = it.name
collarStimulus.text = getString(R.string.collarSummaryStimulus, it.stimulus)
boundingBox = if (it.boundary.size >= 3) Polygon(it.boundary) else null
},
Response.ErrorListener {
Toast.makeText(this, "Failed to retrieve details of collar", Toast.LENGTH_SHORT)
.show()
}
)
queue.add(historyRequest)
queue.add(detailRequest)
}
/**
* Sets [mapPolygon]'s color, borders, and other relevant settings.
* Open the [BoundaryEditorActivity] to edit the current collar's grazing boundary.
*/
private fun configureMapPolygon() {
mapPolygon.fillPaint.color = Color.parseColor("#32a852")
mapPolygon.fillPaint.alpha = 127
mapPolygon.outlinePaint.color = Color.parseColor("#227539")
mapPolygon.outlinePaint.strokeWidth = 0.0f
mapPolygon.title = "Valid Grazing Area"
fun changeGrazingBoundary(v : View) {
startActivity(Intent(this, BoundaryEditorActivity::class.java).apply {
val center = dataPoints.lastOrNull()?.let {
CollarPos(it.longitude.toString(), it.latitude.toString())
} ?: CollarPos("0", "0")
putExtra("center", center)
putExtra("identifier", collarId)
})
}
/**
@@ -234,7 +251,7 @@ class CollarDetailActivity : AppCompatActivity() {
*/
private fun updateBoundingBox(oldValue: Polygon?, polygon: Polygon) {
if(oldValue == null) map.overlays.add(mapPolygon)
val points = polygon.dataPoints.map { GeoPoint(it.longitude.toDouble(), it.latitude.toDouble()) }
val points = polygon.dataPoints.map { it.toGeoPoint() }
val polygonPoints = points.toMutableList()
polygonPoints.add(polygonPoints[0])
mapPolygon.points = polygonPoints
@@ -246,6 +263,7 @@ class CollarDetailActivity : AppCompatActivity() {
* the [mapPolygon] from the [map].
*/
private fun clearBoundingBox() {
Log.d("FencelessGrazing", "Clearing...")
map.overlays.remove(mapPolygon)
map.invalidate()
}

View File

@@ -1,4 +1,4 @@
package com.danilafe.fencelessgrazing
package com.danilafe.fencelessgrazing.ui.activities
import android.content.Intent
import androidx.appcompat.app.AppCompatActivity
@@ -12,8 +12,11 @@ import androidx.recyclerview.widget.RecyclerView
import com.android.volley.RequestQueue
import com.android.volley.Response
import com.android.volley.toolbox.Volley
import com.danilafe.fencelessgrazing.ui.components.CollarClickListener
import com.danilafe.fencelessgrazing.ui.components.CollarSummaryAdapter
import com.danilafe.fencelessgrazing.R
import com.danilafe.fencelessgrazing.model.CollarSummary
import com.danilafe.fencelessgrazing.requests.CollarRequest
import com.danilafe.fencelessgrazing.requests.authenticated.CollarRequest
import org.osmdroid.config.Configuration
import org.osmdroid.tileprovider.tilesource.TileSourceFactory
import org.osmdroid.util.GeoPoint
@@ -116,17 +119,20 @@ class CollarListActivity : AppCompatActivity() {
* Sends the request to the API to retrieve an updated list of active collars.
*/
private fun triggerRefresh() {
val request = CollarRequest(getString(R.string.apiUrl), token,
Response.Listener {
summaries.clear()
summaries.addAll(it)
summaryAdapter.notifyDataSetChanged()
updateMap()
},
Response.ErrorListener {
Toast.makeText(this, "Failed to retrieve collar list!", Toast.LENGTH_SHORT).show()
}
)
val request =
CollarRequest(getString(R.string.apiUrl),
token,
Response.Listener {
summaries.clear()
summaries.addAll(it)
summaryAdapter.notifyDataSetChanged()
updateMap()
},
Response.ErrorListener {
Toast.makeText(this, "Failed to retrieve collar list!", Toast.LENGTH_SHORT)
.show()
}
)
queue.add(request)
}
@@ -158,12 +164,16 @@ class CollarListActivity : AppCompatActivity() {
*/
private fun setupCollarList() {
val layoutManager = LinearLayoutManager(collarList.context)
summaryAdapter = CollarSummaryAdapter(summaries, object : CollarClickListener {
override fun onCollarClick(collar: CollarSummary?) {
if (collar == null) return
startCollarDetailActivity(collar)
}
})
summaryAdapter =
CollarSummaryAdapter(
summaries,
object :
CollarClickListener {
override fun onCollarClick(collar: CollarSummary?) {
if (collar == null) return
startCollarDetailActivity(collar)
}
})
collarList.adapter = summaryAdapter
collarList.layoutManager = layoutManager
collarList.addItemDecoration(DividerItemDecoration(collarList.context, layoutManager.orientation))
@@ -186,7 +196,7 @@ class CollarListActivity : AppCompatActivity() {
centerSet = true
val averageLongitude = summaries.map { it.pos.longitude.toDouble() }.average()
val averageLatitude = summaries.map { it.pos.latitude.toDouble() }.average()
map.controller.setCenter(GeoPoint(averageLongitude, averageLatitude))
map.controller.setCenter(GeoPoint(averageLatitude, averageLongitude))
}
/**
@@ -198,7 +208,7 @@ class CollarListActivity : AppCompatActivity() {
// Create or update overlay
val overlay = collarOverlays[it.id] ?: Marker(map)
overlay.title = it.name
overlay.position = GeoPoint(it.pos.longitude.toDouble(), it.pos.latitude.toDouble())
overlay.position = it.pos.toGeoPoint()
// Store new / existing overlay.
if(!collarOverlays.containsKey(it.id)) map.overlays.add(overlay)

View File

@@ -1,4 +1,4 @@
package com.danilafe.fencelessgrazing
package com.danilafe.fencelessgrazing.ui.activities
import android.os.Bundle
import android.view.LayoutInflater
@@ -9,8 +9,9 @@ import androidx.fragment.app.Fragment
import com.android.volley.RequestQueue
import com.android.volley.Response
import com.android.volley.toolbox.Volley
import com.danilafe.fencelessgrazing.R
import com.danilafe.fencelessgrazing.model.CollarDistance
import com.danilafe.fencelessgrazing.requests.DistanceTraveledRequest
import com.danilafe.fencelessgrazing.requests.authenticated.DistanceTraveledRequest
import com.github.mikephil.charting.charts.BarChart
import com.github.mikephil.charting.components.XAxis
import com.github.mikephil.charting.data.BarData
@@ -61,14 +62,20 @@ class DistanceTraveledGraph() : Fragment() {
val activity = requireActivity()
val sharedPrefs = activity.getSharedPreferences("FencelessGrazing", 0)
val token = sharedPrefs.getString("token", null)!!
val request = DistanceTraveledRequest(activity.getString(R.string.apiUrl), token,
Response.Listener {
updateChartData(it)
},
Response.ErrorListener {
Toast.makeText(activity, "Failed to retrieve distance traveled!", Toast.LENGTH_SHORT).show()
}
)
val request =
DistanceTraveledRequest(activity.getString(R.string.apiUrl),
token,
Response.Listener {
updateChartData(it)
},
Response.ErrorListener {
Toast.makeText(
activity,
"Failed to retrieve distance traveled!",
Toast.LENGTH_SHORT
).show()
}
)
queue.add(request)
}

View File

@@ -1,4 +1,4 @@
package com.danilafe.fencelessgrazing
package com.danilafe.fencelessgrazing.ui.activities
import android.content.Intent
import androidx.appcompat.app.AppCompatActivity
@@ -8,6 +8,7 @@ import android.widget.TextView
import android.widget.Toast
import com.android.volley.Response
import com.android.volley.toolbox.Volley
import com.danilafe.fencelessgrazing.R
import com.danilafe.fencelessgrazing.requests.LoginRequest
/**

View File

@@ -1,10 +1,10 @@
package com.danilafe.fencelessgrazing
package com.danilafe.fencelessgrazing.ui.activities
import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle
import androidx.fragment.app.FragmentActivity
import androidx.viewpager.widget.ViewPager
import androidx.viewpager2.widget.ViewPager2
import com.danilafe.fencelessgrazing.R
import com.danilafe.fencelessgrazing.ui.components.StatisticsGraphAdapter
import com.google.android.material.tabs.TabLayout
import com.google.android.material.tabs.TabLayoutMediator
@@ -37,7 +37,10 @@ class StatisticsActivity : AppCompatActivity() {
tabLayout = findViewById(R.id.statisticsTabs)
viewPager = findViewById(R.id.statisticsPager)
viewPager.adapter = StatisticsGraphAdapter(this)
viewPager.adapter =
StatisticsGraphAdapter(
this
)
TabLayoutMediator(tabLayout, viewPager) { tab, position ->
tab.text = tabNames[position]
}.attach()

View File

@@ -1,4 +1,4 @@
package com.danilafe.fencelessgrazing
package com.danilafe.fencelessgrazing.ui.components
import com.danilafe.fencelessgrazing.model.CollarSummary

View File

@@ -1,15 +1,12 @@
package com.danilafe.fencelessgrazing
package com.danilafe.fencelessgrazing.ui.components
import android.content.Context
import android.view.LayoutInflater
import android.view.ViewGroup
import android.widget.Toast
import androidx.recyclerview.widget.ListAdapter
import androidx.recyclerview.widget.DiffUtil
import com.android.volley.Response
import com.android.volley.toolbox.Volley
import com.danilafe.fencelessgrazing.R
import com.danilafe.fencelessgrazing.model.CollarSummary
import com.danilafe.fencelessgrazing.requests.CollarRequest
import com.danilafe.fencelessgrazing.requests.authenticated.CollarRequest
/**
* A [ListAdapter] subclass to display and manage a list of [CollarSummary] items
@@ -21,7 +18,9 @@ import com.danilafe.fencelessgrazing.requests.CollarRequest
class CollarSummaryAdapter(
private val items: List<CollarSummary>,
private val collarClickListener: CollarClickListener
) : ListAdapter<CollarSummary, CollarViewHolder>(DiffCallback()) {
) : ListAdapter<CollarSummary, CollarViewHolder>(
DiffCallback()
) {
/**
* [DiffUtil.ItemCallback] used for the [ListAdapter]. Compares items using
@@ -37,14 +36,17 @@ class CollarSummaryAdapter(
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): CollarViewHolder {
val layout = LayoutInflater.from(parent.context).inflate(viewType, parent, false)
return CollarViewHolder(layout)
return CollarViewHolder(
layout
)
}
override fun onBindViewHolder(holder: CollarViewHolder, position: Int) {
holder.bindData(getItem(position), collarClickListener)
}
override fun getItemViewType(position: Int): Int = R.layout.collar_summary_layout
override fun getItemViewType(position: Int): Int =
R.layout.collar_summary_layout
override fun getItem(position: Int): CollarSummary = items[position]

View File

@@ -1,8 +1,9 @@
package com.danilafe.fencelessgrazing
package com.danilafe.fencelessgrazing.ui.components
import android.view.View
import android.widget.TextView
import androidx.recyclerview.widget.RecyclerView
import com.danilafe.fencelessgrazing.R
import com.danilafe.fencelessgrazing.model.CollarSummary
/**
@@ -28,7 +29,8 @@ class CollarViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
*/
fun bindData(summary: CollarSummary, collarClickListener: CollarClickListener) {
nameView.text = summary.name
positionView.text = itemView.resources.getString(R.string.collarSummaryLocation,
positionView.text = itemView.resources.getString(
R.string.collarSummaryLocation,
summary.pos.longitude.toDouble(), summary.pos.latitude.toDouble())
itemView.setOnClickListener {
collarClickListener.onCollarClick(summary)

View File

@@ -0,0 +1,18 @@
package com.danilafe.fencelessgrazing.ui.components
import android.graphics.Color
import org.osmdroid.views.MapView
import org.osmdroid.views.overlay.Polygon
/**
* OpenStreetMaps polygon with some default visual settings for the Fenceless Grazing System.
*/
class GrazingPolygon(map: MapView) : Polygon(map) {
init {
fillPaint.color = Color.parseColor("#32a852")
fillPaint.alpha = 127
outlinePaint.color = Color.parseColor("#227539")
outlinePaint.strokeWidth = 0.0f
title = "Valid Grazing Area"
}
}

View File

@@ -1,13 +1,15 @@
package com.danilafe.fencelessgrazing
package com.danilafe.fencelessgrazing.ui.components
import androidx.fragment.app.Fragment
import androidx.fragment.app.FragmentActivity
import androidx.viewpager2.adapter.FragmentStateAdapter
import com.danilafe.fencelessgrazing.ui.activities.DistanceTraveledGraph
/**
* Adapter for the [ViewPager2][androidx.viewpager2.widget.ViewPager2] class.
*/
class StatisticsGraphAdapter(activity : FragmentActivity) : FragmentStateAdapter(activity) {
override fun getItemCount(): Int = 1
override fun createFragment(position: Int): Fragment = DistanceTraveledGraph()
override fun createFragment(position: Int): Fragment =
DistanceTraveledGraph()
}

View File

@@ -0,0 +1,46 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".ui.activities.BoundaryEditorActivity">
<org.osmdroid.views.MapView
android:id="@+id/editorMap"
android:layout_width="match_parent"
android:layout_height="0dp"
app:layout_constraintBottom_toTopOf="@id/saveButtonLayout"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent">
</org.osmdroid.views.MapView>
<LinearLayout
android:id="@+id/saveButtonLayout"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent">
<Button
android:onClick="sendBoundary"
android:id="@+id/saveBoundaryButton"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="@string/saveBoundary" />
<Button
android:onClick="addMarker"
android:id="@+id/addVertexButton"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="@string/addVertex" />
</LinearLayout>
</androidx.constraintlayout.widget.ConstraintLayout>

View File

@@ -4,7 +4,7 @@
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".CollarDetailActivity">
tools:context=".ui.activities.CollarDetailActivity">
<org.osmdroid.views.MapView
android:id="@+id/detailMap"
android:layout_width="match_parent"
@@ -29,9 +29,8 @@
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="10dp"
android:layout_marginTop="10dp"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/collarName" />
app:layout_constraintTop_toBottomOf="@+id/changeBoundaryButton" />
<TextView
android:id="@+id/collarStimulus"
@@ -42,4 +41,14 @@
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/collarPos" />
<Button
android:id="@+id/changeBoundaryButton"
style="@style/Widget.AppCompat.Button.Colored"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:onClick="changeGrazingBoundary"
android:text="@string/changeGrazingBoundary"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/collarName" />
</androidx.constraintlayout.widget.ConstraintLayout>

View File

@@ -4,7 +4,7 @@
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".CollarListActivity">
tools:context=".ui.activities.CollarListActivity">
<org.osmdroid.views.MapView
android:id="@+id/map"

View File

@@ -4,7 +4,7 @@
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity">
tools:context=".ui.activities.MainActivity">
<EditText
android:id="@+id/username"

View File

@@ -4,7 +4,7 @@
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".StatisticsActivity">
tools:context=".ui.activities.StatisticsActivity">
<com.google.android.material.tabs.TabLayout
android:id="@+id/statisticsTabs"

View File

@@ -8,4 +8,7 @@
<string name="collarSummaryStimulus">Required %1d stimuli in the last 24 hours</string>
<string name="distanceTraveled">Distance Traveled</string>
<string name="viewStatistics">View Statistics</string>
<string name="saveBoundary">Save Boundary</string>
<string name="addVertex">Add Vertex</string>
<string name="changeGrazingBoundary">Change Grazing Boundary</string>
</resources>

BIN
gradle/wrapper/gradle-wrapper.jar vendored Normal file

Binary file not shown.

View File

@@ -1,6 +1,5 @@
#Sat Feb 01 14:34:24 PST 2020
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-6.4-all.zip
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-5.4.1-all.zip

22
gradlew vendored
View File

@@ -1,5 +1,21 @@
#!/usr/bin/env sh
#
# Copyright 2015 the original author or authors.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# https://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
##############################################################################
##
## Gradle start up script for UN*X
@@ -28,7 +44,7 @@ APP_NAME="Gradle"
APP_BASE_NAME=`basename "$0"`
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
DEFAULT_JVM_OPTS=""
DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
# Use the maximum available, or set MAX_FD != -1 to use that value.
MAX_FD="maximum"
@@ -109,8 +125,8 @@ if $darwin; then
GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
fi
# For Cygwin, switch paths to Windows format before running java
if $cygwin ; then
# For Cygwin or MSYS, switch paths to Windows format before running java
if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then
APP_HOME=`cygpath --path --mixed "$APP_HOME"`
CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
JAVACMD=`cygpath --unix "$JAVACMD"`

18
gradlew.bat vendored
View File

@@ -1,3 +1,19 @@
@rem
@rem Copyright 2015 the original author or authors.
@rem
@rem Licensed under the Apache License, Version 2.0 (the "License");
@rem you may not use this file except in compliance with the License.
@rem You may obtain a copy of the License at
@rem
@rem https://www.apache.org/licenses/LICENSE-2.0
@rem
@rem Unless required by applicable law or agreed to in writing, software
@rem distributed under the License is distributed on an "AS IS" BASIS,
@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
@rem See the License for the specific language governing permissions and
@rem limitations under the License.
@rem
@if "%DEBUG%" == "" @echo off
@rem ##########################################################################
@rem
@@ -14,7 +30,7 @@ set APP_BASE_NAME=%~n0
set APP_HOME=%DIRNAME%
@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
set DEFAULT_JVM_OPTS=
set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
@rem Find java.exe
if defined JAVA_HOME goto findJavaFromJavaHome