Migrate calendar widget settings

This commit is contained in:
MM20 2022-01-10 23:13:53 +01:00
parent 5834e7e8c4
commit a5cb2e3314
No known key found for this signature in database
GPG Key ID: 0B61A8F2DEAFA389
13 changed files with 295 additions and 225 deletions

View File

@ -5,7 +5,6 @@ import android.os.Bundle
import android.view.MenuItem
import androidx.appcompat.app.AppCompatActivity
import androidx.fragment.app.Fragment
import de.mm20.launcher2.fragment.PreferencesCalendarFragment
import de.mm20.launcher2.fragment.PreferencesMainFragment
import de.mm20.launcher2.fragment.PreferencesServicesFragment
import de.mm20.launcher2.fragment.PreferencesWeatherFragment
@ -31,7 +30,6 @@ class SettingsActivity : AppCompatActivity() {
private fun getStartFragment(): Fragment {
return when (intent.extras?.getString(FRAGMENT, "")) {
FRAGMENT_CALENDAR -> PreferencesCalendarFragment()
FRAGMENT_WEATHER -> PreferencesWeatherFragment()
FRAGMENT_SERVICES -> PreferencesServicesFragment()
else -> PreferencesMainFragment()

View File

@ -1,121 +0,0 @@
package de.mm20.launcher2.fragment
import android.Manifest
import android.content.res.ColorStateList
import android.os.Bundle
import android.widget.CheckBox
import android.widget.LinearLayout
import android.widget.ScrollView
import android.widget.TextView
import androidx.appcompat.app.AppCompatActivity
import androidx.core.app.ActivityCompat
import androidx.core.content.ContextCompat
import androidx.core.view.setPadding
import androidx.preference.Preference
import androidx.preference.PreferenceFragmentCompat
import com.afollestad.materialdialogs.MaterialDialog
import com.afollestad.materialdialogs.bottomsheets.BottomSheet
import com.afollestad.materialdialogs.customview.customView
import de.mm20.launcher2.R
import de.mm20.launcher2.ktx.checkPermission
import de.mm20.launcher2.ktx.dp
import de.mm20.launcher2.preferences.LauncherPreferences
import de.mm20.launcher2.search.data.CalendarEvent
import de.mm20.launcher2.search.data.UserCalendar
class PreferencesCalendarFragment : PreferenceFragmentCompat() {
private var hasCalendarPermission = false
private val calendars = mutableListOf<UserCalendar>()
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
addPreferencesFromResource(R.xml.preferences_calendar)
init()
}
fun init(requestPermission: Boolean = true) {
val context = context ?: return
hasCalendarPermission = context.checkPermission(Manifest.permission.READ_CALENDAR)
if (hasCalendarPermission) {
calendars.clear()
calendars.addAll(CalendarEvent.getCalendars(context))
val unselectedCalendars = LauncherPreferences.instance.unselectedCalendars.toMutableList()
findPreference<Preference>("calendar_calendars")?.apply {
var count = calendars.size - unselectedCalendars.size
summary = resources.getQuantityString(R.plurals.preference_calendar_calendars_summary, count, count)
isEnabled = true
setOnPreferenceClickListener {
val sheetView = LinearLayout(activity)
sheetView.setPadding((8 * context.dp).toInt())
sheetView.orientation = LinearLayout.VERTICAL
sheetView.setBackgroundColor(ContextCompat.getColor(context, R.color.bottom_sheet))
var owner = ""
val padding = (8 * context.dp).toInt()
for (c in calendars) {
if (owner != c.owner) {
owner = c.owner
val text = TextView(activity)
text.setTextColor(ContextCompat.getColor(context, R.color.text_color_secondary_normal))
text.setPadding(padding, 2 * padding, padding, padding)
text.text = owner
sheetView.addView(text)
}
val checkbox = CheckBox(activity)
checkbox.text = c.name
checkbox.buttonTintList = ColorStateList.valueOf(CalendarEvent.getDisplayColor(context, c.color))
checkbox.setPadding(padding)
checkbox.isChecked = !unselectedCalendars.contains(c.id)
checkbox.setOnCheckedChangeListener { _, checked ->
if (checked) {
unselectedCalendars.remove(c.id)
} else {
unselectedCalendars.add(c.id)
}
LauncherPreferences.instance.unselectedCalendars = unselectedCalendars
count = calendars.size - unselectedCalendars.size
summary = resources.getQuantityString(R.plurals.preference_calendar_calendars_summary, count, count)
}
sheetView.addView(checkbox)
}
val scrollView = ScrollView(context)
scrollView.isNestedScrollingEnabled = true
scrollView.addView(sheetView)
MaterialDialog(context, BottomSheet()).show {
customView(view = scrollView)
title(R.string.preference_calendar_calendars)
.negativeButton(R.string.close) {
dismiss()
}
}
true
}
}
} else {
if (requestPermission) {
ActivityCompat.requestPermissions(
requireActivity(),
arrayOf(Manifest.permission.READ_CALENDAR, Manifest.permission.WRITE_CALENDAR),
0)
}
findPreference<Preference>("calendar_calendars")?.apply {
isEnabled = false
setSummary(R.string.preference_permission_denied)
}
}
}
override fun onResume() {
super.onResume()
(activity as AppCompatActivity).supportActionBar?.setTitle(R.string.preference_screen_calendarwidget)
hasCalendarPermission = requireActivity().checkPermission(Manifest.permission.READ_CALENDAR)
&& requireActivity().checkPermission(Manifest.permission.WRITE_CALENDAR)
}
override fun onRequestPermissionsResult(requestCode: Int, permissions: Array<out String>, grantResults: IntArray) {
super.onRequestPermissionsResult(requestCode, permissions, grantResults)
init(false)
}
}

View File

@ -34,10 +34,6 @@ class PreferencesMainFragment : PreferenceFragmentCompat() {
setSettingsScreen(PreferencesSearchFragment())
true
}
findPreference<Preference>("screen_calendar")?.setOnPreferenceClickListener {
setSettingsScreen(PreferencesCalendarFragment())
true
}
findPreference<Preference>("screen_badges")?.setOnPreferenceClickListener {
setSettingsScreen(PreferencesBadgesFragment())
true

View File

@ -1,22 +1,21 @@
package de.mm20.launcher2.calendar
import android.Manifest
import android.content.ContentUris
import android.content.Context
import android.provider.CalendarContract
import androidx.core.database.getStringOrNull
import de.mm20.launcher2.hiddenitems.HiddenItemsRepository
import de.mm20.launcher2.ktx.checkPermission
import de.mm20.launcher2.permissions.PermissionGroup
import de.mm20.launcher2.permissions.PermissionsManager
import de.mm20.launcher2.preferences.LauncherDataStore
import de.mm20.launcher2.preferences.LauncherPreferences
import de.mm20.launcher2.search.data.CalendarEvent
import de.mm20.launcher2.search.data.UserCalendar
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.channels.awaitClose
import kotlinx.coroutines.channels.trySendBlocking
import kotlinx.coroutines.flow.*
import kotlinx.coroutines.withContext
import org.koin.core.component.KoinComponent
import org.koin.core.component.get
import org.koin.core.component.inject
import java.util.*
@ -24,6 +23,8 @@ interface CalendarRepository {
fun search(query: String): Flow<List<CalendarEvent>>
fun getUpcomingEvents(): Flow<List<CalendarEvent>>
suspend fun getCalendars(): List<UserCalendar>
}
class CalendarRepositoryImpl(
@ -155,55 +156,59 @@ class CalendarRepositoryImpl(
}
override fun getUpcomingEvents(): Flow<List<CalendarEvent>> = channelFlow {
val unselectedCalendars = callbackFlow {
val unregister =
LauncherPreferences.instance.doOnPreferenceChange("unselected_calendars") {
trySendBlocking(LauncherPreferences.instance.unselectedCalendars)
}
trySendBlocking(LauncherPreferences.instance.unselectedCalendars)
awaitClose {
unregister()
}
}
val hideAlldayEvents = callbackFlow {
val unregister =
LauncherPreferences.instance.doOnPreferenceChange("calendar_hide_allday") {
trySendBlocking(LauncherPreferences.instance.calendarHideAllday)
}
trySendBlocking(LauncherPreferences.instance.calendarHideAllday)
awaitClose {
unregister()
}
}
hideAlldayEvents.collectLatest { hideAllday ->
unselectedCalendars.collectLatest { unselected ->
hiddenItems.collectLatest { hidden ->
val now = System.currentTimeMillis()
val end = now + 14 * 24 * 60 * 60 * 1000L
val events = withContext(Dispatchers.IO) {
queryCalendarEvents(
query = "",
intervalStart = now,
intervalEnd = end,
limit = 700,
excludeAllDayEvents = hideAllday,
excludeCalendars = unselected
).filter {
!hiddenItems.value.contains(it.key)
}
dataStore.data.map { it.calendarWidget }.collectLatest { settings ->
hiddenItems.collectLatest { hidden ->
val now = System.currentTimeMillis()
val end = now + 14 * 24 * 60 * 60 * 1000L
val events = withContext(Dispatchers.IO) {
queryCalendarEvents(
query = "",
intervalStart = now,
intervalEnd = end,
limit = 700,
excludeAllDayEvents = settings.hideAlldayEvents,
excludeCalendars = settings.excludeCalendarsList
).filter {
!hiddenItems.value.contains(it.key)
}
send(events)
}
send(events)
}
}
}
var unselectedCalendars: List<Long>
get() = LauncherPreferences.instance.unselectedCalendars
set(value) {
LauncherPreferences.instance.unselectedCalendars = value
override suspend fun getCalendars(): List<UserCalendar> {
if (!permissionsManager.checkPermissionOnce(PermissionGroup.Calendar)) return emptyList()
return withContext(Dispatchers.IO) {
val calendars = mutableListOf<UserCalendar>()
val uri = CalendarContract.Calendars.CONTENT_URI
val proj = arrayOf(
CalendarContract.Calendars._ID,
CalendarContract.Calendars.NAME,
CalendarContract.Calendars.ACCOUNT_NAME,
CalendarContract.Calendars.CALENDAR_COLOR,
CalendarContract.Calendars.VISIBLE,
CalendarContract.Calendars.CALENDAR_DISPLAY_NAME,
)
val cursor = context.contentResolver.query(uri, proj, null, null, null)
?: return@withContext emptyList()
while (cursor.moveToNext()) {
try {
calendars.add(
UserCalendar(
id = cursor.getLong(0),
name = cursor.getString(5) ?: cursor.getString(1) ?: "",
owner = cursor.getString(2),
color = cursor.getInt(3)
)
)
} catch (e: NullPointerException) {
continue
}
}
cursor.close()
calendars.sortBy { it.owner }
return@withContext calendars
}
}
}

View File

@ -79,41 +79,7 @@ class CalendarEvent(
return null
}
companion object: KoinComponent {
fun getCalendars(context: Context): List<UserCalendar> {
val calendars = mutableListOf<UserCalendar>()
val uri = CalendarContract.Calendars.CONTENT_URI
val proj = arrayOf(
CalendarContract.Calendars._ID,
CalendarContract.Calendars.NAME,
CalendarContract.Calendars.ACCOUNT_NAME,
CalendarContract.Calendars.CALENDAR_COLOR,
CalendarContract.Calendars.VISIBLE,
CalendarContract.Calendars.CALENDAR_DISPLAY_NAME,
)
if (!context.checkPermission(Manifest.permission.READ_CALENDAR)) return calendars
val cursor = context.contentResolver.query(uri, proj, null, null, null)
?: return emptyList()
while (cursor.moveToNext()) {
try {
calendars.add(
UserCalendar(
id = cursor.getLong(0),
name = cursor.getString(5) ?: cursor.getString(1) ?: "",
owner = cursor.getString(2),
color = cursor.getInt(3)
)
)
} catch (e: NullPointerException) {
continue
}
}
cursor.close()
calendars.sortBy { it.owner }
return calendars
}
companion object {
fun getDisplayColor(context: Context, color: Int): Int {
val hsl = FloatArray(3).let {
ColorUtils.RGBToHSL(color.red, color.green, color.blue, it)

View File

@ -148,15 +148,8 @@
<string name="preference_dynamic_icon_bg">Dynamischer Hintergrund</string>
<string name="preference_dynamic_icon_bg_summary">Hintergrundfarbe an Symbol anpassen</string>
<string name="preference_category_widget">Widget</string>
<string name="preference_screen_calendarwidget">Kalender</string>
<string name="preference_screen_calendar_summary">Kalender, Widgets</string>
<string name="preference_calendar_calendars">Kalender</string>
<string name="preference_permission_denied">Berechtigung fehlt</string>
<string name="preference_calendar_hide_allday">Ganztägige Termine ausblenden</string>
<plurals name="preference_calendar_calendars_summary">
<item quantity="one">%1$d Kalender ausgewählt</item>
<item quantity="other">%1$d Kalender ausgewählt</item>
</plurals>
<string name="date_format_clock_widget">%1$s\n%2$s</string>
<string name="menu_edit_widgets">Widgets bearbeiten</string>
<string name="widget_name_weather">Wetter</string>
@ -432,6 +425,14 @@
<string name="preference_music_filter_sources">Auf Musik-Apps begrenzen</string>
<string name="preference_music_filter_sources_summary">Mediensitzungen von Apps ignorieren, die keine Musik-Apps sind</string>
<string name="preference_screen_calendarwidget">Kalender</string>
<string name="preference_calendar_calendars">Kalender</string>
<string name="preference_calendar_hide_allday">Ganztägige Termine ausblenden</string>
<plurals name="preference_calendar_calendars_summary">
<item quantity="one">%1$d Kalender ausgewählt</item>
<item quantity="other">%1$d Kalender ausgewählt</item>
</plurals>
<string name="preference_screen_search">Suche</string>
<string name="preference_screen_search_summary">Konfigurieren was durchsucht werden soll</string>
<string name="preference_search_favorites">Favoriten</string>

View File

@ -196,11 +196,8 @@
<string name="preference_dynamic_icon_bg">Dynamic background</string>
<string name="preference_dynamic_icon_bg_summary">Adjust background color to icon</string>
<string name="preference_category_widget">Widget</string>
<string name="preference_screen_calendarwidget">Calendar</string>
<string name="preference_screen_calendar_summary">Calendars, Widget settings</string>
<string name="preference_calendar_calendars">Calendars</string>
<string name="preference_permission_denied">Permission denied</string>
<string name="preference_calendar_hide_allday">Hide all-day events</string>
<string name="menu_edit_widgets">Edit widgets</string>
<string name="widget_name_weather">Weather</string>
<string name="widget_name_calendar">Calendar</string>
@ -237,10 +234,6 @@
<string name="file_type_ebook">E-book</string>
<string name="file_type_drawing">Drawing</string>
<string name="file_type_form">Form</string>
<plurals name="preference_calendar_calendars_summary">
<item quantity="one">%1$d calendar selected</item>
<item quantity="other">%1$d calendars selected</item>
</plurals>
<string name="menu_hide">Hide</string>
<string name="menu_unhide">Don\'t hide</string>
<string name="menu_hidden_items">Hidden items</string>
@ -488,6 +481,14 @@
<string name="preference_search_websearch">Web search</string>
<string name="preference_search_websearch_summary">Show shortcuts to different search engines</string>
<string name="preference_screen_calendarwidget">Calendar</string>
<string name="preference_calendar_calendars">Calendars</string>
<string name="preference_calendar_hide_allday">Hide all-day events</string>
<plurals name="preference_calendar_calendars_summary">
<item quantity="one">%1$d calendar selected</item>
<item quantity="other">%1$d calendars selected</item>
</plurals>
<string name="preference_music_filter_sources">Restrict to music apps</string>
<string name="preference_music_filter_sources_summary">Ignore media sessions of apps that are not music apps</string>

View File

@ -21,6 +21,10 @@ fun createFactorySettings(context: Context): Settings {
.setFilterSources(true)
.build()
)
.setCalendarWidget(Settings.CalendarWidgetSettings
.newBuilder()
.setHideAlldayEvents(false)
)
.setClockWidget(Settings.ClockWidgetSettings
.newBuilder()
.setLayout(Settings.ClockWidgetSettings.ClockWidgetLayout.Vertical)

View File

@ -103,4 +103,10 @@ message Settings {
}
WebSearchSettings web_search = 16;
message CalendarWidgetSettings {
bool hide_allday_events = 1;
repeated int64 exclude_calendars = 2;
}
CalendarWidgetSettings calendar_widget = 17;
}

View File

@ -23,6 +23,7 @@ import de.mm20.launcher2.ui.base.BaseActivity
import de.mm20.launcher2.ui.locals.LocalNavController
import de.mm20.launcher2.ui.settings.about.AboutSettingsScreen
import de.mm20.launcher2.ui.settings.appearance.AppearanceSettingsScreen
import de.mm20.launcher2.ui.settings.calendarwidget.CalendarWidgetSettingsScreen
import de.mm20.launcher2.ui.settings.clockwidget.ClockWidgetSettingsScreen
import de.mm20.launcher2.ui.settings.debug.DebugSettingsScreen
import de.mm20.launcher2.ui.settings.license.LicenseScreen
@ -98,6 +99,9 @@ class SettingsActivity : BaseActivity() {
composable("settings/widgets/music") {
MusicWidgetSettingsScreen()
}
composable("settings/widgets/calendar") {
CalendarWidgetSettingsScreen()
}
composable("settings/widgets/clock") {
ClockWidgetSettingsScreen()
}

View File

@ -0,0 +1,155 @@
package de.mm20.launcher2.ui.settings.calendarwidget
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.*
import androidx.compose.runtime.*
import androidx.compose.runtime.livedata.observeAsState
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import androidx.compose.ui.window.Dialog
import androidx.lifecycle.viewmodel.compose.viewModel
import de.mm20.launcher2.search.data.UserCalendar
import de.mm20.launcher2.ui.R
import de.mm20.launcher2.ui.component.preferences.Preference
import de.mm20.launcher2.ui.component.preferences.PreferenceCategory
import de.mm20.launcher2.ui.component.preferences.PreferenceScreen
import de.mm20.launcher2.ui.component.preferences.SwitchPreference
import de.mm20.launcher2.ui.pluralResource
@Composable
fun CalendarWidgetSettingsScreen() {
val viewModel: CalendarWidgetSettingsScreenVM = viewModel()
PreferenceScreen(title = stringResource(R.string.preference_screen_calendarwidget)) {
item {
val excludeAllDayEvents by viewModel.excludeAllDayEvents.observeAsState()
PreferenceCategory {
SwitchPreference(
title = stringResource(R.string.preference_calendar_hide_allday),
value = excludeAllDayEvents == true,
onValueChanged = {
viewModel.setExcludeAllDayEvents(it)
}
)
val calendars by viewModel.calendars.observeAsState(emptyList())
val unselectedCalendars by viewModel.unselectedCalendars.observeAsState(emptyList())
ExcludedCalendarsPreference(
calendars = calendars,
value = unselectedCalendars,
onValueChanged = {
viewModel.setUnselectedCalendars(it)
}
)
}
}
}
}
@Composable
fun ExcludedCalendarsPreference(
calendars: List<UserCalendar>,
value: List<Long>,
onValueChanged: (List<Long>) -> Unit
) {
var showDialog by remember { mutableStateOf(false) }
Preference(
title = stringResource(R.string.preference_calendar_calendars),
summary = pluralResource(
R.plurals.preference_calendar_calendars_summary,
quantity = calendars.size - value.size,
calendars.size - value.size
),
onClick = {
showDialog = true
}
)
if (showDialog) {
Dialog(
onDismissRequest = { showDialog = false },
) {
Surface(
modifier = Modifier
.fillMaxWidth()
.heightIn(max = 400.dp),
shape = RoundedCornerShape(16.dp),
tonalElevation = 16.dp,
shadowElevation = 16.dp,
) {
Column {
Text(
text = stringResource(R.string.preference_calendar_calendars),
style = MaterialTheme.typography.titleLarge,
modifier = Modifier
.fillMaxWidth()
.padding(
start = 24.dp, end = 24.dp, top = 16.dp, bottom = 8.dp
)
)
LazyColumn(
modifier = Modifier
.weight(1f)
.padding(horizontal = 16.dp)
) {
items(calendars) { c ->
Row(
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier
.fillMaxWidth()
.clickable {
if (value.contains(c.id)) {
onValueChanged(
value.filter { it != c.id }
)
} else {
onValueChanged(
value + c.id
)
}
}
) {
Checkbox(
checked = !value.contains(c.id),
onCheckedChange = {
if (it) {
onValueChanged(
value.filter { it != c.id }
)
} else {
onValueChanged(
value + c.id
)
}
},
colors = CheckboxDefaults.colors(
checkedColor = Color(c.color)
)
)
Text(text = c.name)
}
}
}
TextButton(
onClick = {
onValueChanged(value.toList())
showDialog = false
},
modifier = Modifier
.align(Alignment.End)
.padding(vertical = 12.dp, horizontal = 16.dp)
) {
Text(
text = stringResource(android.R.string.ok),
style = MaterialTheme.typography.labelLarge
)
}
}
}
}
}
}

View File

@ -0,0 +1,52 @@
package de.mm20.launcher2.ui.settings.calendarwidget
import androidx.lifecycle.ViewModel
import androidx.lifecycle.asLiveData
import androidx.lifecycle.liveData
import androidx.lifecycle.viewModelScope
import de.mm20.launcher2.calendar.CalendarRepository
import de.mm20.launcher2.preferences.LauncherDataStore
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.launch
import org.koin.core.component.KoinComponent
import org.koin.core.component.inject
class CalendarWidgetSettingsScreenVM : ViewModel(), KoinComponent {
private val dataStore: LauncherDataStore by inject()
private val calendarRepository: CalendarRepository by inject()
val excludeAllDayEvents = dataStore.data.map { it.calendarWidget.hideAlldayEvents }.asLiveData()
fun setExcludeAllDayEvents(excludeAllDayEvents: Boolean) {
viewModelScope.launch {
dataStore.updateData {
it.toBuilder()
.setCalendarWidget(
it.calendarWidget
.toBuilder()
.setHideAlldayEvents(excludeAllDayEvents)
)
.build()
}
}
}
val calendars = liveData {
emit(calendarRepository.getCalendars())
}
val unselectedCalendars =
dataStore.data.map { it.calendarWidget.excludeCalendarsList }.asLiveData()
fun setUnselectedCalendars(unselectedCalendars: List<Long>) {
viewModelScope.launch {
dataStore.updateData {
it.toBuilder()
.setCalendarWidget(
it.calendarWidget.toBuilder()
.clearExcludeCalendars()
.addAllExcludeCalendars(unselectedCalendars)
)
.build()
}
}
}
}

View File

@ -40,7 +40,10 @@ fun WidgetsSettingsScreen() {
)
Preference(
title = stringResource(R.string.preference_screen_calendarwidget),
icon = Icons.Rounded.Today
icon = Icons.Rounded.Today,
onClick = {
navController?.navigate("settings/widgets/calendar")
}
)
}
}