Add caching and a slightly better facility detail

parent 6d158a62
Pipeline #5121 passed with stages
in 3 minutes and 51 seconds
......@@ -47,6 +47,7 @@ dependencies {
implementation 'io.reactivex.rxjava2:rxjava:2.2.12'
implementation 'com.google.android:flexbox:1.1.1'
implementation 'androidx.constraintlayout:constraintlayout:1.1.3'
testImplementation 'junit:junit:4.12'
androidTestImplementation 'androidx.test:runner:1.2.0'
androidTestImplementation 'androidx.test.espresso:espresso-core:3.2.0'
......
package srct.whatsopen
import androidx.test.platform.app.InstrumentationRegistry
import androidx.test.ext.junit.runners.AndroidJUnit4
//import androidx.test.ext.junit.runners.AndroidJUnit4
import org.junit.Test
import org.junit.runner.RunWith
//import org.junit.runner.RunWith
import org.junit.Assert.*
......@@ -13,7 +13,7 @@ import org.junit.Assert.*
*
* See [testing documentation](http://d.android.com/tools/testing).
*/
@RunWith(AndroidJUnit4::class)
//@RunWith(AndroidJUnit4::class)
class ExampleInstrumentedTest {
@Test
fun useAppContext() {
......
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:dist="http://schemas.android.com/apk/distribution"
package="srct.whatsopen" >
package="srct.whatsopen">
<dist:module dist:instant="true" />
......@@ -13,11 +13,20 @@
android:label="@string/app_name"
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="@style/AppTheme" >
android:theme="@style/AppTheme">
<activity
android:name=".FacilityDetailActivity"
android:label="@string/title_activity_facility_detail"
android:parentActivityName=".MainActivity"
android:theme="@style/AppTheme.NoActionBar">
<meta-data
android:name="android.support.PARENT_ACTIVITY"
android:value="srct.whatsopen.MainActivity" />
</activity>
<activity
android:name=".MainActivity"
android:label="@string/app_name"
android:theme="@style/AppTheme.NoActionBar" >
android:theme="@style/AppTheme.NoActionBar">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
......
package srct.whatsopen
import android.os.Bundle
import androidx.appcompat.app.AppCompatActivity
import kotlinx.android.synthetic.main.activity_facility_detail.*
class FacilityDetailActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_facility_detail)
setSupportActionBar(toolbar)
supportActionBar?.setDisplayHomeAsUpEnabled(true)
}
}
......@@ -3,18 +3,16 @@ package srct.whatsopen
import android.annotation.SuppressLint
import android.os.Bundle
import android.util.Log
import com.google.android.material.snackbar.Snackbar
import androidx.appcompat.app.AppCompatActivity
import android.view.Menu
import android.view.MenuItem
import android.widget.TextView
import android.widget.Toast
import androidx.recyclerview.widget.RecyclerView
import androidx.appcompat.app.AppCompatActivity
import androidx.swiperefreshlayout.widget.SwipeRefreshLayout
import com.google.android.flexbox.FlexboxLayoutManager
import io.reactivex.android.schedulers.AndroidSchedulers
import io.reactivex.schedulers.Schedulers
import kotlinx.android.synthetic.main.activity_main.*
import okhttp3.Cache
import okhttp3.OkHttpClient
import retrofit2.Retrofit
import retrofit2.adapter.rxjava2.RxJava2CallAdapterFactory
import retrofit2.converter.gson.GsonConverterFactory
......@@ -22,14 +20,22 @@ import srct.whatsopen.util.MainViewAdapter
import srct.whatsopen.util.WhatsOpenService
class MainActivity : AppCompatActivity() {
private var service: WhatsOpenService? = null
private var refreshLayout: SwipeRefreshLayout? = null
private lateinit var service: WhatsOpenService
private lateinit var refreshLayout: SwipeRefreshLayout
private lateinit var cache: Cache
private lateinit var okHttpClient: OkHttpClient
private val cacheSize = (8 * 1024 * 1024).toLong() // 8 MB cache
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
setSupportActionBar(toolbar)
cache = Cache(applicationContext.cacheDir, cacheSize)
okHttpClient = OkHttpClient.Builder()
.cache(cache)
.build()
val retrofit = Retrofit.Builder()
.client(okHttpClient)
.baseUrl("https://api.srct.gmu.edu/whatsopen/v2/")
.addCallAdapterFactory(RxJava2CallAdapterFactory.create())
.addConverterFactory(GsonConverterFactory.create())
......@@ -37,7 +43,10 @@ class MainActivity : AppCompatActivity() {
service = retrofit.create(WhatsOpenService::class.java)
// setup refresh listener
refreshLayout = findViewById(R.id.swipe_refresh)
refreshLayout!!.setOnRefreshListener(this::refresh)
refreshLayout.setOnRefreshListener {
cache.evictAll() // TODO only evict main API call?
this.refresh()
}
refresh()
}
......@@ -60,12 +69,12 @@ class MainActivity : AppCompatActivity() {
@SuppressLint("CheckResult")
private fun refresh() {
refreshLayout!!.isRefreshing = true
service!!.getData()
refreshLayout.isRefreshing = true
service.getData()
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe({ facilities ->
refreshLayout!!.isRefreshing = false
refreshLayout.isRefreshing = false
recycle_view.adapter = MainViewAdapter(facilities)
}, { err ->
Toast.makeText(
......
......@@ -9,8 +9,8 @@ data class Facility(
@SerializedName("main_schedule") val mainSchedule: Schedule,
@SerializedName("special_schedules") val specialSchedules: List<Schedule>,
@SerializedName("facility_location") val location: Location,
val isFavorite: Boolean,
val statusDuration: String
@SerializedName("slug") val slug: String,
val isFavorite: Boolean
) {
fun currentSchedule(): Schedule {
val now: Date = Calendar.getInstance(TimeZone.getTimeZone("EST5EDT")).time
......
package srct.whatsopen.model
import android.util.Log
import com.google.gson.annotations.SerializedName
import java.util.*
// _Should_ return the number of seconds until the given time.
fun secondsTillTime(hour: Int, minute: Int, day: Int): Int {
val cal = Calendar.getInstance(TimeZone.getTimeZone("EST5EDT"))
// Log.d( "timebug", "current: D ${(cal.get(Calendar.DAY_OF_WEEK) + Calendar.MONDAY) % 7} H ${cal.get(Calendar.HOUR_OF_DAY)} M ${cal.get( Calendar.MINUTE )}" )
val days = (day - (cal.get(Calendar.DAY_OF_WEEK) + Calendar.MONDAY) % 7)
val days = (day - (cal.get(Calendar.DAY_OF_WEEK) + 5) % 7)
val hours = hour - cal.get(Calendar.HOUR_OF_DAY)
val minutes = minute - cal.get(Calendar.MINUTE)
val seconds = 60 - cal.get(Calendar.SECOND)
// Log.d("timebug", "other : D $day H $hour M $minute")
// Log.d("timebug", "delta : D $days H $hours M $minutes S $seconds")
var time = days * (60 * 60 * 24) + (60 * 60 * hours) + (60 * minutes) + seconds
if (time < 0) {
time += (7 * 24 * 60 * 60)
} // else {
// Log.d("timebug", " good")
// }
// Log.d( "timebug", "final : D ${time / (24 * 60 * 60)} H ${(time % (24 * 60 * 60)) / (60 * 60)} M ${(time % 3600) / 60} S ${time % 60}\n" )
}
return time
}
......@@ -28,14 +21,11 @@ fun secondsTillTime(hour: Int, minute: Int, day: Int): Int {
data class OpenTimes(
@SerializedName("start_day") var startDay: Int = 0,
@SerializedName("end_day") var endDay: Int = 0,
// These really should be Dates or something but idk exactly how to represent them
@SerializedName("start_time") var startTime: String,
@SerializedName("end_time") var endTime: String
) {
fun isOpen(): Boolean {
// TODO this is _broken_
// Log.d( "srct.whatsopen", "start $startTime end $endTime ttc ${timeTillClose()} tto ${timeTillOpen()}" )
return timeTillClose() < timeTillOpen()
}
......@@ -54,6 +44,19 @@ data class OpenTimes(
val sm = startTime.split(":")[1]
val eh = endTime.split(":")[0].toInt()
val em = endTime.split(":")[1]
return (if (sh < 12) "$sh:${sm}am" else "${(sh - 12)}:${sm}pm") + "-" + (if (eh < 12) "$eh:${em}am" else "${eh - 12}:${em}pm")
return (if (sh < 12) "${if (sh == 0) 12 else sh}:${sm}am" else "${if (sh == 12) 12 else sh - 12}:${sm}pm") + "-" + (if (eh < 12) "${if (eh == 0) 12 else eh}:${em}am" else "${if (eh == 12) 12 else eh - 12}:${em}pm")
}
fun getDay(): String {
return when {
startDay == 0 -> "Monday"
startDay == 1 -> "Tuesday"
startDay == 2 -> "Wednesday"
startDay == 3 -> "Thursday"
startDay == 4 -> "Friday"
startDay == 5 -> "Saturday"
startDay == 6 -> "Sunday"
else -> "UnknownDay"
}
}
}
\ No newline at end of file
package srct.whatsopen.util
import android.util.Log
import android.content.Intent
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.TextView
import androidx.recyclerview.widget.RecyclerView
import srct.whatsopen.FacilityDetailActivity
import srct.whatsopen.R
import srct.whatsopen.model.Facility
class MainViewAdapter(var facilities: List<Facility>) : RecyclerView.Adapter<Holder>() {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): Holder {
// return Holder(View.inflate(parent.context, R.layout.item_facility, null))
return Holder(
LayoutInflater.from(parent.context).inflate(
R.layout.item_facility,
......@@ -29,13 +29,19 @@ class MainViewAdapter(var facilities: List<Facility>) : RecyclerView.Adapter<Hol
override fun onBindViewHolder(holder: Holder, position: Int) {
val facility = facilities[position]
holder.itemView.findViewById<TextView>(R.id.facility_title).text = facility.name
// Log.d("srct.whatsopen", "title: $facility")
val text: String = if (facility.currentSchedule().isOpen24Hours) {
"Open 24 hours"
} else {
"${if (facility.isOpen()) "Open" else "Closed"}: ${facility.currentHours()}"
"${if (facility.isOpen()) "Today: " else "Next Open: ${facility.currentHours()!!.getDay()} "}${facility.currentHours()}"
}
holder.itemView.findViewById<TextView>(R.id.change_text).text = text
holder.itemView.setOnClickListener {
run {
val intent = Intent(holder.itemView.context, FacilityDetailActivity::class.java)
intent.putExtra("name", "")
holder.itemView.context.startActivity(intent)
}
}
}
}
......
......@@ -2,9 +2,12 @@ package srct.whatsopen.util
import io.reactivex.Observable
import retrofit2.http.GET
import retrofit2.http.Headers
import srct.whatsopen.model.Facility
interface WhatsOpenService {
// cache for 12 hours by default, allow 1 month stale
@Headers("Cache-Control: public, max-age=43200, max-stale=2628003")
@GET("facilities")
fun getData(): Observable<List<Facility>>
}
\ No newline at end of file
......@@ -4,6 +4,6 @@
android:viewportWidth="24.0"
android:viewportHeight="24.0">
<path
android:fillColor="#FF000000"
android:fillColor="@android:color/background_dark"
android:pathData="M12,21.35l-1.45,-1.32C5.4,15.36 2,12.28 2,8.5 2,5.42 4.42,3 7.5,3c1.74,0 3.41,0.81 4.5,2.09C13.09,3.81 14.76,3 16.5,3 19.58,3 22,5.42 22,8.5c0,3.78 -3.4,6.86 -8.55,11.54L12,21.35z"/>
</vector>
<?xml version="1.0" encoding="utf-8"?>
<androidx.coordinatorlayout.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent">
<com.google.android.material.appbar.AppBarLayout
android:id="@+id/appbar"
android:layout_height="192dp"
android:layout_width="match_parent">
<com.google.android.material.appbar.CollapsingToolbarLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
app:toolbarId="@+id/toolbar"
app:layout_scrollFlags="scroll|exitUntilCollapsed"
app:layout_scrollInterpolator="@android:anim/decelerate_interpolator"
app:contentScrim="?attr/colorPrimary">
<ImageView
android:id="@+id/facility_bg_image"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:contentDescription="@string/bg_image_desc"
android:scaleType="centerCrop"
app:layout_collapseMode="parallax" />
<androidx.appcompat.widget.Toolbar
android:id="@+id/toolbar"
android:layout_height="?attr/actionBarSize"
android:layout_width="match_parent" />
</com.google.android.material.appbar.CollapsingToolbarLayout>
</com.google.android.material.appbar.AppBarLayout>
<androidx.core.widget.NestedScrollView
android:layout_width="match_parent"
android:layout_height="match_parent"
app:layout_behavior="com.google.android.material.appbar.AppBarLayout$ScrollingViewBehavior">
<TextView
android:id="@+id/open_hours"
android:layout_width="match_parent"
android:layout_height="wrap_content"
tools:text="Hours" />
</androidx.core.widget.NestedScrollView>
</androidx.coordinatorlayout.widget.CoordinatorLayout>
\ No newline at end of file
......@@ -8,22 +8,14 @@
android:orientation="vertical"
tools:context=".MainActivity">
<androidx.appcompat.widget.Toolbar
android:id="@+id/toolbar"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="?attr/colorPrimary"
android:minHeight="?attr/actionBarSize"
android:theme="?attr/actionBarTheme">
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@string/app_name"
android:textColor="@android:color/primary_text_dark"
android:textSize="18sp" />
</androidx.appcompat.widget.Toolbar>
android:theme="?attr/actionBarTheme" />
<androidx.swiperefreshlayout.widget.SwipeRefreshLayout
android:id="@+id/swipe_refresh"
......
......@@ -28,6 +28,7 @@
android:id="@+id/facility_title"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:textAppearance="@style/TextAppearance.AppCompat.Large"
tools:text="Facility Name" />
<TextView
......@@ -42,6 +43,7 @@
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:contentDescription="@string/favorited_icon"
app:layout_flexShrink="0"
app:srcCompat="@drawable/ic_favorite_border_24dp" />
</com.google.android.flexbox.FlexboxLayout>
......
<resources>
<string name="app_name">What\'s Open</string>
<string name="large_text">
"Material is the metaphor.\n\n"
"A material metaphor is the unifying theory of a rationalized space and a system of motion."
"The material is grounded in tactile reality, inspired by the study of paper and ink, yet "
"technologically advanced and open to imagination and magic.\n"
"Surfaces and edges of the material provide visual cues that are grounded in reality. The "
"use of familiar tactile attributes helps users quickly understand affordances. Yet the "
"flexibility of the material creates new affordances that supercede those in the physical "
"world, without breaking the rules of physics.\n"
"The fundamentals of light, surface, and movement are key to conveying how objects move, "
"interact, and exist in space and in relation to each other. Realistic lighting shows "
"seams, divides space, and indicates moving parts.\n\n"
"Bold, graphic, intentional.\n\n"
"The foundational elements of print based design typography, grids, space, scale, color, "
"and use of imagery guide visual treatments. These elements do far more than please the "
"eye. They create hierarchy, meaning, and focus. Deliberate color choices, edge to edge "
"imagery, large scale typography, and intentional white space create a bold and graphic "
"interface that immerse the user in the experience.\n"
"An emphasis on user actions makes core functionality immediately apparent and provides "
"waypoints for the user.\n\n"
"Motion provides meaning.\n\n"
"Motion respects and reinforces the user as the prime mover. Primary user actions are "
"inflection points that initiate motion, transforming the whole design.\n"
"All action takes place in a single environment. Objects are presented to the user without "
"breaking the continuity of experience even as they transform and reorganize.\n"
"Motion is meaningful and appropriate, serving to focus attention and maintain continuity. "
"Feedback is subtle yet clear. Transitions are efficient yet coherent.\n\n"
"3D world.\n\n"
"The material environment is a 3D space, which means all objects have x, y, and z "
"dimensions. The z-axis is perpendicularly aligned to the plane of the display, with the "
"positive z-axis extending towards the viewer. Every sheet of material occupies a single "
"position along the z-axis and has a standard 1dp thickness.\n"
"On the web, the z-axis is used for layering and not for perspective. The 3D world is "
"emulated by manipulating the y-axis.\n\n"
"Light and shadow.\n\n"
"Within the material environment, virtual lights illuminate the scene. Key lights create "
"directional shadows, while ambient light creates soft shadows from all angles.\n"
"Shadows in the material environment are cast by these two light sources. In Android "
"development, shadows occur when light sources are blocked by sheets of material at "
"various positions along the z-axis. On the web, shadows are depicted by manipulating the "
"y-axis only. The following example shows the card with a height of 6dp.\n\n"
"Resting elevation.\n\n"
"All material objects, regardless of size, have a resting elevation, or default elevation "
"that does not change. If an object changes elevation, it should return to its resting "
"elevation as soon as possible.\n\n"
"Component elevations.\n\n"
"The resting elevation for a component type is consistent across apps (e.g., FAB elevation "
"does not vary from 6dp in one app to 16dp in another app).\n"
"Components may have different resting elevations across platforms, depending on the depth "
"of the environment (e.g., TV has a greater depth than mobile or desktop).\n\n"
"Responsive elevation and dynamic elevation offsets.\n\n"
"Some component types have responsive elevation, meaning they change elevation in response "
"to user input (e.g., normal, focused, and pressed) or system events. These elevation "
"changes are consistently implemented using dynamic elevation offsets.\n"
"Dynamic elevation offsets are the goal elevation that a component moves towards, relative "
"to the component’s resting state. They ensure that elevation changes are consistent "
"across actions and component types. For example, all components that lift on press have "
"the same elevation change relative to their resting elevation.\n"
"Once the input event is completed or cancelled, the component will return to its resting "
"elevation.\n\n"
"Avoiding elevation interference.\n\n"
"Components with responsive elevations may encounter other components as they move between "
"their resting elevations and dynamic elevation offsets. Because material cannot pass "
"through other material, components avoid interfering with one another any number of ways, "
"whether on a per component basis or using the entire app layout.\n"
"On a component level, components can move or be removed before they cause interference. "
"For example, a floating action button (FAB) can disappear or move off screen before a "
"user picks up a card, or it can move if a snackbar appears.\n"
"On the layout level, design your app layout to minimize opportunities for interference. "
"For example, position the FAB to one side of stream of a cards so the FAB won’t interfere "
"when a user tries to pick up one of cards.\n\n"
</string>
<string name="action_settings">Settings</string>
<string name="favorited_icon">favorited icon</string>
<string name="title_activity_facility_detail">FacilityDetailActivity</string>
<string name="title_activity_basic">BasicActivity</string>
<string name="bg_image_desc">Facility Background Image</string>
</resources>
<resources>
<!-- Base application theme. -->
<style name="AppTheme" parent="Theme.AppCompat.Light.DarkActionBar">
<style name="AppTheme" parent="Theme.AppCompat.DayNight">
<!-- Customize your theme here. -->
<item name="colorPrimary">@color/colorPrimary</item>
<item name="colorPrimaryDark">@color/colorPrimaryDark</item>
<item name="colorAccent">@color/colorAccent</item>
</style>
<style name="AppTheme.NoActionBar">
<item name="windowActionBar">false</item>
<item name="windowNoTitle">true</item>
</style>
<style name="AppTheme.AppBarOverlay" parent="ThemeOverlay.AppCompat.Dark.ActionBar" />
<style name="AppTheme.PopupOverlay" parent="ThemeOverlay.AppCompat.Light" />
</resources>
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment