mirror of
https://github.com/MatomoCamp/matomocamp-companion-android.git
synced 2024-09-19 16:13:46 +02:00
Replace Date and Calendar classes with Java 8 time APIs (#74)
Enable multidex and Java 8 APIs desugaring in the project. Also bump minSDK version to 18 to allow calling DateFormat.getBestDateTimePattern().
This commit is contained in:
parent
d35d8257ec
commit
5440860340
31 changed files with 267 additions and 237 deletions
|
@ -12,8 +12,9 @@ android {
|
|||
|
||||
defaultConfig {
|
||||
applicationId "be.digitalia.fosdem"
|
||||
minSdkVersion 17
|
||||
minSdkVersion 18
|
||||
targetSdkVersion 31
|
||||
multiDexEnabled = true
|
||||
versionCode 1700205
|
||||
versionName "2.0.5"
|
||||
// Supported languages
|
||||
|
@ -48,15 +49,15 @@ android {
|
|||
}
|
||||
|
||||
packagingOptions {
|
||||
exclude 'androidsupportmultidexversion.txt'
|
||||
exclude 'DebugProbesKt.bin'
|
||||
exclude 'kotlin-tooling-metadata.json'
|
||||
}
|
||||
}
|
||||
debug {
|
||||
multiDexEnabled = true
|
||||
}
|
||||
}
|
||||
|
||||
compileOptions {
|
||||
coreLibraryDesugaringEnabled true
|
||||
sourceCompatibility JavaVersion.VERSION_1_8
|
||||
targetCompatibility JavaVersion.VERSION_1_8
|
||||
}
|
||||
|
@ -80,6 +81,8 @@ dependencies {
|
|||
def room_version = "2.4.0"
|
||||
def okhttp_version = "3.12.13"
|
||||
|
||||
coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:1.1.5'
|
||||
|
||||
implementation "androidx.multidex:multidex:2.0.1"
|
||||
implementation "com.google.dagger:hilt-android:$hilt_version"
|
||||
kapt "com.google.dagger:hilt-compiler:$hilt_version"
|
||||
|
|
|
@ -28,8 +28,6 @@ class FosdemApplication : Application() {
|
|||
|
||||
override fun attachBaseContext(base: Context?) {
|
||||
super.attachBaseContext(base)
|
||||
if (BuildConfig.DEBUG) {
|
||||
MultiDex.install(this)
|
||||
}
|
||||
MultiDex.install(this)
|
||||
}
|
||||
}
|
|
@ -9,8 +9,6 @@ import android.net.Uri
|
|||
import android.nfc.NdefRecord
|
||||
import android.os.Build
|
||||
import android.os.Bundle
|
||||
import android.text.format.DateFormat
|
||||
import android.text.format.DateUtils
|
||||
import android.view.Menu
|
||||
import android.view.MenuItem
|
||||
import android.view.View
|
||||
|
@ -47,9 +45,12 @@ import com.google.android.material.progressindicator.BaseProgressIndicator
|
|||
import com.google.android.material.snackbar.Snackbar
|
||||
import dagger.hilt.android.AndroidEntryPoint
|
||||
import kotlinx.coroutines.CancellationException
|
||||
import kotlinx.coroutines.flow.collect
|
||||
import kotlinx.coroutines.flow.first
|
||||
import kotlinx.coroutines.launch
|
||||
import java.time.Duration
|
||||
import java.time.Instant
|
||||
import java.time.ZoneId
|
||||
import java.time.format.DateTimeFormatter
|
||||
import javax.inject.Inject
|
||||
import javax.inject.Named
|
||||
|
||||
|
@ -92,6 +93,8 @@ class MainActivity : AppCompatActivity(R.layout.main), CreateNfcAppDataCallback
|
|||
@Inject
|
||||
lateinit var scheduleDao: ScheduleDao
|
||||
|
||||
private val latestUpdateDateTimeFormatter = DateTimeFormatter.ofPattern(LATEST_UPDATE_DATE_TIME_FORMAT)
|
||||
|
||||
private lateinit var holder: ViewHolder
|
||||
private lateinit var drawerToggle: ActionBarDrawerToggle
|
||||
|
||||
|
@ -188,7 +191,7 @@ class MainActivity : AppCompatActivity(R.layout.main), CreateNfcAppDataCallback
|
|||
val latestUpdateTextView: TextView = navigationView.findViewById(R.id.latest_update)
|
||||
lifecycleScope.launch {
|
||||
scheduleDao.latestUpdateTime.collect { time ->
|
||||
val timeString = time?.let { DateFormat.format(LATEST_UPDATE_DATE_FORMAT, it) }
|
||||
val timeString = time?.atZone(ZoneId.systemDefault())?.format(latestUpdateDateTimeFormatter)
|
||||
?: getString(R.string.never)
|
||||
latestUpdateTextView.text = getString(R.string.last_update, timeString)
|
||||
}
|
||||
|
@ -251,13 +254,15 @@ class MainActivity : AppCompatActivity(R.layout.main), CreateNfcAppDataCallback
|
|||
|
||||
// Scheduled database update
|
||||
lifecycleScope.launch {
|
||||
val now = System.currentTimeMillis()
|
||||
val now = Instant.now()
|
||||
val latestUpdateTime = scheduleDao.latestUpdateTime.first()
|
||||
if (latestUpdateTime == null || latestUpdateTime.time < now - DATABASE_VALIDITY_DURATION) {
|
||||
val latestAttemptTime = preferences.getLong(LATEST_UPDATE_ATTEMPT_TIME_PREF_KEY, -1L)
|
||||
if (latestAttemptTime == -1L || latestAttemptTime < now - AUTO_UPDATE_SNOOZE_DURATION) {
|
||||
if (latestUpdateTime == null || latestUpdateTime < now - DATABASE_VALIDITY_DURATION) {
|
||||
val latestAttemptTime = Instant.ofEpochMilli(
|
||||
preferences.getLong(LATEST_UPDATE_ATTEMPT_TIME_PREF_KEY, 0L)
|
||||
)
|
||||
if (latestAttemptTime == Instant.EPOCH || latestAttemptTime < now - AUTO_UPDATE_SNOOZE_DURATION) {
|
||||
preferences.edit {
|
||||
putLong(LATEST_UPDATE_ATTEMPT_TIME_PREF_KEY, now)
|
||||
putLong(LATEST_UPDATE_ATTEMPT_TIME_PREF_KEY, now.toEpochMilli())
|
||||
}
|
||||
// Try to update immediately. If it fails, the user gets a message and a retry button.
|
||||
api.downloadSchedule()
|
||||
|
@ -358,9 +363,9 @@ class MainActivity : AppCompatActivity(R.layout.main), CreateNfcAppDataCallback
|
|||
const val ACTION_SHORTCUT_LIVE = "${BuildConfig.APPLICATION_ID}.intent.action.SHORTCUT_LIVE"
|
||||
|
||||
private const val ERROR_MESSAGE_DISPLAY_DURATION = 5000
|
||||
private const val DATABASE_VALIDITY_DURATION = DateUtils.DAY_IN_MILLIS
|
||||
private const val AUTO_UPDATE_SNOOZE_DURATION = DateUtils.DAY_IN_MILLIS
|
||||
private val DATABASE_VALIDITY_DURATION = Duration.ofDays(1L)
|
||||
private val AUTO_UPDATE_SNOOZE_DURATION = Duration.ofDays(1L)
|
||||
private const val LATEST_UPDATE_ATTEMPT_TIME_PREF_KEY = "latest_update_attempt_time"
|
||||
private const val LATEST_UPDATE_DATE_FORMAT = "d MMM yyyy kk:mm:ss"
|
||||
private const val LATEST_UPDATE_DATE_TIME_FORMAT = "d MMM yyyy kk:mm:ss"
|
||||
}
|
||||
}
|
|
@ -12,7 +12,6 @@ import androidx.fragment.app.commit
|
|||
import be.digitalia.fosdem.R
|
||||
import be.digitalia.fosdem.fragments.PersonInfoListFragment
|
||||
import be.digitalia.fosdem.model.Person
|
||||
import be.digitalia.fosdem.utils.DateUtils
|
||||
import be.digitalia.fosdem.utils.configureToolbarColors
|
||||
import be.digitalia.fosdem.viewmodels.PersonInfoViewModel
|
||||
import dagger.hilt.android.AndroidEntryPoint
|
||||
|
@ -36,8 +35,7 @@ class PersonInfoActivity : AppCompatActivity(R.layout.person_info) {
|
|||
// Look for the first non-placeholder event in the paged list
|
||||
val statusEvent = viewModel.events.value?.firstOrNull { it != null }
|
||||
if (statusEvent != null) {
|
||||
val year = DateUtils.getYear(statusEvent.event.day.date.time)
|
||||
val url = person.getUrl(year)
|
||||
val url = person.getUrl(statusEvent.event.day.date.year)
|
||||
if (url != null) {
|
||||
try {
|
||||
CustomTabsIntent.Builder()
|
||||
|
|
|
@ -24,12 +24,12 @@ import be.digitalia.fosdem.model.Event
|
|||
import be.digitalia.fosdem.model.RoomStatus
|
||||
import be.digitalia.fosdem.utils.DateUtils
|
||||
import be.digitalia.fosdem.widgets.MultiChoiceHelper
|
||||
import java.text.DateFormat
|
||||
import java.time.format.DateTimeFormatter
|
||||
|
||||
class BookmarksAdapter(context: Context, private val multiChoiceHelper: MultiChoiceHelper) :
|
||||
ListAdapter<Event, BookmarksAdapter.ViewHolder>(DIFF_CALLBACK) {
|
||||
|
||||
private val timeDateFormat = DateUtils.getTimeDateFormat(context)
|
||||
private val timeFormatter = DateUtils.getTimeFormatter(context)
|
||||
|
||||
@ColorInt
|
||||
private val errorColor: Int
|
||||
|
@ -53,7 +53,7 @@ class BookmarksAdapter(context: Context, private val multiChoiceHelper: MultiCho
|
|||
|
||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
|
||||
val view = LayoutInflater.from(parent.context).inflate(R.layout.item_event, parent, false)
|
||||
return ViewHolder(view, multiChoiceHelper, timeDateFormat, errorColor)
|
||||
return ViewHolder(view, multiChoiceHelper, timeFormatter, errorColor)
|
||||
}
|
||||
|
||||
private fun getRoomStatus(event: Event): RoomStatus? {
|
||||
|
@ -101,7 +101,7 @@ class BookmarksAdapter(context: Context, private val multiChoiceHelper: MultiCho
|
|||
}
|
||||
|
||||
class ViewHolder(itemView: View, helper: MultiChoiceHelper,
|
||||
private val timeDateFormat: DateFormat, @ColorInt private val errorColor: Int)
|
||||
private val timeFormatter: DateTimeFormatter, @ColorInt private val errorColor: Int)
|
||||
: MultiChoiceHelper.ViewHolder(itemView, helper), View.OnClickListener {
|
||||
private val title: TextView = itemView.findViewById(R.id.title)
|
||||
private val persons: TextView = itemView.findViewById(R.id.persons)
|
||||
|
@ -130,10 +130,8 @@ class BookmarksAdapter(context: Context, private val multiChoiceHelper: MultiCho
|
|||
|
||||
fun bindDetails(event: Event, previous: Event?, next: Event?, roomStatus: RoomStatus?) {
|
||||
val context = details.context
|
||||
val startTime = event.startTime
|
||||
val endTime = event.endTime
|
||||
val startTimeString = if (startTime != null) timeDateFormat.format(startTime) else "?"
|
||||
val endTimeString = if (endTime != null) timeDateFormat.format(endTime) else "?"
|
||||
val startTimeString = event.startTime?.atZone(DateUtils.conferenceZoneId)?.format(timeFormatter) ?: "?"
|
||||
val endTimeString = event.endTime?.atZone(DateUtils.conferenceZoneId)?.format(timeFormatter) ?: "?"
|
||||
val roomName = event.roomName.orEmpty()
|
||||
val detailsText: CharSequence = "${event.day.shortName}, $startTimeString ― $endTimeString | $roomName"
|
||||
val detailsSpannable = SpannableString(detailsText)
|
||||
|
|
|
@ -21,12 +21,12 @@ import be.digitalia.fosdem.model.Event
|
|||
import be.digitalia.fosdem.model.RoomStatus
|
||||
import be.digitalia.fosdem.model.StatusEvent
|
||||
import be.digitalia.fosdem.utils.DateUtils
|
||||
import java.text.DateFormat
|
||||
import java.time.format.DateTimeFormatter
|
||||
|
||||
class EventsAdapter constructor(context: Context, private val showDay: Boolean = true) :
|
||||
PagedListAdapter<StatusEvent, EventsAdapter.ViewHolder>(DIFF_CALLBACK) {
|
||||
|
||||
private val timeDateFormat = DateUtils.getTimeDateFormat(context)
|
||||
private val timeFormatter = DateUtils.getTimeFormatter(context)
|
||||
|
||||
var roomStatuses: Map<String, RoomStatus>? = null
|
||||
set(value) {
|
||||
|
@ -38,7 +38,7 @@ class EventsAdapter constructor(context: Context, private val showDay: Boolean =
|
|||
|
||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
|
||||
val view = LayoutInflater.from(parent.context).inflate(R.layout.item_event, parent, false)
|
||||
return ViewHolder(view, timeDateFormat)
|
||||
return ViewHolder(view, timeFormatter)
|
||||
}
|
||||
|
||||
private fun getRoomStatus(event: Event): RoomStatus? {
|
||||
|
@ -70,7 +70,7 @@ class EventsAdapter constructor(context: Context, private val showDay: Boolean =
|
|||
}
|
||||
}
|
||||
|
||||
class ViewHolder(itemView: View, private val timeDateFormat: DateFormat)
|
||||
class ViewHolder(itemView: View, private val timeFormatter: DateTimeFormatter)
|
||||
: RecyclerView.ViewHolder(itemView), View.OnClickListener {
|
||||
private val title: TextView = itemView.findViewById(R.id.title)
|
||||
private val persons: TextView = itemView.findViewById(R.id.persons)
|
||||
|
@ -112,10 +112,8 @@ class EventsAdapter constructor(context: Context, private val showDay: Boolean =
|
|||
|
||||
fun bindDetails(event: Event, showDay: Boolean, roomStatus: RoomStatus?) {
|
||||
val context = details.context
|
||||
val startTime = event.startTime
|
||||
val endTime = event.endTime
|
||||
val startTimeString = if (startTime != null) timeDateFormat.format(startTime) else "?"
|
||||
val endTimeString = if (endTime != null) timeDateFormat.format(endTime) else "?"
|
||||
val startTimeString = event.startTime?.atZone(DateUtils.conferenceZoneId)?.format(timeFormatter) ?: "?"
|
||||
val endTimeString = event.endTime?.atZone(DateUtils.conferenceZoneId)?.format(timeFormatter) ?: "?"
|
||||
val roomName = event.roomName.orEmpty()
|
||||
var detailsText: CharSequence = if (showDay) {
|
||||
"${event.day.shortName}, $startTimeString ― $endTimeString | $roomName"
|
||||
|
|
|
@ -18,6 +18,7 @@ import be.digitalia.fosdem.R
|
|||
import be.digitalia.fosdem.model.Event
|
||||
import be.digitalia.fosdem.model.StatusEvent
|
||||
import be.digitalia.fosdem.utils.DateUtils
|
||||
import java.time.Instant
|
||||
|
||||
class TrackScheduleAdapter(context: Context, private val listener: EventClickListener? = null)
|
||||
: ListAdapter<StatusEvent, TrackScheduleAdapter.ViewHolder>(EventsAdapter.DIFF_CALLBACK) {
|
||||
|
@ -26,7 +27,7 @@ class TrackScheduleAdapter(context: Context, private val listener: EventClickLis
|
|||
fun onEventClick(event: Event)
|
||||
}
|
||||
|
||||
private val timeDateFormat = DateUtils.getTimeDateFormat(context)
|
||||
private val timeFormatter = DateUtils.getTimeFormatter(context)
|
||||
@ColorInt
|
||||
private val timeBackgroundColor: Int = ContextCompat.getColor(context, R.color.schedule_time_background)
|
||||
@ColorInt
|
||||
|
@ -46,7 +47,7 @@ class TrackScheduleAdapter(context: Context, private val listener: EventClickLis
|
|||
}
|
||||
}
|
||||
|
||||
var currentTime: Long = -1L
|
||||
var currentTime: Instant? = null
|
||||
set(value) {
|
||||
if (field != value) {
|
||||
field = value
|
||||
|
@ -127,7 +128,7 @@ class TrackScheduleAdapter(context: Context, private val listener: EventClickLis
|
|||
val context = itemView.context
|
||||
this.event = event
|
||||
|
||||
time.text = event.startTime?.let { timeDateFormat.format(it) }
|
||||
time.text = event.startTime?.atZone(DateUtils.conferenceZoneId)?.format(timeFormatter)
|
||||
title.text = event.title
|
||||
val bookmarkDrawable = if (isBookmarked) AppCompatResources.getDrawable(context, R.drawable.ic_bookmark_white_24dp) else null
|
||||
TextViewCompat.setCompoundDrawablesRelativeWithIntrinsicBounds(title, null, null, bookmarkDrawable, null)
|
||||
|
@ -141,8 +142,8 @@ class TrackScheduleAdapter(context: Context, private val listener: EventClickLis
|
|||
room.contentDescription = context.getString(R.string.room_content_description, event.roomName.orEmpty())
|
||||
}
|
||||
|
||||
fun bindTimeColors(event: Event, currentTime: Long) {
|
||||
if (currentTime != -1L && event.isRunningAtTime(currentTime)) {
|
||||
fun bindTimeColors(event: Event, currentTime: Instant?) {
|
||||
if (currentTime != null && event.isRunningAtTime(currentTime)) {
|
||||
// Contrast colors for running event
|
||||
time.setBackgroundColor(timeRunningBackgroundColor)
|
||||
time.setTextColor(timeRunningForegroundColor)
|
||||
|
|
|
@ -1,7 +1,6 @@
|
|||
package be.digitalia.fosdem.api
|
||||
|
||||
import android.os.SystemClock
|
||||
import android.text.format.DateUtils
|
||||
import androidx.annotation.MainThread
|
||||
import androidx.lifecycle.LiveData
|
||||
import androidx.lifecycle.MutableLiveData
|
||||
|
@ -20,6 +19,7 @@ import be.digitalia.fosdem.parsers.EventsParser
|
|||
import be.digitalia.fosdem.parsers.RoomStatusesParser
|
||||
import be.digitalia.fosdem.utils.BackgroundWorkScope
|
||||
import be.digitalia.fosdem.utils.ByteCountSource
|
||||
import be.digitalia.fosdem.utils.DateUtils
|
||||
import be.digitalia.fosdem.utils.network.HttpClient
|
||||
import kotlinx.coroutines.CancellationException
|
||||
import kotlinx.coroutines.Job
|
||||
|
@ -27,6 +27,8 @@ import kotlinx.coroutines.delay
|
|||
import kotlinx.coroutines.flow.first
|
||||
import kotlinx.coroutines.launch
|
||||
import okio.buffer
|
||||
import java.time.LocalTime
|
||||
import java.util.concurrent.TimeUnit
|
||||
import javax.inject.Inject
|
||||
import javax.inject.Singleton
|
||||
import kotlin.math.pow
|
||||
|
@ -103,9 +105,12 @@ class FosdemApi @Inject constructor(
|
|||
val startEndTimestamps = LongArray(days.size * 2)
|
||||
var index = 0
|
||||
for (day in days) {
|
||||
val dayStart = day.date.time
|
||||
startEndTimestamps[index++] = dayStart + DAY_START_TIME
|
||||
startEndTimestamps[index++] = dayStart + DAY_END_TIME
|
||||
startEndTimestamps[index++] = day.date.atTime(DAY_START_TIME)
|
||||
.atZone(DateUtils.conferenceZoneId)
|
||||
.toEpochSecond() * 1000L
|
||||
startEndTimestamps[index++] = day.date.atTime(DAY_END_TIME)
|
||||
.atZone(DateUtils.conferenceZoneId)
|
||||
.toEpochSecond() * 1000L
|
||||
}
|
||||
scheduler(*startEndTimestamps)
|
||||
}
|
||||
|
@ -170,13 +175,10 @@ class FosdemApi @Inject constructor(
|
|||
}
|
||||
|
||||
companion object {
|
||||
// 8:30 (local time)
|
||||
private const val DAY_START_TIME = 8 * DateUtils.HOUR_IN_MILLIS + 30 * DateUtils.MINUTE_IN_MILLIS
|
||||
|
||||
// 19:00 (local time)
|
||||
private const val DAY_END_TIME = 19 * DateUtils.HOUR_IN_MILLIS
|
||||
private const val ROOM_STATUS_REFRESH_DELAY = 90L * DateUtils.SECOND_IN_MILLIS
|
||||
private const val ROOM_STATUS_FIRST_RETRY_DELAY = 30L * DateUtils.SECOND_IN_MILLIS
|
||||
private const val ROOM_STATUS_EXPIRATION_DELAY = 6L * DateUtils.MINUTE_IN_MILLIS
|
||||
private val DAY_START_TIME = LocalTime.of(8, 30)
|
||||
private val DAY_END_TIME = LocalTime.of(19, 0)
|
||||
private val ROOM_STATUS_REFRESH_DELAY = TimeUnit.SECONDS.toMillis(90L)
|
||||
private val ROOM_STATUS_FIRST_RETRY_DELAY = TimeUnit.SECONDS.toMillis(30L)
|
||||
private val ROOM_STATUS_EXPIRATION_DELAY = TimeUnit.MINUTES.toMillis(6L)
|
||||
}
|
||||
}
|
|
@ -6,19 +6,22 @@ import androidx.room.Dao
|
|||
import androidx.room.Insert
|
||||
import androidx.room.OnConflictStrategy
|
||||
import androidx.room.Query
|
||||
import androidx.room.TypeConverters
|
||||
import androidx.room.withTransaction
|
||||
import be.digitalia.fosdem.db.converters.NonNullInstantTypeConverters
|
||||
import be.digitalia.fosdem.db.entities.Bookmark
|
||||
import be.digitalia.fosdem.model.AlarmInfo
|
||||
import be.digitalia.fosdem.model.Event
|
||||
import be.digitalia.fosdem.utils.BackgroundWorkScope
|
||||
import kotlinx.coroutines.launch
|
||||
import java.time.Instant
|
||||
|
||||
@Dao
|
||||
abstract class BookmarksDao(private val appDatabase: AppDatabase) {
|
||||
/**
|
||||
* Returns the bookmarks.
|
||||
*
|
||||
* @param minStartTime When greater than 0, only return the events starting after this time.
|
||||
* @param minStartTime When greater than Instant.EPOCH, only return the events starting after this time.
|
||||
*/
|
||||
@Query("""SELECT e.id, e.start_time, e.end_time, e.room_name, e.slug, et.title, et.subtitle, e.abstract, e.description,
|
||||
GROUP_CONCAT(p.name, ', ') AS persons, e.day_index, d.date AS day_date, e.track_id, t.name AS track_name, t.type AS track_type
|
||||
|
@ -32,7 +35,8 @@ abstract class BookmarksDao(private val appDatabase: AppDatabase) {
|
|||
WHERE e.start_time > :minStartTime
|
||||
GROUP BY e.id
|
||||
ORDER BY e.start_time ASC""")
|
||||
abstract fun getBookmarks(minStartTime: Long): LiveData<List<Event>>
|
||||
@TypeConverters(NonNullInstantTypeConverters::class)
|
||||
abstract fun getBookmarks(minStartTime: Instant): LiveData<List<Event>>
|
||||
|
||||
@Query("""SELECT e.id, e.start_time, e.end_time, e.room_name, e.slug, et.title, et.subtitle, e.abstract, e.description,
|
||||
GROUP_CONCAT(p.name, ', ') AS persons, e.day_index, d.date AS day_date, e.track_id, t.name AS track_name, t.type AS track_type
|
||||
|
@ -54,7 +58,8 @@ abstract class BookmarksDao(private val appDatabase: AppDatabase) {
|
|||
WHERE e.start_time > :minStartTime
|
||||
ORDER BY e.start_time ASC""")
|
||||
@WorkerThread
|
||||
abstract fun getBookmarksAlarmInfo(minStartTime: Long): Array<AlarmInfo>
|
||||
@TypeConverters(NonNullInstantTypeConverters::class)
|
||||
abstract fun getBookmarksAlarmInfo(minStartTime: Instant): Array<AlarmInfo>
|
||||
|
||||
@Query("SELECT COUNT(*) FROM bookmarks WHERE event_id = :event")
|
||||
abstract fun getBookmarkStatus(event: Event): LiveData<Boolean>
|
||||
|
|
|
@ -12,6 +12,8 @@ import androidx.room.Insert
|
|||
import androidx.room.OnConflictStrategy
|
||||
import androidx.room.Query
|
||||
import androidx.room.Transaction
|
||||
import androidx.room.TypeConverters
|
||||
import be.digitalia.fosdem.db.converters.NonNullInstantTypeConverters
|
||||
import be.digitalia.fosdem.db.entities.EventEntity
|
||||
import be.digitalia.fosdem.db.entities.EventTitles
|
||||
import be.digitalia.fosdem.db.entities.EventToPerson
|
||||
|
@ -24,7 +26,6 @@ import be.digitalia.fosdem.model.Person
|
|||
import be.digitalia.fosdem.model.StatusEvent
|
||||
import be.digitalia.fosdem.model.Track
|
||||
import be.digitalia.fosdem.utils.BackgroundWorkScope
|
||||
import be.digitalia.fosdem.utils.DateUtils
|
||||
import kotlinx.coroutines.async
|
||||
import kotlinx.coroutines.coroutineScope
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
|
@ -33,7 +34,8 @@ import kotlinx.coroutines.flow.first
|
|||
import kotlinx.coroutines.flow.map
|
||||
import kotlinx.coroutines.flow.shareIn
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import java.util.Date
|
||||
import java.time.Instant
|
||||
import java.time.LocalDate
|
||||
import java.util.HashSet
|
||||
|
||||
@Dao
|
||||
|
@ -42,8 +44,8 @@ abstract class ScheduleDao(private val appDatabase: AppDatabase) {
|
|||
/**
|
||||
* @return The latest update time, or null if not available.
|
||||
*/
|
||||
val latestUpdateTime: Flow<Date?> = appDatabase.dataStore.data.map { prefs ->
|
||||
prefs[LATEST_UPDATE_TIME_PREF_KEY]?.let { Date(it) }
|
||||
val latestUpdateTime: Flow<Instant?> = appDatabase.dataStore.data.map { prefs ->
|
||||
prefs[LATEST_UPDATE_TIME_PREF_KEY]?.let { Instant.ofEpochMilli(it) }
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -69,11 +71,11 @@ abstract class ScheduleDao(private val appDatabase: AppDatabase) {
|
|||
0
|
||||
}
|
||||
if (totalEvents > 0) { // Set last update time and server's last modified tag
|
||||
val now = System.currentTimeMillis()
|
||||
val now = Instant.now()
|
||||
runBlocking {
|
||||
appDatabase.dataStore.edit { prefs ->
|
||||
prefs.clear()
|
||||
prefs[LATEST_UPDATE_TIME_PREF_KEY] = now
|
||||
prefs[LATEST_UPDATE_TIME_PREF_KEY] = now.toEpochMilli()
|
||||
if (lastModifiedTag != null) {
|
||||
prefs[LAST_MODIFIED_TAG_PREF] = lastModifiedTag
|
||||
}
|
||||
|
@ -230,16 +232,9 @@ abstract class ScheduleDao(private val appDatabase: AppDatabase) {
|
|||
protected abstract fun getDaysInternal(): Flow<List<Day>>
|
||||
|
||||
suspend fun getYear(): Int {
|
||||
// Compute from days if available
|
||||
val days = days.first()
|
||||
val date = if (days.isNotEmpty()) {
|
||||
days[0].date.time
|
||||
} else {
|
||||
// Use the current year by default
|
||||
System.currentTimeMillis()
|
||||
}
|
||||
|
||||
return DateUtils.getYear(date)
|
||||
// Compute from days if available, fall back to current year
|
||||
val date = days.first().firstOrNull()?.date ?: LocalDate.now()
|
||||
return date.year
|
||||
}
|
||||
|
||||
@Query("""SELECT t.id, t.name, t.type FROM tracks t
|
||||
|
@ -332,7 +327,8 @@ abstract class ScheduleDao(private val appDatabase: AppDatabase) {
|
|||
WHERE e.start_time BETWEEN :minStartTime AND :maxStartTime
|
||||
GROUP BY e.id
|
||||
ORDER BY e.start_time ASC""")
|
||||
abstract fun getEventsWithStartTime(minStartTime: Long, maxStartTime: Long): DataSource.Factory<Int, StatusEvent>
|
||||
@TypeConverters(NonNullInstantTypeConverters::class)
|
||||
abstract fun getEventsWithStartTime(minStartTime: Instant, maxStartTime: Instant): DataSource.Factory<Int, StatusEvent>
|
||||
|
||||
/**
|
||||
* Returns events in progress at the specified time, ordered by descending start time.
|
||||
|
@ -350,7 +346,8 @@ abstract class ScheduleDao(private val appDatabase: AppDatabase) {
|
|||
WHERE e.start_time <= :time AND :time < e.end_time
|
||||
GROUP BY e.id
|
||||
ORDER BY e.start_time DESC""")
|
||||
abstract fun getEventsInProgress(time: Long): DataSource.Factory<Int, StatusEvent>
|
||||
@TypeConverters(NonNullInstantTypeConverters::class)
|
||||
abstract fun getEventsInProgress(time: Instant): DataSource.Factory<Int, StatusEvent>
|
||||
|
||||
/**
|
||||
* Returns the events presented by the specified person.
|
||||
|
|
|
@ -1,14 +0,0 @@
|
|||
package be.digitalia.fosdem.db.converters
|
||||
|
||||
import androidx.room.TypeConverter
|
||||
import java.util.Date
|
||||
|
||||
object NonNullDateTypeConverters {
|
||||
@JvmStatic
|
||||
@TypeConverter
|
||||
fun toDate(value: Long): Date = Date(value)
|
||||
|
||||
@JvmStatic
|
||||
@TypeConverter
|
||||
fun fromDate(value: Date): Long = value.time
|
||||
}
|
|
@ -0,0 +1,14 @@
|
|||
package be.digitalia.fosdem.db.converters
|
||||
|
||||
import androidx.room.TypeConverter
|
||||
import java.time.Instant
|
||||
|
||||
object NonNullInstantTypeConverters {
|
||||
@JvmStatic
|
||||
@TypeConverter
|
||||
fun toInstant(value: Long): Instant = Instant.ofEpochSecond(value)
|
||||
|
||||
@JvmStatic
|
||||
@TypeConverter
|
||||
fun fromInstant(value: Instant): Long = value.epochSecond
|
||||
}
|
|
@ -0,0 +1,14 @@
|
|||
package be.digitalia.fosdem.db.converters
|
||||
|
||||
import androidx.room.TypeConverter
|
||||
import java.time.LocalDate
|
||||
|
||||
object NonNullLocalDateTypeConverters {
|
||||
@JvmStatic
|
||||
@TypeConverter
|
||||
fun toLocalDate(value: Long): LocalDate = LocalDate.ofEpochDay(value)
|
||||
|
||||
@JvmStatic
|
||||
@TypeConverter
|
||||
fun fromLocalDate(value: LocalDate): Long = value.toEpochDay()
|
||||
}
|
|
@ -1,14 +0,0 @@
|
|||
package be.digitalia.fosdem.db.converters
|
||||
|
||||
import androidx.room.TypeConverter
|
||||
import java.util.Date
|
||||
|
||||
object NullableDateTypeConverters {
|
||||
@JvmStatic
|
||||
@TypeConverter
|
||||
fun toDate(value: Long?): Date? = value?.let { Date(it) }
|
||||
|
||||
@JvmStatic
|
||||
@TypeConverter
|
||||
fun fromDate(value: Date?): Long? = value?.time
|
||||
}
|
|
@ -0,0 +1,14 @@
|
|||
package be.digitalia.fosdem.db.converters
|
||||
|
||||
import androidx.room.TypeConverter
|
||||
import java.time.Instant
|
||||
|
||||
object NullableInstantTypeConverters {
|
||||
@JvmStatic
|
||||
@TypeConverter
|
||||
fun toInstant(value: Long?): Instant? = value?.let { Instant.ofEpochSecond(it) }
|
||||
|
||||
@JvmStatic
|
||||
@TypeConverter
|
||||
fun fromInstant(value: Instant?): Long? = value?.epochSecond
|
||||
}
|
|
@ -5,8 +5,8 @@ import androidx.room.Entity
|
|||
import androidx.room.Index
|
||||
import androidx.room.PrimaryKey
|
||||
import androidx.room.TypeConverters
|
||||
import be.digitalia.fosdem.db.converters.NullableDateTypeConverters
|
||||
import java.util.Date
|
||||
import be.digitalia.fosdem.db.converters.NullableInstantTypeConverters
|
||||
import java.time.Instant
|
||||
|
||||
@Entity(tableName = EventEntity.TABLE_NAME, indices = [
|
||||
Index(value = ["day_index"], name = "event_day_index_idx"),
|
||||
|
@ -20,11 +20,11 @@ class EventEntity(
|
|||
@ColumnInfo(name = "day_index")
|
||||
val dayIndex: Int,
|
||||
@ColumnInfo(name = "start_time")
|
||||
@field:TypeConverters(NullableDateTypeConverters::class)
|
||||
val startTime: Date?,
|
||||
@field:TypeConverters(NullableInstantTypeConverters::class)
|
||||
val startTime: Instant?,
|
||||
@ColumnInfo(name = "end_time")
|
||||
@field:TypeConverters(NullableDateTypeConverters::class)
|
||||
val endTime: Date?,
|
||||
@field:TypeConverters(NullableInstantTypeConverters::class)
|
||||
val endTime: Instant?,
|
||||
@ColumnInfo(name = "room_name")
|
||||
val roomName: String?,
|
||||
val slug: String?,
|
||||
|
|
|
@ -99,9 +99,9 @@ class EventDetailsFragment : Fragment(R.layout.fragment_event_details) {
|
|||
}
|
||||
|
||||
view.findViewById<TextView>(R.id.time).apply {
|
||||
val timeDateFormat = DateUtils.getTimeDateFormat(context)
|
||||
val startTime = event.startTime?.let { timeDateFormat.format(it) } ?: "?"
|
||||
val endTime = event.endTime?.let { timeDateFormat.format(it) } ?: "?"
|
||||
val timeFormatter = DateUtils.getTimeFormatter(context)
|
||||
val startTime = event.startTime?.atZone(DateUtils.conferenceZoneId)?.format(timeFormatter) ?: "?"
|
||||
val endTime = event.endTime?.atZone(DateUtils.conferenceZoneId)?.format(timeFormatter) ?: "?"
|
||||
text = "${event.day}, $startTime ― $endTime"
|
||||
contentDescription = getString(R.string.time_content_description, text)
|
||||
}
|
||||
|
@ -226,8 +226,8 @@ class EventDetailsFragment : Fragment(R.layout.fragment_event_details) {
|
|||
description = "$speakersLabel: $personsSummary\n\n$description"
|
||||
}
|
||||
putExtra(CalendarContract.Events.DESCRIPTION, description)
|
||||
event.startTime?.let { putExtra(CalendarContract.EXTRA_EVENT_BEGIN_TIME, it.time) }
|
||||
event.endTime?.let { putExtra(CalendarContract.EXTRA_EVENT_END_TIME, it.time) }
|
||||
event.startTime?.let { putExtra(CalendarContract.EXTRA_EVENT_BEGIN_TIME, it.toEpochMilli()) }
|
||||
event.endTime?.let { putExtra(CalendarContract.EXTRA_EVENT_END_TIME, it.toEpochMilli()) }
|
||||
}
|
||||
|
||||
try {
|
||||
|
|
|
@ -5,14 +5,13 @@ import android.os.SystemClock
|
|||
import androidx.core.os.HandlerCompat
|
||||
import androidx.lifecycle.LiveData
|
||||
import java.util.Arrays
|
||||
import java.util.concurrent.TimeUnit
|
||||
|
||||
object LiveDataFactory {
|
||||
|
||||
private val handler = HandlerCompat.createAsync(Looper.getMainLooper())
|
||||
|
||||
fun interval(period: Long, unit: TimeUnit): LiveData<Long> {
|
||||
return IntervalLiveData(unit.toMillis(period))
|
||||
fun interval(periodInMillis: Long): LiveData<Long> {
|
||||
return IntervalLiveData(periodInMillis)
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -3,17 +3,17 @@ package be.digitalia.fosdem.model
|
|||
import android.os.Parcelable
|
||||
import androidx.room.ColumnInfo
|
||||
import androidx.room.TypeConverters
|
||||
import be.digitalia.fosdem.db.converters.NullableDateTypeConverters
|
||||
import be.digitalia.fosdem.utils.DateParceler
|
||||
import be.digitalia.fosdem.db.converters.NullableInstantTypeConverters
|
||||
import be.digitalia.fosdem.utils.InstantParceler
|
||||
import kotlinx.parcelize.Parcelize
|
||||
import kotlinx.parcelize.WriteWith
|
||||
import java.util.Date
|
||||
import java.time.Instant
|
||||
|
||||
@Parcelize
|
||||
data class AlarmInfo(
|
||||
@ColumnInfo(name = "event_id")
|
||||
val eventId: Long,
|
||||
@ColumnInfo(name = "start_time")
|
||||
@field:TypeConverters(NullableDateTypeConverters::class)
|
||||
val startTime: @WriteWith<DateParceler> Date?
|
||||
@field:TypeConverters(NullableInstantTypeConverters::class)
|
||||
val startTime: @WriteWith<InstantParceler> Instant?
|
||||
) : Parcelable
|
|
@ -4,13 +4,12 @@ import android.os.Parcelable
|
|||
import androidx.room.Entity
|
||||
import androidx.room.PrimaryKey
|
||||
import androidx.room.TypeConverters
|
||||
import be.digitalia.fosdem.db.converters.NonNullDateTypeConverters
|
||||
import be.digitalia.fosdem.utils.DateParceler
|
||||
import be.digitalia.fosdem.utils.DateUtils.withBelgiumTimeZone
|
||||
import be.digitalia.fosdem.db.converters.NonNullLocalDateTypeConverters
|
||||
import be.digitalia.fosdem.utils.LocalDateParceler
|
||||
import kotlinx.parcelize.Parcelize
|
||||
import kotlinx.parcelize.WriteWith
|
||||
import java.text.SimpleDateFormat
|
||||
import java.util.Date
|
||||
import java.time.LocalDate
|
||||
import java.time.format.DateTimeFormatter
|
||||
import java.util.Locale
|
||||
|
||||
@Entity(tableName = Day.TABLE_NAME)
|
||||
|
@ -18,8 +17,8 @@ import java.util.Locale
|
|||
data class Day(
|
||||
@PrimaryKey
|
||||
val index: Int,
|
||||
@field:TypeConverters(NonNullDateTypeConverters::class)
|
||||
val date: @WriteWith<DateParceler> Date
|
||||
@field:TypeConverters(NonNullLocalDateTypeConverters::class)
|
||||
val date: @WriteWith<LocalDateParceler> LocalDate
|
||||
) : Comparable<Day>, Parcelable {
|
||||
|
||||
val name: String
|
||||
|
@ -37,6 +36,6 @@ data class Day(
|
|||
companion object {
|
||||
const val TABLE_NAME = "days"
|
||||
|
||||
private val DAY_DATE_FORMAT = SimpleDateFormat("EEEE", Locale.US).withBelgiumTimeZone()
|
||||
private val DAY_DATE_FORMAT = DateTimeFormatter.ofPattern("EEEE", Locale.US)
|
||||
}
|
||||
}
|
|
@ -5,12 +5,12 @@ import androidx.room.ColumnInfo
|
|||
import androidx.room.Embedded
|
||||
import androidx.room.TypeConverters
|
||||
import be.digitalia.fosdem.api.FosdemUrls
|
||||
import be.digitalia.fosdem.db.converters.NullableDateTypeConverters
|
||||
import be.digitalia.fosdem.utils.DateParceler
|
||||
import be.digitalia.fosdem.utils.DateUtils
|
||||
import be.digitalia.fosdem.db.converters.NullableInstantTypeConverters
|
||||
import be.digitalia.fosdem.utils.InstantParceler
|
||||
import kotlinx.parcelize.Parcelize
|
||||
import kotlinx.parcelize.WriteWith
|
||||
import java.util.Date
|
||||
import java.time.Duration
|
||||
import java.time.Instant
|
||||
|
||||
@Parcelize
|
||||
data class Event(
|
||||
|
@ -18,11 +18,11 @@ data class Event(
|
|||
@Embedded(prefix = "day_")
|
||||
val day: Day,
|
||||
@ColumnInfo(name = "start_time")
|
||||
@field:TypeConverters(NullableDateTypeConverters::class)
|
||||
val startTime: @WriteWith<DateParceler> Date? = null,
|
||||
@field:TypeConverters(NullableInstantTypeConverters::class)
|
||||
val startTime: @WriteWith<InstantParceler> Instant? = null,
|
||||
@ColumnInfo(name = "end_time")
|
||||
@field:TypeConverters(NullableDateTypeConverters::class)
|
||||
val endTime: @WriteWith<DateParceler> Date? = null,
|
||||
@field:TypeConverters(NullableInstantTypeConverters::class)
|
||||
val endTime: @WriteWith<InstantParceler> Instant? = null,
|
||||
@ColumnInfo(name = "room_name")
|
||||
val roomName: String?,
|
||||
val slug: String?,
|
||||
|
@ -38,22 +38,21 @@ data class Event(
|
|||
val personsSummary: String?
|
||||
) : Parcelable {
|
||||
|
||||
fun isRunningAtTime(time: Long): Boolean {
|
||||
return startTime != null && endTime != null && time in startTime.time..endTime.time
|
||||
fun isRunningAtTime(time: Instant): Boolean {
|
||||
return startTime != null && endTime != null && time in startTime..endTime
|
||||
}
|
||||
|
||||
/**
|
||||
* @return The event duration in minutes
|
||||
*/
|
||||
val duration: Int
|
||||
val duration: Duration
|
||||
get() = if (startTime == null || endTime == null) {
|
||||
0
|
||||
} else ((endTime.time - startTime.time) / android.text.format.DateUtils.MINUTE_IN_MILLIS).toInt()
|
||||
Duration.ZERO
|
||||
} else {
|
||||
Duration.between(startTime, endTime)
|
||||
}
|
||||
|
||||
val url: String?
|
||||
get() {
|
||||
val s = slug ?: return null
|
||||
return FosdemUrls.getEvent(s, DateUtils.getYear(day.date.time))
|
||||
return FosdemUrls.getEvent(s, day.date.year)
|
||||
}
|
||||
|
||||
override fun toString(): String = title.orEmpty()
|
||||
|
|
|
@ -20,8 +20,7 @@ data class Person(
|
|||
) : Parcelable {
|
||||
|
||||
fun getUrl(year: Int): String? {
|
||||
val n = name ?: return null
|
||||
return FosdemUrls.getPerson(n.toSlug(), year)
|
||||
return name?.let { FosdemUrls.getPerson(it.toSlug(), year) }
|
||||
}
|
||||
|
||||
override fun toString(): String = name.orEmpty()
|
||||
|
|
|
@ -7,8 +7,7 @@ import be.digitalia.fosdem.model.EventDetails
|
|||
import be.digitalia.fosdem.model.Link
|
||||
import be.digitalia.fosdem.model.Person
|
||||
import be.digitalia.fosdem.model.Track
|
||||
import be.digitalia.fosdem.utils.DateUtils.belgiumTimeZone
|
||||
import be.digitalia.fosdem.utils.DateUtils.withBelgiumTimeZone
|
||||
import be.digitalia.fosdem.utils.DateUtils
|
||||
import be.digitalia.fosdem.utils.isEndDocument
|
||||
import be.digitalia.fosdem.utils.isNextEndTag
|
||||
import be.digitalia.fosdem.utils.isStartTag
|
||||
|
@ -16,10 +15,9 @@ import be.digitalia.fosdem.utils.skipToEndTag
|
|||
import be.digitalia.fosdem.utils.xmlPullParserFactory
|
||||
import okio.BufferedSource
|
||||
import org.xmlpull.v1.XmlPullParser
|
||||
import java.text.SimpleDateFormat
|
||||
import java.util.Calendar
|
||||
import java.util.Date
|
||||
import java.util.Locale
|
||||
import java.time.Duration
|
||||
import java.time.Instant
|
||||
import java.time.LocalDate
|
||||
|
||||
/**
|
||||
* Main parser for FOSDEM schedule data in pentabarf XML format.
|
||||
|
@ -28,10 +26,6 @@ import java.util.Locale
|
|||
*/
|
||||
class EventsParser : Parser<Sequence<DetailedEvent>> {
|
||||
|
||||
private val dateFormat = SimpleDateFormat("yyyy-MM-dd", Locale.US).withBelgiumTimeZone()
|
||||
// Calendar used to compute the events time, according to Belgium timezone
|
||||
private val calendar = Calendar.getInstance(belgiumTimeZone, Locale.US)
|
||||
|
||||
override fun parse(source: BufferedSource): Sequence<DetailedEvent> {
|
||||
val parser: XmlPullParser = xmlPullParserFactory.newPullParser().apply {
|
||||
setInput(source.inputStream(), null)
|
||||
|
@ -48,7 +42,7 @@ class EventsParser : Parser<Sequence<DetailedEvent>> {
|
|||
"day" -> {
|
||||
currentDay = Day(
|
||||
index = parser.getAttributeValue(null, "index")!!.toInt(),
|
||||
date = dateFormat.parse(parser.getAttributeValue(null, "date"))!!
|
||||
date = LocalDate.parse(parser.getAttributeValue(null, "date"))
|
||||
)
|
||||
}
|
||||
"room" -> currentRoomName = parser.getAttributeValue(null, "name")
|
||||
|
@ -65,7 +59,7 @@ class EventsParser : Parser<Sequence<DetailedEvent>> {
|
|||
|
||||
private fun parseEvent(parser: XmlPullParser, day: Day, roomName: String?): DetailedEvent {
|
||||
val id = parser.getAttributeValue(null, "id")!!.toLong()
|
||||
var startTime: Date? = null
|
||||
var startTime: Instant? = null
|
||||
var duration: String? = null
|
||||
var slug: String? = null
|
||||
var title: String? = null
|
||||
|
@ -83,12 +77,10 @@ class EventsParser : Parser<Sequence<DetailedEvent>> {
|
|||
"start" -> {
|
||||
val timeString = parser.nextText()
|
||||
if (!timeString.isNullOrEmpty()) {
|
||||
startTime = with(calendar) {
|
||||
time = day.date
|
||||
set(Calendar.HOUR_OF_DAY, getHours(timeString))
|
||||
set(Calendar.MINUTE, getMinutes(timeString))
|
||||
time
|
||||
}
|
||||
startTime = day.date
|
||||
.atTime(getHours(timeString), getMinutes(timeString))
|
||||
.atZone(DateUtils.conferenceZoneId)
|
||||
.toInstant()
|
||||
}
|
||||
}
|
||||
"duration" -> duration = parser.nextText()
|
||||
|
@ -128,11 +120,7 @@ class EventsParser : Parser<Sequence<DetailedEvent>> {
|
|||
}
|
||||
|
||||
val endTime = if (startTime != null && !duration.isNullOrEmpty()) {
|
||||
with(calendar) {
|
||||
add(Calendar.HOUR_OF_DAY, getHours(duration))
|
||||
add(Calendar.MINUTE, getMinutes(duration))
|
||||
time
|
||||
}
|
||||
startTime + Duration.ofMinutes(getHours(duration) * 60L + getMinutes(duration))
|
||||
} else null
|
||||
|
||||
val event = Event(
|
||||
|
|
|
@ -18,7 +18,6 @@ import be.digitalia.fosdem.db.BookmarksDao
|
|||
import be.digitalia.fosdem.db.ScheduleDao
|
||||
import be.digitalia.fosdem.ical.ICalendarWriter
|
||||
import be.digitalia.fosdem.model.Event
|
||||
import be.digitalia.fosdem.utils.DateUtils
|
||||
import be.digitalia.fosdem.utils.stripHtml
|
||||
import be.digitalia.fosdem.utils.toSlug
|
||||
import dagger.hilt.EntryPoint
|
||||
|
@ -31,10 +30,10 @@ import okio.sink
|
|||
import java.io.FileNotFoundException
|
||||
import java.io.IOException
|
||||
import java.io.OutputStream
|
||||
import java.text.SimpleDateFormat
|
||||
import java.util.Calendar
|
||||
import java.time.LocalDateTime
|
||||
import java.time.ZoneOffset
|
||||
import java.time.format.DateTimeFormatter
|
||||
import java.util.Locale
|
||||
import java.util.TimeZone
|
||||
|
||||
/**
|
||||
* Content Provider generating the current bookmarks list in iCalendar format.
|
||||
|
@ -101,12 +100,7 @@ class BookmarksExportProvider : ContentProvider() {
|
|||
}
|
||||
|
||||
private class DownloadThread(private val outputStream: OutputStream, private val bookmarksDao: BookmarksDao) : Thread() {
|
||||
private val calendar = Calendar.getInstance(DateUtils.belgiumTimeZone, Locale.US)
|
||||
// Format all times in GMT
|
||||
private val dateFormat = SimpleDateFormat("yyyyMMdd'T'HHmmss'Z'", Locale.US).apply {
|
||||
timeZone = TimeZone.getTimeZone("GMT+0")
|
||||
}
|
||||
private val dtStamp = dateFormat.format(System.currentTimeMillis())
|
||||
private val dtStamp = LocalDateTime.now(ZoneOffset.UTC).format(DATE_TIME_FORMAT)
|
||||
|
||||
override fun run() {
|
||||
try {
|
||||
|
@ -130,11 +124,11 @@ class BookmarksExportProvider : ContentProvider() {
|
|||
private fun writeEvent(writer: ICalendarWriter, event: Event) = with(writer) {
|
||||
write("BEGIN", "VEVENT")
|
||||
|
||||
val year = DateUtils.getYear(event.day.date.time, calendar)
|
||||
val year = event.day.date.year
|
||||
write("UID", "${event.id}@$year@${BuildConfig.APPLICATION_ID}")
|
||||
write("DTSTAMP", dtStamp)
|
||||
event.startTime?.let { write("DTSTART", dateFormat.format(it)) }
|
||||
event.endTime?.let { write("DTEND", dateFormat.format(it)) }
|
||||
event.startTime?.let { write("DTSTART", it.atOffset(ZoneOffset.UTC).format(DATE_TIME_FORMAT)) }
|
||||
event.endTime?.let { write("DTEND", it.atOffset(ZoneOffset.UTC).format(DATE_TIME_FORMAT)) }
|
||||
write("SUMMARY", event.title)
|
||||
var description = event.abstractText
|
||||
if (description.isNullOrEmpty()) {
|
||||
|
@ -176,6 +170,7 @@ class BookmarksExportProvider : ContentProvider() {
|
|||
.appendPath("bookmarks.ics")
|
||||
.build()
|
||||
private val COLUMNS = arrayOf(OpenableColumns.DISPLAY_NAME, OpenableColumns.SIZE)
|
||||
private val DATE_TIME_FORMAT = DateTimeFormatter.ofPattern("yyyyMMdd'T'HHmmss'Z'", Locale.US)
|
||||
|
||||
fun getIntent(activity: Activity): Intent {
|
||||
// Supports granting read permission for the attached shared file
|
||||
|
|
|
@ -40,6 +40,7 @@ import be.digitalia.fosdem.utils.roomNameToResourceName
|
|||
import dagger.hilt.android.AndroidEntryPoint
|
||||
import kotlinx.coroutines.flow.first
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import java.time.Instant
|
||||
import javax.inject.Inject
|
||||
|
||||
/**
|
||||
|
@ -75,9 +76,9 @@ class AlarmIntentService : JobIntentService() {
|
|||
val delay = runBlocking { userSettingsProvider.notificationsDelayInMillis.first() }
|
||||
val now = System.currentTimeMillis()
|
||||
var hasAlarms = false
|
||||
for (info in bookmarksDao.getBookmarksAlarmInfo(0L)) {
|
||||
for (info in bookmarksDao.getBookmarksAlarmInfo(Instant.EPOCH)) {
|
||||
val startTime = info.startTime
|
||||
val notificationTime = if (startTime == null) -1L else startTime.time - delay
|
||||
val notificationTime = if (startTime == null) -1L else startTime.toEpochMilli() - delay
|
||||
val pi = getAlarmPendingIntent(info.eventId)
|
||||
if (notificationTime < now) {
|
||||
// Cancel pending alarms that are now scheduled in the past, if any
|
||||
|
@ -94,7 +95,7 @@ class AlarmIntentService : JobIntentService() {
|
|||
}
|
||||
ACTION_DISABLE_ALARMS -> {
|
||||
// Cancel alarms of every bookmark in the future
|
||||
for (info in bookmarksDao.getBookmarksAlarmInfo(System.currentTimeMillis())) {
|
||||
for (info in bookmarksDao.getBookmarksAlarmInfo(Instant.now())) {
|
||||
alarmManager.cancel(getAlarmPendingIntent(info.eventId))
|
||||
}
|
||||
setAlarmReceiverEnabled(false)
|
||||
|
@ -107,7 +108,7 @@ class AlarmIntentService : JobIntentService() {
|
|||
var isFirstAlarm = true
|
||||
for ((eventId, startTime) in alarmInfos) {
|
||||
// Only schedule future events. If they start before the delay, the alarm will go off immediately
|
||||
if (startTime != null && startTime.time >= now) {
|
||||
if (startTime != null && startTime.toEpochMilli() >= now) {
|
||||
if (isFirstAlarm) {
|
||||
setAlarmReceiverEnabled(true)
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
|
@ -115,7 +116,10 @@ class AlarmIntentService : JobIntentService() {
|
|||
}
|
||||
isFirstAlarm = false
|
||||
}
|
||||
AlarmManagerCompat.setExactAndAllowWhileIdle(alarmManager, AlarmManager.RTC_WAKEUP, startTime.time - delay, getAlarmPendingIntent(eventId))
|
||||
AlarmManagerCompat.setExactAndAllowWhileIdle(
|
||||
alarmManager, AlarmManager.RTC_WAKEUP,
|
||||
startTime.toEpochMilli() - delay, getAlarmPendingIntent(eventId)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -187,7 +191,7 @@ class AlarmIntentService : JobIntentService() {
|
|||
val notificationBuilder = NotificationCompat.Builder(this, NOTIFICATION_CHANNEL)
|
||||
.setSmallIcon(R.drawable.ic_stat_fosdem)
|
||||
.setColor(notificationColor)
|
||||
.setWhen(event.startTime?.time ?: System.currentTimeMillis())
|
||||
.setWhen(event.startTime?.toEpochMilli() ?: System.currentTimeMillis())
|
||||
.setContentTitle(event.title)
|
||||
.setContentText(contentText)
|
||||
.setStyle(NotificationCompat.BigTextStyle().bigText(bigText).setSummaryText(trackName))
|
||||
|
|
|
@ -1,12 +1,12 @@
|
|||
package be.digitalia.fosdem.settings
|
||||
|
||||
import android.content.Context
|
||||
import android.text.format.DateUtils
|
||||
import androidx.preference.PreferenceManager
|
||||
import be.digitalia.fosdem.R
|
||||
import dagger.hilt.android.qualifiers.ApplicationContext
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.map
|
||||
import java.util.concurrent.TimeUnit
|
||||
import javax.inject.Inject
|
||||
import javax.inject.Singleton
|
||||
|
||||
|
@ -35,6 +35,6 @@ class UserSettingsProvider @Inject constructor(@ApplicationContext context: Cont
|
|||
get() = sharedPreferences.getStringAsFlow(PreferenceKeys.NOTIFICATIONS_DELAY)
|
||||
.map {
|
||||
// Convert from minutes to milliseconds
|
||||
(it?.toLong() ?: 0L) * DateUtils.MINUTE_IN_MILLIS
|
||||
TimeUnit.MINUTES.toMillis(it?.toLong() ?: 0L)
|
||||
}
|
||||
}
|
|
@ -1,25 +1,19 @@
|
|||
package be.digitalia.fosdem.utils
|
||||
|
||||
import android.content.Context
|
||||
import java.text.DateFormat
|
||||
import java.util.Calendar
|
||||
import java.util.Locale
|
||||
import java.util.TimeZone
|
||||
import android.text.format.DateFormat
|
||||
import androidx.core.os.ConfigurationCompat
|
||||
import java.time.ZoneId
|
||||
import java.time.ZoneOffset
|
||||
import java.time.format.DateTimeFormatter
|
||||
|
||||
object DateUtils {
|
||||
val belgiumTimeZone: TimeZone = TimeZone.getTimeZone("GMT+1")
|
||||
val conferenceZoneId: ZoneId = ZoneOffset.ofHours(1)
|
||||
|
||||
fun DateFormat.withBelgiumTimeZone(): DateFormat {
|
||||
timeZone = belgiumTimeZone
|
||||
return this
|
||||
}
|
||||
|
||||
fun getTimeDateFormat(context: Context): DateFormat {
|
||||
return android.text.format.DateFormat.getTimeFormat(context).withBelgiumTimeZone()
|
||||
}
|
||||
|
||||
fun getYear(timestamp: Long, calendar: Calendar = Calendar.getInstance(belgiumTimeZone, Locale.US)): Int {
|
||||
calendar.timeInMillis = timestamp
|
||||
return calendar.get(Calendar.YEAR)
|
||||
fun getTimeFormatter(context: Context): DateTimeFormatter {
|
||||
val primaryLocale = ConfigurationCompat.getLocales(context.resources.configuration)[0]
|
||||
val basePattern = if (DateFormat.is24HourFormat(context)) "Hm" else "hm"
|
||||
val bestPattern = DateFormat.getBestDateTimePattern(primaryLocale, basePattern)
|
||||
return DateTimeFormatter.ofPattern(bestPattern, primaryLocale)
|
||||
}
|
||||
}
|
|
@ -2,14 +2,41 @@ package be.digitalia.fosdem.utils
|
|||
|
||||
import android.os.Parcel
|
||||
import kotlinx.parcelize.Parceler
|
||||
import java.util.Date
|
||||
import java.time.Instant
|
||||
import java.time.LocalDate
|
||||
|
||||
object DateParceler : Parceler<Date?> {
|
||||
object InstantParceler : Parceler<Instant?> {
|
||||
|
||||
override fun create(parcel: Parcel): Date? {
|
||||
val value = parcel.readLong()
|
||||
return if (value == -1L) null else Date(value)
|
||||
override fun create(parcel: Parcel): Instant? {
|
||||
val nanoAdjustment = parcel.readInt()
|
||||
if (nanoAdjustment == Int.MIN_VALUE) {
|
||||
return null
|
||||
}
|
||||
val epochSecond = parcel.readLong()
|
||||
return Instant.ofEpochSecond(epochSecond, nanoAdjustment.toLong())
|
||||
}
|
||||
|
||||
override fun Date?.write(parcel: Parcel, flags: Int) = parcel.writeLong(this?.time ?: -1L)
|
||||
override fun Instant?.write(parcel: Parcel, flags: Int) {
|
||||
if (this == null) {
|
||||
parcel.writeInt(Int.MIN_VALUE)
|
||||
} else {
|
||||
parcel.writeInt(nano)
|
||||
parcel.writeLong(epochSecond)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
object LocalDateParceler : Parceler<LocalDate> {
|
||||
|
||||
override fun create(parcel: Parcel): LocalDate {
|
||||
val year = parcel.readInt()
|
||||
val monthDay = parcel.readInt()
|
||||
return LocalDate.of(year, monthDay shr 16, monthDay and 0xFFFF)
|
||||
}
|
||||
|
||||
override fun LocalDate.write(parcel: Parcel, flags: Int) {
|
||||
parcel.writeInt(year)
|
||||
// pack month and day into a single int
|
||||
parcel.writeInt((monthValue shl 16) or dayOfMonth)
|
||||
}
|
||||
}
|
|
@ -2,7 +2,6 @@ package be.digitalia.fosdem.viewmodels
|
|||
|
||||
import android.app.Application
|
||||
import android.net.Uri
|
||||
import android.text.format.DateUtils
|
||||
import androidx.lifecycle.LiveData
|
||||
import androidx.lifecycle.MutableLiveData
|
||||
import androidx.lifecycle.ViewModel
|
||||
|
@ -18,6 +17,8 @@ import kotlinx.coroutines.Dispatchers
|
|||
import kotlinx.coroutines.withContext
|
||||
import okio.buffer
|
||||
import okio.source
|
||||
import java.time.Duration
|
||||
import java.time.Instant
|
||||
import java.util.concurrent.TimeUnit
|
||||
import javax.inject.Inject
|
||||
|
||||
|
@ -33,12 +34,12 @@ class BookmarksViewModel @Inject constructor(
|
|||
val bookmarks: LiveData<List<Event>> = upcomingOnlyLiveData.switchMap { upcomingOnly: Boolean ->
|
||||
if (upcomingOnly) {
|
||||
// Refresh upcoming bookmarks every 2 minutes
|
||||
LiveDataFactory.interval(2L, TimeUnit.MINUTES)
|
||||
LiveDataFactory.interval(REFRESH_PERIOD)
|
||||
.switchMap {
|
||||
bookmarksDao.getBookmarks(System.currentTimeMillis() - TIME_OFFSET)
|
||||
bookmarksDao.getBookmarks(Instant.now() - TIME_OFFSET)
|
||||
}
|
||||
} else {
|
||||
bookmarksDao.getBookmarks(-1L)
|
||||
bookmarksDao.getBookmarks(Instant.EPOCH)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -63,7 +64,8 @@ class BookmarksViewModel @Inject constructor(
|
|||
}
|
||||
|
||||
companion object {
|
||||
private val REFRESH_PERIOD = TimeUnit.MINUTES.toMillis(2L)
|
||||
// In upcomingOnly mode, events that just started are still shown for 5 minutes
|
||||
private const val TIME_OFFSET = 5L * DateUtils.MINUTE_IN_MILLIS
|
||||
private val TIME_OFFSET = Duration.ofMinutes(5L)
|
||||
}
|
||||
}
|
|
@ -1,6 +1,5 @@
|
|||
package be.digitalia.fosdem.viewmodels
|
||||
|
||||
import android.text.format.DateUtils
|
||||
import androidx.lifecycle.LiveData
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.switchMap
|
||||
|
@ -10,24 +9,27 @@ import be.digitalia.fosdem.db.ScheduleDao
|
|||
import be.digitalia.fosdem.livedata.LiveDataFactory
|
||||
import be.digitalia.fosdem.model.StatusEvent
|
||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||
import java.time.Duration
|
||||
import java.time.Instant
|
||||
import java.util.concurrent.TimeUnit
|
||||
import javax.inject.Inject
|
||||
|
||||
@HiltViewModel
|
||||
class LiveViewModel @Inject constructor(scheduleDao: ScheduleDao) : ViewModel() {
|
||||
|
||||
private val heartbeat = LiveDataFactory.interval(1L, TimeUnit.MINUTES)
|
||||
private val heartbeat = LiveDataFactory.interval(REFRESH_PERIOD)
|
||||
|
||||
val nextEvents: LiveData<PagedList<StatusEvent>> = heartbeat.switchMap {
|
||||
val now = System.currentTimeMillis()
|
||||
val now = Instant.now()
|
||||
scheduleDao.getEventsWithStartTime(now, now + NEXT_EVENTS_INTERVAL).toLiveData(20)
|
||||
}
|
||||
|
||||
val eventsInProgress: LiveData<PagedList<StatusEvent>> = heartbeat.switchMap {
|
||||
scheduleDao.getEventsInProgress(System.currentTimeMillis()).toLiveData(20)
|
||||
scheduleDao.getEventsInProgress(Instant.now()).toLiveData(20)
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val NEXT_EVENTS_INTERVAL = 30L * DateUtils.MINUTE_IN_MILLIS
|
||||
private val REFRESH_PERIOD = TimeUnit.MINUTES.toMillis(1L)
|
||||
private val NEXT_EVENTS_INTERVAL = Duration.ofMinutes(30L)
|
||||
}
|
||||
}
|
|
@ -1,6 +1,5 @@
|
|||
package be.digitalia.fosdem.viewmodels
|
||||
|
||||
import android.text.format.DateUtils
|
||||
import androidx.lifecycle.LiveData
|
||||
import androidx.lifecycle.MutableLiveData
|
||||
import androidx.lifecycle.ViewModel
|
||||
|
@ -11,7 +10,10 @@ import be.digitalia.fosdem.livedata.LiveDataFactory
|
|||
import be.digitalia.fosdem.model.Day
|
||||
import be.digitalia.fosdem.model.StatusEvent
|
||||
import be.digitalia.fosdem.model.Track
|
||||
import be.digitalia.fosdem.utils.DateUtils
|
||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||
import java.time.Duration
|
||||
import java.time.Instant
|
||||
import java.util.concurrent.TimeUnit
|
||||
import javax.inject.Inject
|
||||
|
||||
|
@ -25,21 +27,22 @@ class TrackScheduleListViewModel @Inject constructor(scheduleDao: ScheduleDao) :
|
|||
}
|
||||
|
||||
/**
|
||||
* @return The current time during the target day, or -1 outside of the target day.
|
||||
* @return The current time during the target day, or null outside of the target day.
|
||||
*/
|
||||
val currentTime: LiveData<Long> = dayTrackLiveData
|
||||
val currentTime: LiveData<Instant?> = dayTrackLiveData
|
||||
.switchMap { (day, _) ->
|
||||
// Auto refresh during the day passed as argument
|
||||
val dayStart = day.date.time
|
||||
LiveDataFactory.scheduler(dayStart, dayStart + DateUtils.DAY_IN_MILLIS)
|
||||
val dayStart = day.date.atStartOfDay(DateUtils.conferenceZoneId).toInstant()
|
||||
LiveDataFactory.scheduler(
|
||||
dayStart.toEpochMilli(),
|
||||
(dayStart + Duration.ofDays(1L)).toEpochMilli()
|
||||
)
|
||||
}
|
||||
.switchMap { isOn ->
|
||||
if (isOn) {
|
||||
LiveDataFactory.interval(REFRESH_TIME_INTERVAL, TimeUnit.MILLISECONDS).map {
|
||||
System.currentTimeMillis()
|
||||
}
|
||||
LiveDataFactory.interval(TIME_REFRESH_PERIOD).map { Instant.now() }
|
||||
} else {
|
||||
MutableLiveData(-1L)
|
||||
MutableLiveData(null)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -51,6 +54,6 @@ class TrackScheduleListViewModel @Inject constructor(scheduleDao: ScheduleDao) :
|
|||
}
|
||||
|
||||
companion object {
|
||||
private const val REFRESH_TIME_INTERVAL = DateUtils.MINUTE_IN_MILLIS
|
||||
private val TIME_REFRESH_PERIOD = TimeUnit.MINUTES.toMillis(1L)
|
||||
}
|
||||
}
|
Loading…
Reference in a new issue