1
0
Fork 0
mirror of https://github.com/MatomoCamp/matomocamp-companion-android.git synced 2024-09-19 16:13:46 +02:00

Import bookmarks feature (#65)

- move ICalendarWriter to ical package
- create ICalendarReader and share CRLF constant with ICalendarWriter
- add import icon
- implement bookmarks parser and add file picker to send parsed bookmark ids to ExternalBookmarksActivity
- add feature to import all bookmarks shown in the external bookmarks list at once, with confirmation dialog.
This commit is contained in:
Christophe Beyls 2021-02-06 01:33:41 +01:00 committed by GitHub
parent d7e9dcf63b
commit 258598e730
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
18 changed files with 389 additions and 49 deletions

View file

@ -18,9 +18,11 @@ class ExternalBookmarksActivity : SimpleToolbarActivity() {
if (savedInstanceState == null) { if (savedInstanceState == null) {
val intent = intent val intent = intent
val bookmarkIds = if (intent.hasNfcAppData()) { val bookmarkIds = when {
intent.extractNfcAppData().toBookmarks() intent.hasExtra(EXTRA_BOOKMARK_IDS) -> intent.getLongArrayExtra(EXTRA_BOOKMARK_IDS)
} else null intent.hasNfcAppData() -> intent.extractNfcAppData().toBookmarks()
else -> null
}
if (bookmarkIds == null) { if (bookmarkIds == null) {
// Invalid data format, exit // Invalid data format, exit
finish() finish()
@ -33,4 +35,8 @@ class ExternalBookmarksActivity : SimpleToolbarActivity() {
} }
} }
} }
companion object {
const val EXTRA_BOOKMARK_IDS = "bookmark_ids"
}
} }

View file

