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) {
val intent = intent
val bookmarkIds = if (intent.hasNfcAppData()) {
intent.extractNfcAppData().toBookmarks()
} else null
val bookmarkIds = when {
intent.hasExtra(EXTRA_BOOKMARK_IDS) -> intent.getLongArrayExtra(EXTRA_BOOKMARK_IDS)
intent.hasNfcAppData() -> intent.extractNfcAppData().toBookmarks()
else -> null
}
if (bookmarkIds == null) {
// Invalid data format, exit
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
import android.annotation.SuppressLint
import android.content.Context
import android.content.Intent
import android.content.SharedPreferences.OnSharedPreferenceChangeListener
import androidx.annotation.MainThread
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.utils.PreferenceKeys
@ -14,6 +15,7 @@ import be.digitalia.fosdem.utils.PreferenceKeys
*
* @author Christophe Beyls
*/
@SuppressLint("StaticFieldLeak")
object FosdemAlarmManager {
private lateinit var context: Context
@ -51,20 +53,17 @@ object FosdemAlarmManager {
}
@MainThread
fun onBookmarkAdded(event: Event) {
fun onBookmarksAdded(alarmInfos: List<AlarmInfo>) {
if (isEnabled) {
val serviceIntent = Intent(AlarmIntentService.ACTION_ADD_BOOKMARK).apply {
putExtra(AlarmIntentService.EXTRA_EVENT_ID, event.id)
event.startTime?.let {
putExtra(AlarmIntentService.EXTRA_EVENT_START_TIME, it.time)
}
}
val arrayList = if (alarmInfos is ArrayList<AlarmInfo>) alarmInfos else ArrayList(alarmInfos)
val serviceIntent = Intent(AlarmIntentService.ACTION_ADD_BOOKMARKS)
.putParcelableArrayListExtra(AlarmIntentService.EXTRA_ALARM_INFOS, arrayList)
AlarmIntentService.enqueueWork(context, serviceIntent)
}
}
@MainThread
fun onBookmarksRemoved(eventIds: LongArray?) {
fun onBookmarksRemoved(eventIds: LongArray) {
if (isEnabled) {
val serviceIntent = Intent(AlarmIntentService.ACTION_REMOVE_BOOKMARKS)
.putExtra(AlarmIntentService.EXTRA_EVENT_IDS, eventIds)

View file

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

View file

@ -1,7 +1,10 @@
package be.digitalia.fosdem.fragments
import android.app.Activity
import android.app.Dialog
import android.content.Context
import android.content.Intent
import android.net.Uri
import android.nfc.NdefRecord
import android.os.Bundle
import android.view.Menu
@ -11,17 +14,22 @@ import android.view.View
import androidx.appcompat.app.AppCompatActivity
import androidx.appcompat.view.ActionMode
import androidx.core.content.edit
import androidx.fragment.app.DialogFragment
import androidx.fragment.app.Fragment
import androidx.fragment.app.viewModels
import androidx.lifecycle.lifecycleScope
import androidx.recyclerview.widget.DividerItemDecoration
import androidx.recyclerview.widget.LinearLayoutManager
import be.digitalia.fosdem.R
import be.digitalia.fosdem.activities.ExternalBookmarksActivity
import be.digitalia.fosdem.adapters.BookmarksAdapter
import be.digitalia.fosdem.providers.BookmarksExportProvider
import be.digitalia.fosdem.utils.CreateNfcAppDataCallback
import be.digitalia.fosdem.utils.toBookmarksNfcAppData
import be.digitalia.fosdem.viewmodels.BookmarksViewModel
import be.digitalia.fosdem.widgets.MultiChoiceHelper
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import java.util.concurrent.CancellationException
/**
* Bookmarks list, optionally filterable.
@ -133,9 +141,52 @@ class BookmarksListFragment : Fragment(R.layout.recyclerview), CreateNfcAppDataC
startActivity(Intent.createChooser(exportIntent, getString(R.string.export_bookmarks)))
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
}
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? {
val context = context ?: return null
val bookmarks = viewModel.bookmarks.value
@ -146,5 +197,6 @@ class BookmarksListFragment : Fragment(R.layout.recyclerview), CreateNfcAppDataC
companion object {
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
import android.app.Dialog
import android.os.Bundle
import android.view.Menu
import android.view.MenuInflater
import android.view.MenuItem
import android.view.View
import androidx.fragment.app.DialogFragment
import androidx.fragment.app.Fragment
import androidx.fragment.app.viewModels
import androidx.recyclerview.widget.DividerItemDecoration
@ -9,10 +14,17 @@ import androidx.recyclerview.widget.LinearLayoutManager
import be.digitalia.fosdem.R
import be.digitalia.fosdem.adapters.EventsAdapter
import be.digitalia.fosdem.viewmodels.ExternalBookmarksViewModel
import com.google.android.material.dialog.MaterialAlertDialogBuilder
class ExternalBookmarksListFragment : Fragment(R.layout.recyclerview) {
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?) {
super.onViewCreated(view, savedInstanceState)
@ -34,11 +46,51 @@ class ExternalBookmarksListFragment : Fragment(R.layout.recyclerview) {
setBookmarkIds(bookmarkIds)
bookmarks.observe(viewLifecycleOwner) { bookmarks ->
adapter.submitList(bookmarks)
addAllMenuItem?.isEnabled = bookmarks.isNotEmpty()
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 {
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.ByteString.Companion.encodeUtf8
import java.io.Closeable
import java.io.IOException
@ -15,9 +15,9 @@ class ICalendarWriter(private val sink: BufferedSink) : Closeable {
if (value != null) {
with(sink) {
writeUtf8(key)
writeUtf8CodePoint(':'.toInt())
writeByte(':'.toInt())
// Escape line break sequences
// Fold line break sequences
val length = value.length
var start = 0
var end = 0
@ -26,7 +26,7 @@ class ICalendarWriter(private val sink: BufferedSink) : Closeable {
if (c == '\r' || c == '\n') {
writeUtf8(value, start, end)
write(CRLF)
writeUtf8CodePoint(' '.toInt())
writeByte(' '.toInt())
do {
end++
} while (end < length && (value[end] == '\r' || value[end] == '\n'))
@ -46,9 +46,4 @@ class ICalendarWriter(private val sink: BufferedSink) : Closeable {
override fun 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
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 kotlinx.parcelize.Parcelize
import kotlinx.parcelize.WriteWith
import java.util.Date
class AlarmInfo(
@Parcelize
data class AlarmInfo(
@ColumnInfo(name = "event_id")
val eventId: Long,
@ColumnInfo(name = "start_time")
@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.api.FosdemUrls
import be.digitalia.fosdem.db.AppDatabase
import be.digitalia.fosdem.ical.ICalendarWriter
import be.digitalia.fosdem.model.Event
import be.digitalia.fosdem.utils.DateUtils
import be.digitalia.fosdem.utils.ICalendarWriter
import be.digitalia.fosdem.utils.stripHtml
import be.digitalia.fosdem.utils.toSlug
import okio.buffer
@ -144,12 +144,12 @@ class BookmarksExportProvider : ContentProvider() {
}
companion object {
const val TYPE = "text/calendar"
private val URI = Uri.Builder()
.scheme("content")
.authority("${BuildConfig.APPLICATION_ID}.bookmarks")
.appendPath("bookmarks.ics")
.build()
private const val TYPE = "text/calendar"
private val COLUMNS = arrayOf(OpenableColumns.DISPLAY_NAME, OpenableColumns.SIZE)
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.RoomImageDialogActivity
import be.digitalia.fosdem.db.AppDatabase
import be.digitalia.fosdem.model.AlarmInfo
import be.digitalia.fosdem.model.Event
import be.digitalia.fosdem.receivers.AlarmReceiver
import be.digitalia.fosdem.utils.PreferenceKeys
@ -86,17 +87,24 @@ class AlarmIntentService : JobIntentService() {
}
setAlarmReceiverEnabled(false)
}
ACTION_ADD_BOOKMARK -> {
ACTION_ADD_BOOKMARKS -> {
val delay = delay
val eventId = intent.getLongExtra(EXTRA_EVENT_ID, -1L)
val startTime = intent.getLongExtra(EXTRA_EVENT_START_TIME, -1L)
@Suppress("UNCHECKED_CAST")
val alarmInfos = intent.getParcelableArrayListExtra<AlarmInfo>(EXTRA_ALARM_INFOS)!!
val now = System.currentTimeMillis()
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 != -1L && startTime >= System.currentTimeMillis()) {
if (startTime != null && startTime.time >= now) {
if (isFirstAlarm) {
setAlarmReceiverEnabled(true)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
createNotificationChannel(this)
}
AlarmManagerCompat.setExact(alarmManager, AlarmManager.RTC_WAKEUP, startTime - delay, getAlarmPendingIntent(eventId))
isFirstAlarm = false
}
AlarmManagerCompat.setExact(alarmManager, AlarmManager.RTC_WAKEUP, startTime.time - delay, getAlarmPendingIntent(eventId))
}
}
}
ACTION_REMOVE_BOOKMARKS -> {
@ -226,9 +234,8 @@ class AlarmIntentService : JobIntentService() {
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_ADD_BOOKMARK = "${BuildConfig.APPLICATION_ID}.action.ADD_BOOKMARK"
const val EXTRA_EVENT_ID = "event_id"
const val EXTRA_EVENT_START_TIME = "event_start"
const val ACTION_ADD_BOOKMARKS = "${BuildConfig.APPLICATION_ID}.action.ADD_BOOKMARK"
const val EXTRA_ALARM_INFOS = "alarm_info"
const val ACTION_REMOVE_BOOKMARKS = "${BuildConfig.APPLICATION_ID}.action.REMOVE_BOOKMARKS"
const val EXTRA_EVENT_IDS = "event_ids"

View file

@ -1,14 +1,21 @@
package be.digitalia.fosdem.viewmodels
import android.app.Application
import android.net.Uri
import android.text.format.DateUtils
import androidx.lifecycle.AndroidViewModel
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.switchMap
import be.digitalia.fosdem.BuildConfig
import be.digitalia.fosdem.db.AppDatabase
import be.digitalia.fosdem.livedata.LiveDataFactory
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
class BookmarksViewModel(application: Application) : AndroidViewModel(application) {
@ -37,7 +44,16 @@ class BookmarksViewModel(application: Application) : AndroidViewModel(applicatio
}
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 {

View file

@ -25,4 +25,9 @@ class ExternalBookmarksViewModel(application: Application) : AndroidViewModel(ap
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"
app:iconTint="?colorControlNormal"
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>

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="export_bookmarks">Export bookmarks</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="remove_bookmarks">Remove bookmarks</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>
<!-- 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 -->
<string name="event_not_found_error">Unable to load the session details.\nMake sure the database has been updated to the latest version.</string>