You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
tasks/app/src/main/java/org/tasks/preferences/Preferences.kt

594 lines
21 KiB
Kotlin

package org.tasks.preferences
import android.annotation.SuppressLint
import android.content.ContentResolver
import android.content.Context
import android.content.Intent
import android.content.SharedPreferences
import android.content.pm.PackageManager
import android.media.RingtoneManager
import android.net.Uri
import android.os.Binder
import androidx.core.app.NotificationCompat
import androidx.core.net.toUri
import androidx.documentfile.provider.DocumentFile
import androidx.preference.PreferenceManager
import com.todoroo.andlib.utility.AndroidUtilities
import com.todoroo.andlib.utility.DateUtilities
import com.todoroo.andlib.utility.DateUtilities.now
import com.todoroo.astrid.activity.BeastModePreferences
import com.todoroo.astrid.core.SortHelper
import com.todoroo.astrid.data.Task
import com.todoroo.astrid.data.Task.Companion.NOTIFY_AFTER_DEADLINE
import com.todoroo.astrid.data.Task.Companion.NOTIFY_AT_DEADLINE
import org.tasks.BuildConfig
import org.tasks.R
import org.tasks.Strings.isNullOrEmpty
import org.tasks.billing.Purchase
import org.tasks.data.TaskAttachment
import org.tasks.extensions.Context.getResourceUri
import org.tasks.themes.ColorProvider
import org.tasks.themes.ThemeBase
import org.tasks.time.DateTime
import timber.log.Timber
import java.io.File
import java.net.URI
import java.util.concurrent.TimeUnit
class Preferences @JvmOverloads constructor(
private val context: Context,
name: String? = getSharedPreferencesName(context)
) : QueryPreferences {
private val prefs: SharedPreferences = context.getSharedPreferences(name, Context.MODE_PRIVATE)
fun androidBackupServiceEnabled() = getBoolean(R.string.p_backups_android_backup_enabled, true)
fun showBackupWarnings() = !getBoolean(R.string.p_backups_ignore_warnings, false)
fun addTasksToTop(): Boolean = getBoolean(R.string.p_add_to_top, true)
fun backButtonSavesTask(): Boolean = getBoolean(R.string.p_back_button_saves_task, false)
val isCurrentlyQuietHours: Boolean
get() {
if (quietHoursEnabled()) {
val dateTime = DateTime()
val start = dateTime.withMillisOfDay(quietHoursStart)
val end = dateTime.withMillisOfDay(quietHoursEnd)
return if (start.isAfter(end)) {
dateTime.isBefore(end) || dateTime.isAfter(start)
} else {
dateTime.isAfter(start) && dateTime.isBefore(end)
}
}
return false
}
fun adjustForQuietHours(time: Long): Long {
if (quietHoursEnabled()) {
val dateTime = DateTime(time)
val start = dateTime.withMillisOfDay(quietHoursStart)
val end = dateTime.withMillisOfDay(quietHoursEnd)
if (start.isAfter(end)) {
if (dateTime.isBefore(end)) {
return end.millis
} else if (dateTime.isAfter(start)) {
return end.plusDays(1).millis
}
} else {
if (dateTime.isAfter(start) && dateTime.isBefore(end)) {
return end.millis
}
}
}
return time
}
private fun quietHoursEnabled(): Boolean = getBoolean(R.string.p_rmd_enable_quiet, false)
val isDefaultDueTimeEnabled: Boolean
get() = getBoolean(R.string.p_rmd_time_enabled, true)
val defaultDueTime: Int
get() = getInt(R.string.p_rmd_time, TimeUnit.HOURS.toMillis(18).toInt())
private val quietHoursStart: Int
get() = getMillisPerDayPref(R.string.p_rmd_quietStart, R.integer.default_quiet_hours_start)
private val quietHoursEnd: Int
get() = getMillisPerDayPref(R.string.p_rmd_quietEnd, R.integer.default_quiet_hours_end)
val dateShortcutMorning: Int
get() = getMillisPerDayPref(R.string.p_date_shortcut_morning, R.integer.default_morning)
val dateShortcutAfternoon: Int
get() = getMillisPerDayPref(R.string.p_date_shortcut_afternoon, R.integer.default_afternoon)
val dateShortcutEvening: Int
get() = getMillisPerDayPref(R.string.p_date_shortcut_evening, R.integer.default_evening)
val purchases: List<Purchase>
get() = try {
getStringSet(R.string.p_purchases).map(::Purchase)
} catch (e: Exception) {
Timber.e(e)
emptyList()
}
fun setPurchases(purchases: Collection<Purchase>) {
setPurchases(purchases.map(Purchase::toJson).toHashSet())
}
private fun setPurchases(set: HashSet<String>) {
try {
setStringSet(R.string.p_purchases, set)
} catch (e: Exception) {
Timber.e(e)
}
}
val dateShortcutNight: Int
get() = getMillisPerDayPref(R.string.p_date_shortcut_night, R.integer.default_night)
private fun getMillisPerDayPref(resId: Int, defResId: Int): Int {
val setting = getInt(resId, -1)
return if (setting < 0 || setting > DateTime.MAX_MILLIS_PER_DAY) {
context.resources.getInteger(defResId)
} else setting
}
val isDefaultCalendarSet: Boolean
get() {
val defaultCalendar = defaultCalendar
return defaultCalendar != null && defaultCalendar != "-1" && defaultCalendar != "0"
}
val ringtone: Uri?
get() = getRingtone(
R.string.p_rmd_ringtone,
RingtoneManager.getDefaultUri(RingtoneManager.TYPE_NOTIFICATION)
)
val completionSound: Uri?
get() = getRingtone(
R.string.p_completion_ringtone,
context.getResourceUri(R.raw.long_rising_tone)
)
private fun getRingtone(pref: Int, default: Uri): Uri? {
val ringtone = getStringValue(pref)
return when {
ringtone == null -> default
ringtone.isNotBlank() -> ringtone.toUri()
else -> null
}
}
val isTrackingEnabled: Boolean
get() = getBoolean(R.string.p_collect_statistics, true)
val defaultCalendar: String?
get() = getStringValue(R.string.gcal_p_default)
val firstDayOfWeek: Int
get() {
val firstDayOfWeek = getIntegerFromString(R.string.p_start_of_week, 0)
return if (firstDayOfWeek < 1 || firstDayOfWeek > 7) 0 else firstDayOfWeek
}
val showEditScreenWithoutUnlock: Boolean
get() = getBoolean(R.string.p_show_edit_screen_without_unlock, false)
@SuppressLint("ApplySharedPref")
fun clear() {
prefs.edit().clear().commit()
}
fun setDefaults() {
PreferenceManager.setDefaultValues(context, R.xml.preferences, true)
PreferenceManager.setDefaultValues(context, R.xml.preferences_look_and_feel, true)
PreferenceManager.setDefaultValues(context, R.xml.preferences_notifications, true)
PreferenceManager.setDefaultValues(context, R.xml.preferences_task_defaults, true)
PreferenceManager.setDefaultValues(context, R.xml.preferences_date_and_time, true)
PreferenceManager.setDefaultValues(context, R.xml.preferences_navigation_drawer, true)
PreferenceManager.setDefaultValues(context, R.xml.preferences_backups, true)
PreferenceManager.setDefaultValues(context, R.xml.preferences_advanced, true)
PreferenceManager.setDefaultValues(context, R.xml.help_and_feedback, true)
BeastModePreferences.setDefaultOrder(this, context)
}
fun reset() {
clear()
setDefaults()
}
fun getStringValue(keyResource: Int): String? = getStringValue(context.getString(keyResource))
fun getStringValue(key: String?): String? = try {
prefs.getString(key, null)
} catch (e: Exception) {
Timber.e(e)
null
}
val defaultRemindersSet: Set<String>
get() = getStringSet(
R.string.p_default_reminders_key,
hashSetOf(NOTIFY_AT_DEADLINE.toString(), NOTIFY_AFTER_DEADLINE.toString())
)
val defaultReminders: Int
get() = defaultRemindersSet
.mapNotNull { it.toIntOrNull() }
.sum()
val defaultRingMode: Int
get() = getIntegerFromString(R.string.p_default_reminders_mode_key, 0)
val fontSize: Int
get() = getInt(R.string.p_fontSize, 16)
val mapTheme: Int
get() = getIntegerFromString(R.string.p_map_theme, 0)
fun getIntegerFromString(keyResource: Int, defaultValue: Int): Int =
getIntegerFromString(context.getString(keyResource), defaultValue)
fun getIntegerFromString(keyResource: String?, defaultValue: Int): Int =
getStringValue(keyResource)?.toIntOrNull() ?: defaultValue
private fun getUri(key: Int): Uri? {
val uri = getStringValue(key)
return if (isNullOrEmpty(uri)) null else Uri.parse(uri)
}
fun setUri(key: Int, uri: URI) {
setString(key, uri.toString())
}
fun setUri(key: Int, uri: Uri) {
setString(key, uri.toString())
}
fun setString(key: Int, newValue: String?) {
setString(context.getString(key), newValue)
}
fun setString(key: String?, newValue: String?) {
val editor = prefs.edit()
editor.putString(key, newValue)
editor.apply()
}
fun setStringSet(key: Int, newValue: Set<String>) =
setStringSet(context.getString(key), newValue)
fun setStringSet(key: String, newValue: Set<String>) {
val editor = prefs.edit()
editor.putStringSet(key, newValue)
editor.apply()
}
private fun getStringSet(key: Int, defaultValue: Set<String> = emptySet()) =
getStringSet(context.getString(key), defaultValue)
private fun getStringSet(key: String, defaultValue: Set<String> = emptySet()): Set<String> =
prefs.getStringSet(key, defaultValue)!!
fun setStringFromInteger(keyResource: Int, newValue: Int) {
val editor = prefs.edit()
editor.putString(context.getString(keyResource), newValue.toString())
editor.apply()
}
fun getBoolean(key: String?, defValue: Boolean): Boolean = try {
prefs.getBoolean(key, defValue)
} catch (e: ClassCastException) {
Timber.w(e)
defValue
}
fun getBoolean(keyResources: Int, defValue: Boolean): Boolean =
getBoolean(context.getString(keyResources), defValue)
fun setBoolean(keyResource: Int, value: Boolean) {
setBoolean(context.getString(keyResource), value)
}
fun setBoolean(key: String?, value: Boolean) {
val editor = prefs.edit()
editor.putBoolean(key, value)
editor.apply()
}
fun getInt(resourceId: Int, defValue: Int): Int =
getInt(context.getString(resourceId), defValue)
fun getInt(key: String?, defValue: Int): Int = prefs.getInt(key, defValue)
fun setInt(resourceId: Int, value: Int) {
setInt(context.getString(resourceId), value)
}
fun setInt(key: String?, value: Int) {
val editor = prefs.edit()
editor.putInt(key, value)
editor.apply()
}
fun getLong(resourceId: Int, defValue: Long): Long =
getLong(context.getString(resourceId), defValue)
fun getLong(key: String?, defValue: Long): Long = prefs.getLong(key, defValue)
fun setLong(resourceId: Int, value: Long) {
setLong(context.getString(resourceId), value)
}
fun setLong(key: String?, value: Long) {
val editor = prefs.edit()
editor.putLong(key, value)
editor.apply()
}
fun clear(key: String?) {
val editor = prefs.edit()
editor.remove(key)
editor.apply()
}
val lastSetVersion: Int
get() = getInt(R.string.p_current_version, 0)
fun setCurrentVersion(version: Int) {
setInt(R.string.p_current_version, version)
}
var installVersion: Int
get() = getInt(R.string.p_install_version, 0)
set(value) = setInt(R.string.p_install_version, value)
var installDate: Long
get() = getLong(R.string.p_install_date, 0L)
set(value) = setLong(R.string.p_install_date, value)
override var sortMode: Int
get() = getInt(R.string.p_sort_mode, SortHelper.SORT_AUTO)
set(value) { setInt(R.string.p_sort_mode, value) }
override var groupMode: Int
get() = getInt(R.string.p_group_mode, SortHelper.GROUP_NONE)
set(value) { setInt(R.string.p_group_mode, value) }
override var completedMode: Int
get() = getInt(R.string.p_completed_mode, SortHelper.SORT_COMPLETED)
set(value) { setInt(R.string.p_completed_mode, value) }
override var subtaskMode: Int
get() = getInt(R.string.p_subtask_mode, SortHelper.SORT_MANUAL)
set(value) { setInt(R.string.p_subtask_mode, value) }
override var showHidden: Boolean
get() = getBoolean(R.string.p_show_hidden_tasks, true)
set(value) { setBoolean(R.string.p_show_hidden_tasks, value) }
override var showCompleted: Boolean
get() = getBoolean(R.string.p_show_completed_tasks, true)
set(value) { setBoolean(R.string.p_show_completed_tasks, value) }
override var alwaysDisplayFullDate: Boolean
get() = getBoolean(R.string.p_always_display_full_date, false)
set(value) { setBoolean(R.string.p_always_display_full_date, value)}
override var completedTasksAtBottom: Boolean
get() = getBoolean(R.string.p_completed_tasks_at_bottom, true)
set(value) { setBoolean(R.string.p_completed_tasks_at_bottom, value) }
val backupDirectory: Uri?
get() = getDirectory(R.string.p_backup_dir, "backups")
val externalStorage: Uri
get() = root.uri
val attachmentsDirectory: Uri?
get() = getDirectory(R.string.p_attachment_dir, TaskAttachment.FILES_DIRECTORY_DEFAULT)
private fun getDirectory(pref: Int, name: String): Uri? {
val uri = getUri(pref)
if (uri != null) {
when (uri.scheme) {
ContentResolver.SCHEME_FILE -> {
val file = File(uri.path)
try {
if (file.canWrite()) {
return uri
}
} catch (ignored: SecurityException) {
}
}
ContentResolver.SCHEME_CONTENT -> if (hasWritePermission(context, uri)) {
return uri
}
}
}
return getDefaultDirectory(name)
}
private fun getDefaultDirectory(name: String): Uri? =
root
.createDirectory(name)
?.uri
?: getDefaultFileLocation(name)?.let { Uri.fromFile(it) }
private val root: DocumentFile
get() = DocumentFile.fromFile(context.getExternalFilesDir(null)!!)
private fun getDefaultFileLocation(type: String): File? {
val externalFilesDir = context.getExternalFilesDir(null) ?: return null
val path = String.format("%s/%s", externalFilesDir.absolutePath, type)
val file = File(path)
return if (file.isDirectory || file.mkdirs()) file else null
}
val cacheDirectory: Uri
get() {
var cacheDir = context.externalCacheDir
if (cacheDir == null) {
cacheDir = context.cacheDir
}
return DocumentFile.fromFile(cacheDir!!).uri
}
private fun hasWritePermission(context: Context, uri: Uri): Boolean =
(PackageManager.PERMISSION_GRANTED
== context.checkUriPermission(
uri,
Binder.getCallingPid(),
Binder.getCallingUid(),
Intent.FLAG_GRANT_READ_URI_PERMISSION or Intent.FLAG_GRANT_WRITE_URI_PERMISSION))
val notificationDefaults: Int
get() {
var result = 0
if (getBoolean(R.string.p_rmd_vibrate, true)) {
result = result or NotificationCompat.DEFAULT_VIBRATE
}
if (getBoolean(R.string.p_led_notification, true)) {
result = result or NotificationCompat.DEFAULT_LIGHTS
}
return result
}
fun remove(resId: Int) {
val editor = prefs.edit()
editor.remove(context.getString(resId))
editor.apply()
}
fun bundleNotifications(): Boolean = getBoolean(R.string.p_bundle_notifications, true)
fun usePersistentReminders(): Boolean =
AndroidUtilities.preUpsideDownCake() && getBoolean(R.string.p_rmd_persistent, true)
fun useSwipeToSnooze(): Boolean =
getBoolean(R.string.p_rmd_swipe_to_snooze_enabled, false)
fun swipeToSnoozeIntervalMS(): Long =
getIntegerFromString(R.string.p_rmd_swipe_to_snooze_time_minutes, 0).toLong()
var isSyncOngoing: Boolean
get() = syncFlags.any { getBoolean(it, false) }
set(value) {
syncFlags.forEach { setBoolean(it, value) }
}
var lastSync: Long
get() = getLong(R.string.p_last_sync, 0L)
set(value) {
setLong(R.string.p_last_sync, value)
}
fun <T> getPrefs(c: Class<T>): Map<String, T> =
prefs.all.filter { (_, value) -> c.isInstance(value) } as Map<String, T>
val isFlipperEnabled: Boolean
get() = BuildConfig.DEBUG && getBoolean(R.string.p_flipper, false)
var isPositionHackEnabled: Boolean
get() = getLong(R.string.p_google_tasks_position_hack, 0) > now() - DateUtilities.ONE_WEEK
set(value) { setLong(R.string.p_google_tasks_position_hack, if (value) now() else 0) }
override var isManualSort: Boolean
get() = getBoolean(R.string.p_manual_sort, false)
set(value) { setBoolean(R.string.p_manual_sort, value) }
val isAstridSortEnabled: Boolean
get() = getBoolean(R.string.p_astrid_sort_enabled, false)
override var isAstridSort: Boolean
get() = isAstridSortEnabled && getBoolean(R.string.p_astrid_sort, false)
set(value) {
setBoolean(R.string.p_astrid_sort, value)
}
override var sortAscending: Boolean
get() = getBoolean(R.string.p_sort_ascending, true)
set(value) { setBoolean(R.string.p_sort_ascending, value) }
override var groupAscending: Boolean
get() = getBoolean(R.string.p_group_ascending, false)
set(value) { setBoolean(R.string.p_group_ascending, value) }
override var completedAscending: Boolean
get() = getBoolean(R.string.p_completed_ascending, false)
set(value) { setBoolean(R.string.p_completed_ascending, value) }
override var subtaskAscending: Boolean
get() = getBoolean(R.string.p_subtask_ascending, false)
set(value) { setBoolean(R.string.p_subtask_ascending, value) }
val defaultPriority: Int
get() = getIntegerFromString(R.string.p_default_importance_key, Task.Priority.LOW)
val themeBase: Int
get() = getInt(R.string.p_theme, ThemeBase.DEFAULT_BASE_THEME)
val showSubtaskChip: Boolean
get() = getBoolean(R.string.p_subtask_chips, true)
val showStartDateChip: Boolean
get() = getBoolean(R.string.p_start_date_chip, true)
val showPlaceChip: Boolean
get() = getBoolean(R.string.p_place_chips, true)
val showListChip: Boolean
get() = getBoolean(R.string.p_list_chips, true)
val showTagChip: Boolean
get() = getBoolean(R.string.p_tag_chips, true)
fun alreadyNotified(account: String?, scope: String?): Boolean =
getBoolean(context.getString(R.string.p_notified_oauth_error, account, scope), false)
fun setAlreadyNotified(account: String?, scope: String?, value: Boolean) {
setBoolean(context.getString(R.string.p_notified_oauth_error, account, scope), value)
}
val defaultThemeColor: Int
get() = getInt(R.string.p_theme_color, ColorProvider.BLUE_500)
val markdown: Boolean
get() = getBoolean(R.string.p_markdown, false)
val isTopAppBar: Boolean
get() = getIntegerFromString(R.string.p_app_bar_position, 1) == 0
var lastReviewRequest: Long
get() = getLong(R.string.p_last_review_request, 0L)
set(value) = setLong(R.string.p_last_review_request, value)
var lastSubscribeRequest: Long
get() = getLong(R.string.p_last_subscribe_request, 0L)
set(value) = setLong(R.string.p_last_subscribe_request, value)
var shownBeastModeHint: Boolean
get() = getBoolean(R.string.p_shown_beast_mode_hint, false)
set(value) = setBoolean(R.string.p_shown_beast_mode_hint, value)
val desaturateDarkMode: Boolean
get() = getBoolean(R.string.p_desaturate_colors, true)
val linkify: Boolean
get() = getBoolean(R.string.p_linkify_task_edit, false)
companion object {
private fun getSharedPreferencesName(context: Context): String =
context.packageName + "_preferences"
private val syncFlags = listOf(
R.string.p_sync_ongoing,
R.string.p_sync_ongoing_android,
)
}
}