@ -1,11 +1,12 @@
package be.digitalia.fosdem.alarms package be.digitalia.fosdem.alarms
import android.annotation.SuppressLint
import android.content.Context import android.content.Context
import android.content.Intent import android.content.Intent
import android.content.SharedPreferences.OnSharedPreferenceChangeListener import android.content.SharedPreferences.OnSharedPreferenceChangeListener
import androidx.annotation.MainThread import androidx.annotation.MainThread
import androidx.preference.PreferenceManager import androidx.preference.PreferenceManager
import be.digitalia.fosdem.model.Event import be.digitalia.fosdem.model.AlarmInfo
import be.digitalia.fosdem.services.AlarmIntentService import be.digitalia.fosdem.services.AlarmIntentService
import be.digitalia.fosdem.utils.PreferenceKeys import be.digitalia.fosdem.utils.PreferenceKeys
@ -14,6 +15,7 @@ import be.digitalia.fosdem.utils.PreferenceKeys
* *
* @author Christophe Beyls * @author Christophe Beyls
*/ */
@SuppressLint("StaticFieldLeak")
object FosdemAlarmManager { object FosdemAlarmManager {
private lateinit var context: Context private lateinit var context: Context
@ -51,20 +53,17 @@ object FosdemAlarmManager {
} }
@MainThread @MainThread
fun onBookmarkAdded(event: Event) { fun onBookmarksAdded(alarmInfos: List<AlarmInfo>) {
if (isEnabled) { if (isEnabled) {
val serviceIntent = Intent(AlarmIntentService.ACTION_ADD_BOOKMARK).apply { val arrayList = if (alarmInfos is ArrayList<AlarmInfo>) alarmInfos else ArrayList(alarmInfos)
putExtra(AlarmIntentService.EXTRA_EVENT_ID, event.id) val serviceIntent = Intent(AlarmIntentService.ACTION_ADD_BOOKMARKS)
event.startTime?.let { .putParcelableArrayListExtra(AlarmIntentService.EXTRA_ALARM_INFOS, arrayList)
putExtra(AlarmIntentService.EXTRA_EVENT_START_TIME, it.time)
}
}
AlarmIntentService.enqueueWork(context, serviceIntent) AlarmIntentService.enqueueWork(context, serviceIntent)
} }
} }
@MainThread @MainThread
fun onBookmarksRemoved(eventIds: LongArray?) { fun onBookmarksRemoved(eventIds: LongArray) {
if (isEnabled) { if (isEnabled) {
val serviceIntent = Intent(AlarmIntentService.ACTION_REMOVE_BOOKMARKS) val serviceIntent = Intent(AlarmIntentService.ACTION_REMOVE_BOOKMARKS)
.putExtra(AlarmIntentService.EXTRA_EVENT_IDS, eventIds) .putExtra(AlarmIntentService.EXTRA_EVENT_IDS, eventIds)

View file

@ -6,6 +6,7 @@ import androidx.room.Dao
import androidx.room.Insert import androidx.room.Insert
import androidx.room.OnConflictStrategy import androidx.room.OnConflictStrategy
import androidx.room.Query import androidx.room.Query
import androidx.room.withTransaction
import be.digitalia.fosdem.alarms.FosdemAlarmManager import be.digitalia.fosdem.alarms.FosdemAlarmManager
import be.digitalia.fosdem.db.entities.Bookmark import be.digitalia.fosdem.db.entities.Bookmark
import be.digitalia.fosdem.model.AlarmInfo import be.digitalia.fosdem.model.AlarmInfo
@ -14,7 +15,7 @@ import be.digitalia.fosdem.utils.BackgroundWorkScope
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
@Dao @Dao
abstract class BookmarksDao { abstract class BookmarksDao(private val appDatabase: AppDatabase) {
/** /**
* Returns the bookmarks. * Returns the bookmarks.
@ -62,20 +63,44 @@ abstract class BookmarksDao {
fun addBookmarkAsync(event: Event) { fun addBookmarkAsync(event: Event) {
BackgroundWorkScope.launch { BackgroundWorkScope.launch {
if (addBookmarkInternal(Bookmark(event.id)) != -1L) { val ids = addBookmarksInternal(listOf(Bookmark(event.id)))
FosdemAlarmManager.onBookmarkAdded(event) if (ids[0] != -1L) {
FosdemAlarmManager.onBookmarksAdded(listOf(AlarmInfo(eventId = event.id, startTime = event.startTime)))
} }
} }
} }
@Insert(onConflict = OnConflictStrategy.IGNORE) fun addBookmarksAsync(eventIds: LongArray) {
protected abstract suspend fun addBookmarkInternal(bookmark: Bookmark): Long BackgroundWorkScope.launch {
appDatabase.withTransaction {
// Get AlarmInfos first to filter out non-existing items
val alarmInfos = getAlarmInfos(eventIds)
alarmInfos.isNotEmpty() || return@withTransaction
fun removeBookmarkAsync(event: Event) { val ids = addBookmarksInternal(alarmInfos.map { Bookmark(it.eventId) })
removeBookmarksAsync(event.id) // Filter out items that were already in bookmarks
val addedAlarmInfos = alarmInfos.filterIndexed { index, _ -> ids[index] != -1L }
if (addedAlarmInfos.isNotEmpty()) {
FosdemAlarmManager.onBookmarksAdded(addedAlarmInfos)
}
}
}
} }
fun removeBookmarksAsync(vararg eventIds: Long) { @Query("""SELECT id as event_id, start_time
FROM events
WHERE id IN (:ids)
ORDER BY start_time ASC""")
protected abstract suspend fun getAlarmInfos(ids: LongArray): List<AlarmInfo>
@Insert(onConflict = OnConflictStrategy.IGNORE)
protected abstract suspend fun addBookmarksInternal(bookmarks: List<Bookmark>): LongArray
fun removeBookmarkAsync(event: Event) {
removeBookmarksAsync(longArrayOf(event.id))
}
fun removeBookmarksAsync(eventIds: LongArray) {
BackgroundWorkScope.launch { BackgroundWorkScope.launch {
if (removeBookmarksInternal(eventIds) > 0) { if (removeBookmarksInternal(eventIds) > 0) {
FosdemAlarmManager.onBookmarksRemoved(eventIds) FosdemAlarmManager.onBookmarksRemoved(eventIds)

View file

@ -1,7 +1,10 @@
package be.digitalia.fosdem.fragments package be.digitalia.fosdem.fragments
import android.app.Activity
import android.app.Dialog
import android.content.Context import android.content.Context
import android.content.Intent import android.content.Intent
import android.net.Uri
import android.nfc.NdefRecord import android.nfc.NdefRecord
import android.os.Bundle import android.os.Bundle
import android.view.Menu import android.view.Menu
@ -11,17 +14,22 @@ import android.view.View
import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.app.AppCompatActivity
import androidx.appcompat.view.ActionMode import androidx.appcompat.view.ActionMode
import androidx.core.content.edit import androidx.core.content.edit
import androidx.fragment.app.DialogFragment
import androidx.fragment.app.Fragment import androidx.fragment.app.Fragment
import androidx.fragment.app.viewModels import androidx.fragment.app.viewModels
import androidx.lifecycle.lifecycleScope
import androidx.recyclerview.widget.DividerItemDecoration import androidx.recyclerview.widget.DividerItemDecoration
import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.LinearLayoutManager
import be.digitalia.fosdem.R import be.digitalia.fosdem.R
import be.digitalia.fosdem.activities.ExternalBookmarksActivity
import be.digitalia.fosdem.adapters.BookmarksAdapter import be.digitalia.fosdem.adapters.BookmarksAdapter
import be.digitalia.fosdem.providers.BookmarksExportProvider import be.digitalia.fosdem.providers.BookmarksExportProvider
import be.digitalia.fosdem.utils.CreateNfcAppDataCallback import be.digitalia.fosdem.utils.CreateNfcAppDataCallback
import be.digitalia.fosdem.utils.toBookmarksNfcAppData import be.digitalia.fosdem.utils.toBookmarksNfcAppData
import be.digitalia.fosdem.viewmodels.BookmarksViewModel import be.digitalia.fosdem.viewmodels.BookmarksViewModel
import be.digitalia.fosdem.widgets.MultiChoiceHelper import be.digitalia.fosdem.widgets.MultiChoiceHelper
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import java.util.concurrent.CancellationException
/** /**
* Bookmarks list, optionally filterable. * Bookmarks list, optionally filterable.
@ -133,9 +141,52 @@ class BookmarksListFragment : Fragment(R.layout.recyclerview), CreateNfcAppDataC
startActivity(Intent.createChooser(exportIntent, getString(R.string.export_bookmarks))) startActivity(Intent.createChooser(exportIntent, getString(R.string.export_bookmarks)))
true true
} }
R.id.import_bookmarks -> {
val importIntent = Intent(Intent.ACTION_GET_CONTENT)
.setType(BookmarksExportProvider.TYPE)
try {
startActivityForResult(importIntent, IMPORT_REQUEST_CODE)
} catch (ignore: Exception) {
}
true
}
else -> false else -> false
} }
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
if (requestCode == IMPORT_REQUEST_CODE && resultCode == Activity.RESULT_OK) {
data?.data?.let {
importBookmarks(it)
}
}
}
private fun importBookmarks(uri: Uri) {
lifecycleScope.launchWhenStarted {
try {
val bookmarkIds = viewModel.readBookmarkIds(uri)
val intent = Intent(requireContext(), ExternalBookmarksActivity::class.java)
.putExtra(ExternalBookmarksActivity.EXTRA_BOOKMARK_IDS, bookmarkIds)
startActivity(intent)
} catch (e: Exception) {
if (e is CancellationException) {
throw e
}
ImportBookmarksErrorDialogFragment().show(parentFragmentManager, "importBookmarksError")
}
}
}
class ImportBookmarksErrorDialogFragment : DialogFragment() {
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
return MaterialAlertDialogBuilder(requireContext())
.setTitle(R.string.import_bookmarks)
.setMessage(R.string.import_bookmarks_error)
.setPositiveButton(android.R.string.ok, null)
.create()
}
}
override fun createNfcAppData(): NdefRecord? { override fun createNfcAppData(): NdefRecord? {
val context = context ?: return null val context = context ?: return null
val bookmarks = viewModel.bookmarks.value val bookmarks = viewModel.bookmarks.value
@ -146,5 +197,6 @@ class BookmarksListFragment : Fragment(R.layout.recyclerview), CreateNfcAppDataC
companion object { companion object {
private const val PREF_UPCOMING_ONLY = "bookmarks_upcoming_only" private const val PREF_UPCOMING_ONLY = "bookmarks_upcoming_only"
private const val IMPORT_REQUEST_CODE = 1
} }
} }

View file

@ -1,7 +1,12 @@
package be.digitalia.fosdem.fragments package be.digitalia.fosdem.fragments
import android.app.Dialog
import android.os.Bundle import android.os.Bundle
import android.view.Menu
import android.view.MenuInflater
import android.view.MenuItem
import android.view.View import android.view.View
import androidx.fragment.app.DialogFragment
import androidx.fragment.app.Fragment import androidx.fragment.app.Fragment
import androidx.fragment.app.viewModels import androidx.fragment.app.viewModels
import androidx.recyclerview.widget.DividerItemDecoration import androidx.recyclerview.widget.DividerItemDecoration
@ -9,10 +14,17 @@ import androidx.recyclerview.widget.LinearLayoutManager
import be.digitalia.fosdem.R import be.digitalia.fosdem.R
import be.digitalia.fosdem.adapters.EventsAdapter import be.digitalia.fosdem.adapters.EventsAdapter
import be.digitalia.fosdem.viewmodels.ExternalBookmarksViewModel import be.digitalia.fosdem.viewmodels.ExternalBookmarksViewModel
import com.google.android.material.dialog.MaterialAlertDialogBuilder
class ExternalBookmarksListFragment : Fragment(R.layout.recyclerview) { class ExternalBookmarksListFragment : Fragment(R.layout.recyclerview) {
private val viewModel: ExternalBookmarksViewModel by viewModels() private val viewModel: ExternalBookmarksViewModel by viewModels()
private var addAllMenuItem: MenuItem? = null
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setHasOptionsMenu(true)
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) { override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState) super.onViewCreated(view, savedInstanceState)
@ -34,11 +46,51 @@ class ExternalBookmarksListFragment : Fragment(R.layout.recyclerview) {
setBookmarkIds(bookmarkIds) setBookmarkIds(bookmarkIds)
bookmarks.observe(viewLifecycleOwner) { bookmarks -> bookmarks.observe(viewLifecycleOwner) { bookmarks ->
adapter.submitList(bookmarks) adapter.submitList(bookmarks)
addAllMenuItem?.isEnabled = bookmarks.isNotEmpty()
holder.isProgressBarVisible = false holder.isProgressBarVisible = false
} }
} }
} }
override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
inflater.inflate(R.menu.external_bookmarks, menu)
menu.findItem(R.id.add_all)?.let { item ->
val bookmarks = viewModel.bookmarks.value
item.isEnabled = bookmarks != null && bookmarks.isNotEmpty()
addAllMenuItem = item
}
}
override fun onDestroyOptionsMenu() {
super.onDestroyOptionsMenu()
addAllMenuItem = null
}
override fun onOptionsItemSelected(item: MenuItem) = when (item.itemId) {
R.id.add_all -> {
val dialogFragment = ConfirmAddAllDialogFragment()
dialogFragment.setTargetFragment(this, 0)
dialogFragment.show(parentFragmentManager, "confirmAddAll")
true
}
else -> false
}
class ConfirmAddAllDialogFragment : DialogFragment() {
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
return MaterialAlertDialogBuilder(requireContext())
.setTitle(R.string.external_bookmarks_add_all_confirmation_title)
.setMessage(R.string.external_bookmarks_add_all_confirmation_text)
.setPositiveButton(android.R.string.ok) { _, _ -> (targetFragment as ExternalBookmarksListFragment).onConfirmAddAll() }
.setNegativeButton(android.R.string.cancel, null)
.create()
}
}
fun onConfirmAddAll() {
viewModel.addAll()
}
companion object { companion object {
private const val ARG_BOOKMARK_IDS = "bookmark_ids" private const val ARG_BOOKMARK_IDS = "bookmark_ids"

View file

@ -0,0 +1,98 @@
package be.digitalia.fosdem.ical
import be.digitalia.fosdem.ical.internal.CRLF
import okio.Buffer
import okio.BufferedSource
import java.io.Closeable
import java.io.IOException
/**
* Optimized streaming ICalendar reader.
*/
class ICalendarReader(private val source: BufferedSource) : Closeable {
private var state = STATE_BEGIN_KEY
class Options private constructor(internal val suffixedKey: okio.Options) {
companion object {
fun of(vararg keys: String): Options {
val buffer = Buffer()
val result = Array(keys.size) {
buffer.writeUtf8(keys[it])
buffer.writeByte(':'.toInt())
buffer.readByteString()
}
return Options(okio.Options.of(*result))
}
}
}
fun hasNext(): Boolean = !source.exhausted()
fun nextKey(): String {
check(state == STATE_BEGIN_KEY)
val endPosition = source.indexOf(':'.toByte())
endPosition >= 0L || throw IOException("Invalid key")
val result = source.readUtf8(endPosition)
source.skip(1L)
state = STATE_BEGIN_VALUE
return result
}
fun skipKey() {
check(state == STATE_BEGIN_KEY)
val endPosition = source.indexOf(':'.toByte())
endPosition >= 0L || throw IOException("Invalid key")
source.skip(endPosition + 1L)
state = STATE_BEGIN_VALUE
}
fun selectKey(options: Options): Int {
check(state == STATE_BEGIN_KEY)
val result = source.select(options.suffixedKey)
if (result >= 0) {
state = STATE_BEGIN_VALUE
}
return result
}
private inline fun BufferedSource.unfoldLines(lineLambda: BufferedSource.(endPosition: Long) -> Unit) {
while (true) {
val endPosition = indexOf(CRLF)
if (endPosition < 0L) {
// buffer now contains the rest of the file until the end
lineLambda(buffer.size)
break
}
lineLambda(endPosition)
skip(2L)
if (!request(1L) || buffer[0L] != ' '.toByte()) {
break
}
skip(1L)
}
}
fun nextValue(): String {
check(state == STATE_BEGIN_VALUE)
val resultBuffer = Buffer()
source.unfoldLines { read(resultBuffer, it) }
state = STATE_BEGIN_KEY
return resultBuffer.readUtf8()
}
fun skipValue() {
check(state == STATE_BEGIN_VALUE)
source.unfoldLines { skip(it) }
state = STATE_BEGIN_KEY
}
@Throws(IOException::class)
override fun close() {
source.close()
}
companion object {
private const val STATE_BEGIN_KEY = 0
private const val STATE_BEGIN_VALUE = 1
}
}

View file

@ -1,7 +1,7 @@
package be.digitalia.fosdem.utils package be.digitalia.fosdem.ical
import be.digitalia.fosdem.ical.internal.CRLF
import okio.BufferedSink import okio.BufferedSink
import okio.ByteString.Companion.encodeUtf8
import java.io.Closeable import java.io.Closeable
import java.io.IOException import java.io.IOException
@ -15,9 +15,9 @@ class ICalendarWriter(private val sink: BufferedSink) : Closeable {
if (value != null) { if (value != null) {
with(sink) { with(sink) {
writeUtf8(key) writeUtf8(key)
writeUtf8CodePoint(':'.toInt()) writeByte(':'.toInt())
// Escape line break sequences // Fold line break sequences
val length = value.length val length = value.length
var start = 0 var start = 0
var end = 0 var end = 0
@ -26,7 +26,7 @@ class ICalendarWriter(private val sink: BufferedSink) : Closeable {
if (c == '\r' || c == '\n') { if (c == '\r' || c == '\n') {
writeUtf8(value, start, end) writeUtf8(value, start, end)
write(CRLF) write(CRLF)
writeUtf8CodePoint(' '.toInt()) writeByte(' '.toInt())
do { do {
end++ end++
} while (end < length && (value[end] == '\r' || value[end] == '\n')) } while (end < length && (value[end] == '\r' || value[end] == '\n'))
@ -46,9 +46,4 @@ class ICalendarWriter(private val sink: BufferedSink) : Closeable {
override fun close() { override fun close() {
sink.close() sink.close()
} }
companion object {
private val CRLF = "\r\n".encodeUtf8()
}
} }

View file

@ -0,0 +1,5 @@
package be.digitalia.fosdem.ical.internal
import okio.ByteString.Companion.encodeUtf8
internal val CRLF = "\r\n".encodeUtf8()

View file

@ -1,14 +1,19 @@
package be.digitalia.fosdem.model package be.digitalia.fosdem.model
import android.os.Parcelable
import androidx.room.ColumnInfo import androidx.room.ColumnInfo
import androidx.room.TypeConverters import androidx.room.TypeConverters
import be.digitalia.fosdem.db.converters.NullableDateTypeConverters import be.digitalia.fosdem.db.converters.NullableDateTypeConverters
import be.digitalia.fosdem.utils.DateParceler
import kotlinx.parcelize.Parcelize
import kotlinx.parcelize.WriteWith
import java.util.Date import java.util.Date
class AlarmInfo( @Parcelize
data class AlarmInfo(
@ColumnInfo(name = "event_id") @ColumnInfo(name = "event_id")
val eventId: Long, val eventId: Long,
@ColumnInfo(name = "start_time") @ColumnInfo(name = "start_time")
@field:TypeConverters(NullableDateTypeConverters::class) @field:TypeConverters(NullableDateTypeConverters::class)
val startTime: Date? val startTime: @WriteWith<DateParceler> Date?
) ) : Parcelable

View file

@ -0,0 +1,45 @@
package be.digitalia.fosdem.parsers
import be.digitalia.fosdem.ical.ICalendarReader
import okio.BufferedSource
/**
* Extract event ids from an exported bookmarks file and validate that events match the given application id and year.
*/
class ExportedBookmarksParser(private val applicationId: String, year: Int) : Parser<LongArray> {
private val yearString = year.toString()
override fun parse(source: BufferedSource): LongArray {
val reader = ICalendarReader(source)
val eventIdList = mutableListOf<String>()
while (reader.hasNext()) {
if (reader.selectKey(KEYS) == -1) {
reader.skipKey()
reader.skipValue()
} else {
val uid = reader.nextValue()
val parts = uid.split("@")
// validate UID
if (parts.size < 3) {
throw DataException("Invalid UID format: $uid")
}
if (parts[2] != applicationId) {
throw DataException("Invalid application id. Expected: $applicationId, actual: ${parts[2]}")
}
if (parts[1] != yearString) {
throw DataException("Invalid conference year. Expected: $yearString, actual: ${parts[1]}")
}
eventIdList += parts[0]
}
}
return LongArray(eventIdList.size) { eventIdList[it].toLong() }
}
class DataException(message: String) : RuntimeException(message)
companion object {
private val KEYS = ICalendarReader.Options.of("UID")
}
}

View file

@ -14,9 +14,9 @@ import be.digitalia.fosdem.BuildConfig
import be.digitalia.fosdem.R import be.digitalia.fosdem.R
import be.digitalia.fosdem.api.FosdemUrls import be.digitalia.fosdem.api.FosdemUrls
import be.digitalia.fosdem.db.AppDatabase import be.digitalia.fosdem.db.AppDatabase
import be.digitalia.fosdem.ical.ICalendarWriter
import be.digitalia.fosdem.model.Event import be.digitalia.fosdem.model.Event
import be.digitalia.fosdem.utils.DateUtils import be.digitalia.fosdem.utils.DateUtils
import be.digitalia.fosdem.utils.ICalendarWriter
import be.digitalia.fosdem.utils.stripHtml import be.digitalia.fosdem.utils.stripHtml
import be.digitalia.fosdem.utils.toSlug import be.digitalia.fosdem.utils.toSlug
import okio.buffer import okio.buffer
@ -144,12 +144,12 @@ class BookmarksExportProvider : ContentProvider() {
} }
companion object { companion object {
const val TYPE = "text/calendar"
private val URI = Uri.Builder() private val URI = Uri.Builder()
.scheme("content") .scheme("content")
.authority("${BuildConfig.APPLICATION_ID}.bookmarks") .authority("${BuildConfig.APPLICATION_ID}.bookmarks")
.appendPath("bookmarks.ics") .appendPath("bookmarks.ics")
.build() .build()
private const val TYPE = "text/calendar"
private val COLUMNS = arrayOf(OpenableColumns.DISPLAY_NAME, OpenableColumns.SIZE) private val COLUMNS = arrayOf(OpenableColumns.DISPLAY_NAME, OpenableColumns.SIZE)
fun getIntent(activity: Activity?): Intent { fun getIntent(activity: Activity?): Intent {

View file

@ -32,6 +32,7 @@ import be.digitalia.fosdem.activities.EventDetailsActivity
import be.digitalia.fosdem.activities.MainActivity import be.digitalia.fosdem.activities.MainActivity
import be.digitalia.fosdem.activities.RoomImageDialogActivity import be.digitalia.fosdem.activities.RoomImageDialogActivity
import be.digitalia.fosdem.db.AppDatabase import be.digitalia.fosdem.db.AppDatabase
import be.digitalia.fosdem.model.AlarmInfo
import be.digitalia.fosdem.model.Event import be.digitalia.fosdem.model.Event
import be.digitalia.fosdem.receivers.AlarmReceiver import be.digitalia.fosdem.receivers.AlarmReceiver
import be.digitalia.fosdem.utils.PreferenceKeys import be.digitalia.fosdem.utils.PreferenceKeys
@ -86,17 +87,24 @@ class AlarmIntentService : JobIntentService() {
} }
setAlarmReceiverEnabled(false) setAlarmReceiverEnabled(false)
} }
ACTION_ADD_BOOKMARK -> { ACTION_ADD_BOOKMARKS -> {
val delay = delay val delay = delay
val eventId = intent.getLongExtra(EXTRA_EVENT_ID, -1L) @Suppress("UNCHECKED_CAST")
val startTime = intent.getLongExtra(EXTRA_EVENT_START_TIME, -1L) val alarmInfos = intent.getParcelableArrayListExtra<AlarmInfo>(EXTRA_ALARM_INFOS)!!
// Only schedule future events. If they start before the delay, the alarm will go off immediately val now = System.currentTimeMillis()
if (startTime != -1L && startTime >= System.currentTimeMillis()) { var isFirstAlarm = true
setAlarmReceiverEnabled(true) for ((eventId, startTime) in alarmInfos) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { // Only schedule future events. If they start before the delay, the alarm will go off immediately
createNotificationChannel(this) if (startTime != null && startTime.time >= now) {
if (isFirstAlarm) {
setAlarmReceiverEnabled(true)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
createNotificationChannel(this)
}
isFirstAlarm = false
}
AlarmManagerCompat.setExact(alarmManager, AlarmManager.RTC_WAKEUP, startTime.time - delay, getAlarmPendingIntent(eventId))
} }
AlarmManagerCompat.setExact(alarmManager, AlarmManager.RTC_WAKEUP, startTime - delay, getAlarmPendingIntent(eventId))
} }
} }
ACTION_REMOVE_BOOKMARKS -> { ACTION_REMOVE_BOOKMARKS -> {
@ -226,9 +234,8 @@ class AlarmIntentService : JobIntentService() {
const val ACTION_UPDATE_ALARMS = "${BuildConfig.APPLICATION_ID}.action.UPDATE_ALARMS" const val ACTION_UPDATE_ALARMS = "${BuildConfig.APPLICATION_ID}.action.UPDATE_ALARMS"
const val ACTION_DISABLE_ALARMS = "${BuildConfig.APPLICATION_ID}.action.DISABLE_ALARMS" const val ACTION_DISABLE_ALARMS = "${BuildConfig.APPLICATION_ID}.action.DISABLE_ALARMS"
const val ACTION_ADD_BOOKMARK = "${BuildConfig.APPLICATION_ID}.action.ADD_BOOKMARK" const val ACTION_ADD_BOOKMARKS = "${BuildConfig.APPLICATION_ID}.action.ADD_BOOKMARK"
const val EXTRA_EVENT_ID = "event_id" const val EXTRA_ALARM_INFOS = "alarm_info"
const val EXTRA_EVENT_START_TIME = "event_start"
const val ACTION_REMOVE_BOOKMARKS = "${BuildConfig.APPLICATION_ID}.action.REMOVE_BOOKMARKS" const val ACTION_REMOVE_BOOKMARKS = "${BuildConfig.APPLICATION_ID}.action.REMOVE_BOOKMARKS"
const val EXTRA_EVENT_IDS = "event_ids" const val EXTRA_EVENT_IDS = "event_ids"

View file

@ -1,14 +1,21 @@
package be.digitalia.fosdem.viewmodels package be.digitalia.fosdem.viewmodels
import android.app.Application import android.app.Application
import android.net.Uri
import android.text.format.DateUtils import android.text.format.DateUtils
import androidx.lifecycle.AndroidViewModel import androidx.lifecycle.AndroidViewModel
import androidx.lifecycle.LiveData import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.switchMap import androidx.lifecycle.switchMap
import be.digitalia.fosdem.BuildConfig
import be.digitalia.fosdem.db.AppDatabase import be.digitalia.fosdem.db.AppDatabase
import be.digitalia.fosdem.livedata.LiveDataFactory import be.digitalia.fosdem.livedata.LiveDataFactory
import be.digitalia.fosdem.model.Event import be.digitalia.fosdem.model.Event
import be.digitalia.fosdem.parsers.ExportedBookmarksParser
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import okio.buffer
import okio.source
import java.util.concurrent.TimeUnit import java.util.concurrent.TimeUnit
class BookmarksViewModel(application: Application) : AndroidViewModel(application) { class BookmarksViewModel(application: Application) : AndroidViewModel(application) {
@ -37,7 +44,16 @@ class BookmarksViewModel(application: Application) : AndroidViewModel(applicatio
} }
fun removeBookmarks(eventIds: LongArray) { fun removeBookmarks(eventIds: LongArray) {
appDatabase.bookmarksDao.removeBookmarksAsync(*eventIds) appDatabase.bookmarksDao.removeBookmarksAsync(eventIds)
}
suspend fun readBookmarkIds(uri: Uri): LongArray {
return withContext(Dispatchers.IO) {
val parser = ExportedBookmarksParser(BuildConfig.APPLICATION_ID, appDatabase.scheduleDao.getYear())
checkNotNull(getApplication<Application>().contentResolver.openInputStream(uri)).source().buffer().use {
parser.parse(it)
}
}
} }
companion object { companion object {

View file

@ -25,4 +25,9 @@ class ExternalBookmarksViewModel(application: Application) : AndroidViewModel(ap
bookmarkIdsLiveData.value = bookmarkIds bookmarkIdsLiveData.value = bookmarkIds
} }
} }
fun addAll() {
val bookmarkIds = bookmarkIdsLiveData.value ?: return
appDatabase.bookmarksDao.addBookmarksAsync(bookmarkIds)
}
} }

View file

@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24.0"
android:viewportHeight="24.0">
<path
android:fillColor="#FFFFFFFF"
android:pathData="M19,9h-4V3H9v6H5l7,7 7,-7zM5,18v2h14v-2H5z"/>
</vector>

View file

@ -20,5 +20,11 @@
android:title="@string/export_bookmarks" android:title="@string/export_bookmarks"
app:iconTint="?colorControlNormal" app:iconTint="?colorControlNormal"
app:showAsAction="ifRoom" /> app:showAsAction="ifRoom" />
<item
android:id="@+id/import_bookmarks"
android:icon="@drawable/ic_file_download_white_24dp"
android:title="@string/import_bookmarks"
app:iconTint="?colorControlNormal"
app:showAsAction="ifRoom" />
</menu> </menu>

View file

@ -0,0 +1,10 @@
<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<item
android:id="@+id/add_all"
android:title="@string/external_bookmarks_add_all"
app:showAsAction="ifRoom" />
</menu>

View file

@ -28,6 +28,8 @@
<string name="upcoming_only">Upcoming only</string> <string name="upcoming_only">Upcoming only</string>
<string name="export_bookmarks">Export bookmarks</string> <string name="export_bookmarks">Export bookmarks</string>
<string name="export_bookmarks_file_name">FOSDEM %1$d bookmarks.ics</string> <string name="export_bookmarks_file_name">FOSDEM %1$d bookmarks.ics</string>
<string name="import_bookmarks">Import bookmarks</string>
<string name="import_bookmarks_error">Unable to import bookmarks from the selected file.\n\nMake sure the file was created using this application and the conference year is matching.</string>
<string name="no_bookmark">No bookmark.</string> <string name="no_bookmark">No bookmark.</string>
<string name="remove_bookmarks">Remove bookmarks</string> <string name="remove_bookmarks">Remove bookmarks</string>
<string name="bookmark_conflict_content_description">%1$s\n Other bookmarks are scheduled at the same time.</string> <string name="bookmark_conflict_content_description">%1$s\n Other bookmarks are scheduled at the same time.</string>
@ -62,7 +64,10 @@
<string name="no_search_result">No result.</string> <string name="no_search_result">No result.</string>
<!-- External bookmarks --> <!-- External bookmarks -->
<string name="external_bookmarks_title">Your friend\'s bookmarks</string> <string name="external_bookmarks_title">External bookmarks</string>
<string name="external_bookmarks_add_all">Add all</string>
<string name="external_bookmarks_add_all_confirmation_title">Confirmation</string>
<string name="external_bookmarks_add_all_confirmation_text">Add all events to your local bookmarks?</string>
<!-- Errors --> <!-- Errors -->
<string name="event_not_found_error">Unable to load the session details.\nMake sure the database has been updated to the latest version.</string> <string name="event_not_found_error">Unable to load the session details.\nMake sure the database has been updated to the latest version.</string>