convert EmojiPreference and EmojiCompatFont to Kotlin (#1922)

* convert EmojiPreference and EmojiCompatFont to Kotlin

* move preference related to to dedicated preference package

* update proguard-rules.pro

* reformat & add comment

* maintain disposable information in EmojiPreference instead of EmojiCompatFont
This commit is contained in:
Konrad Pozniak 2020-09-02 12:27:51 +02:00 committed by GitHub
commit 1d309850b0
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
21 changed files with 709 additions and 906 deletions

View file

@ -0,0 +1,343 @@
/* Copyright 2018 Conny Duck
*
* This file is a part of Tusky.
*
* This program is free software; you can redistribute it and/or modify it under the terms of the
* GNU General Public License as published by the Free Software Foundation; either version 3 of the
* License, or (at your option) any later version.
*
* Tusky is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even
* the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General
* Public License for more details.
*
* You should have received a copy of the GNU General Public License along with Tusky; if not,
* see <http://www.gnu.org/licenses>. */
package com.keylesspalace.tusky.components.preference
import android.content.Intent
import android.graphics.drawable.Drawable
import android.os.Build
import android.os.Bundle
import android.util.Log
import androidx.preference.PreferenceFragmentCompat
import com.google.android.material.snackbar.Snackbar
import com.keylesspalace.tusky.*
import com.keylesspalace.tusky.appstore.EventHub
import com.keylesspalace.tusky.appstore.PreferenceChangedEvent
import com.keylesspalace.tusky.components.instancemute.InstanceListActivity
import com.keylesspalace.tusky.db.AccountEntity
import com.keylesspalace.tusky.db.AccountManager
import com.keylesspalace.tusky.di.Injectable
import com.keylesspalace.tusky.entity.Account
import com.keylesspalace.tusky.entity.Filter
import com.keylesspalace.tusky.entity.Status
import com.keylesspalace.tusky.network.MastodonApi
import com.keylesspalace.tusky.settings.*
import com.keylesspalace.tusky.util.ThemeUtils
import com.mikepenz.iconics.IconicsDrawable
import com.mikepenz.iconics.typeface.library.googlematerial.GoogleMaterial
import com.mikepenz.iconics.utils.colorInt
import com.mikepenz.iconics.utils.sizeRes
import retrofit2.Call
import retrofit2.Callback
import retrofit2.Response
import javax.inject.Inject
class AccountPreferencesFragment : PreferenceFragmentCompat(), Injectable {
@Inject
lateinit var accountManager: AccountManager
@Inject
lateinit var mastodonApi: MastodonApi
@Inject
lateinit var eventHub: EventHub
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
val context = requireContext()
makePreferenceScreen {
preference {
setTitle(R.string.pref_title_edit_notification_settings)
icon = IconicsDrawable(context, GoogleMaterial.Icon.gmd_notifications).apply {
sizeRes = R.dimen.preference_icon_size
colorInt = ThemeUtils.getColor(context, R.attr.iconColor)
}
setOnPreferenceClickListener {
openNotificationPrefs()
true
}
}
preference {
setTitle(R.string.title_tab_preferences)
icon = getTintedIcon(R.drawable.ic_tabs)
setOnPreferenceClickListener {
val intent = Intent(context, TabPreferenceActivity::class.java)
activity?.startActivity(intent)
activity?.overridePendingTransition(R.anim.slide_from_right,
R.anim.slide_to_left)
true
}
}
preference {
setTitle(R.string.action_view_mutes)
icon = getTintedIcon(R.drawable.ic_mute_24dp)
setOnPreferenceClickListener {
val intent = Intent(context, AccountListActivity::class.java)
intent.putExtra("type", AccountListActivity.Type.MUTES)
activity?.startActivity(intent)
activity?.overridePendingTransition(R.anim.slide_from_right,
R.anim.slide_to_left)
true
}
}
preference {
setTitle(R.string.action_view_blocks)
icon = IconicsDrawable(context, GoogleMaterial.Icon.gmd_block).apply {
sizeRes = R.dimen.preference_icon_size
colorInt = ThemeUtils.getColor(context, R.attr.iconColor)
}
setOnPreferenceClickListener {
val intent = Intent(context, AccountListActivity::class.java)
intent.putExtra("type", AccountListActivity.Type.BLOCKS)
activity?.startActivity(intent)
activity?.overridePendingTransition(R.anim.slide_from_right,
R.anim.slide_to_left)
true
}
}
preference {
setTitle(R.string.title_domain_mutes)
icon = getTintedIcon(R.drawable.ic_mute_24dp)
setOnPreferenceClickListener {
val intent = Intent(context, InstanceListActivity::class.java)
activity?.startActivity(intent)
activity?.overridePendingTransition(R.anim.slide_from_right,
R.anim.slide_to_left)
true
}
}
preferenceCategory(R.string.pref_publishing) {
listPreference {
setTitle(R.string.pref_default_post_privacy)
setEntries(R.array.post_privacy_names)
setEntryValues(R.array.post_privacy_values)
key = PrefKeys.DEFAULT_POST_PRIVACY
setSummaryProvider { entry }
val visibility = accountManager.activeAccount?.defaultPostPrivacy
?: Status.Visibility.PUBLIC
value = visibility.serverString()
icon = getIconForVisibility(visibility)
setOnPreferenceChangeListener { _, newValue ->
icon = getIconForVisibility(
Status.Visibility.byString(newValue as String)
)
syncWithServer(visibility = newValue)
eventHub.dispatch(PreferenceChangedEvent(key))
true
}
}
switchPreference {
setTitle(R.string.pref_default_media_sensitivity)
setIcon(R.drawable.ic_eye_24dp)
key = PrefKeys.DEFAULT_MEDIA_SENSITIVITY
isSingleLineTitle = false
val sensitivity = accountManager.activeAccount?.defaultMediaSensitivity
?: false
setDefaultValue(sensitivity)
icon = getIconForSensitivity(sensitivity)
setOnPreferenceChangeListener { _, newValue ->
icon = getIconForSensitivity(newValue as Boolean)
syncWithServer(sensitive = newValue)
eventHub.dispatch(PreferenceChangedEvent(key))
true
}
}
}
preferenceCategory(R.string.pref_title_timelines) {
switchPreference {
key = PrefKeys.MEDIA_PREVIEW_ENABLED
setTitle(R.string.pref_title_show_media_preview)
isSingleLineTitle = false
isChecked = accountManager.activeAccount?.mediaPreviewEnabled ?: true
setOnPreferenceChangeListener { _, newValue ->
updateAccount { it.mediaPreviewEnabled = newValue as Boolean }
eventHub.dispatch(PreferenceChangedEvent(key))
true
}
}
switchPreference {
key = PrefKeys.ALWAYS_SHOW_SENSITIVE_MEDIA
setTitle(R.string.pref_title_alway_show_sensitive_media)
isSingleLineTitle = false
isChecked = accountManager.activeAccount?.alwaysShowSensitiveMedia ?: false
setOnPreferenceChangeListener { _, newValue ->
updateAccount { it.alwaysShowSensitiveMedia = newValue as Boolean }
eventHub.dispatch(PreferenceChangedEvent(key))
true
}
}
switchPreference {
key = PrefKeys.ALWAYS_OPEN_SPOILER
setTitle(R.string.pref_title_alway_open_spoiler)
isSingleLineTitle = false
isChecked = accountManager.activeAccount?.alwaysOpenSpoiler ?: false
setOnPreferenceChangeListener { _, newValue ->
updateAccount { it.alwaysOpenSpoiler = newValue as Boolean }
eventHub.dispatch(PreferenceChangedEvent(key))
true
}
}
}
preferenceCategory(R.string.pref_title_timeline_filters) {
preference {
setTitle(R.string.pref_title_public_filter_keywords)
setOnPreferenceClickListener {
launchFilterActivity(Filter.PUBLIC,
R.string.pref_title_public_filter_keywords)
true
}
}
preference {
setTitle(R.string.title_notifications)
setOnPreferenceClickListener {
launchFilterActivity(Filter.NOTIFICATIONS, R.string.title_notifications)
true
}
}
preference {
setTitle(R.string.title_home)
setOnPreferenceClickListener {
launchFilterActivity(Filter.HOME, R.string.title_home)
true
}
}
preference {
setTitle(R.string.pref_title_thread_filter_keywords)
setOnPreferenceClickListener {
launchFilterActivity(Filter.THREAD,
R.string.pref_title_thread_filter_keywords)
true
}
}
preference {
setTitle(R.string.title_accounts)
setOnPreferenceClickListener {
launchFilterActivity(Filter.ACCOUNT, R.string.title_accounts)
true
}
}
}
}
}
private fun openNotificationPrefs() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
val intent = Intent()
intent.action = "android.settings.APP_NOTIFICATION_SETTINGS"
intent.putExtra("android.provider.extra.APP_PACKAGE", BuildConfig.APPLICATION_ID)
startActivity(intent)
} else {
activity?.let {
val intent = PreferencesActivity.newIntent(it, PreferencesActivity.NOTIFICATION_PREFERENCES)
it.startActivity(intent)
it.overridePendingTransition(R.anim.slide_from_right, R.anim.slide_to_left)
}
}
}
private inline fun updateAccount(changer: (AccountEntity) -> Unit) {
accountManager.activeAccount?.let { account ->
changer(account)
accountManager.saveAccount(account)
}
}
private fun syncWithServer(visibility: String? = null, sensitive: Boolean? = null) {
mastodonApi.accountUpdateSource(visibility, sensitive)
.enqueue(object : Callback<Account> {
override fun onResponse(call: Call<Account>, response: Response<Account>) {
val account = response.body()
if (response.isSuccessful && account != null) {
accountManager.activeAccount?.let {
it.defaultPostPrivacy = account.source?.privacy
?: Status.Visibility.PUBLIC
it.defaultMediaSensitivity = account.source?.sensitive ?: false
accountManager.saveAccount(it)
}
} else {
Log.e("AccountPreferences", "failed updating settings on server")
showErrorSnackbar(visibility, sensitive)
}
}
override fun onFailure(call: Call<Account>, t: Throwable) {
Log.e("AccountPreferences", "failed updating settings on server", t)
showErrorSnackbar(visibility, sensitive)
}
})
}
private fun showErrorSnackbar(visibility: String?, sensitive: Boolean?) {
view?.let { view ->
Snackbar.make(view, R.string.pref_failed_to_sync, Snackbar.LENGTH_LONG)
.setAction(R.string.action_retry) { syncWithServer(visibility, sensitive) }
.show()
}
}
private fun getIconForVisibility(visibility: Status.Visibility): Drawable? {
val drawableId = when (visibility) {
Status.Visibility.PRIVATE -> R.drawable.ic_lock_outline_24dp
Status.Visibility.UNLISTED -> R.drawable.ic_lock_open_24dp
else -> R.drawable.ic_public_24dp
}
return getTintedIcon(drawableId)
}
private fun getIconForSensitivity(sensitive: Boolean): Drawable? {
val drawableId = if (sensitive) {
R.drawable.ic_hide_media_24dp
} else {
R.drawable.ic_eye_24dp
}
return getTintedIcon(drawableId)
}
private fun getTintedIcon(iconId: Int): Drawable? {
return ThemeUtils.getTintedDrawable(requireContext(), iconId, R.attr.iconColor)
}
private fun launchFilterActivity(filterContext: String, titleResource: Int) {
val intent = Intent(context, FiltersActivity::class.java)
intent.putExtra(FiltersActivity.FILTERS_CONTEXT, filterContext)
intent.putExtra(FiltersActivity.FILTERS_TITLE, getString(titleResource))
activity?.startActivity(intent)
activity?.overridePendingTransition(R.anim.slide_from_right, R.anim.slide_to_left)
}
companion object {
fun newInstance() = AccountPreferencesFragment()
}
}

View file

@ -0,0 +1,258 @@
package com.keylesspalace.tusky.components.preference
import android.app.AlarmManager
import android.app.PendingIntent
import android.content.Context
import android.content.Intent
import android.os.Build
import android.util.Log
import android.view.LayoutInflater
import android.view.View
import android.widget.*
import androidx.appcompat.app.AlertDialog
import androidx.preference.Preference
import androidx.preference.PreferenceManager
import com.keylesspalace.tusky.R
import com.keylesspalace.tusky.SplashActivity
import com.keylesspalace.tusky.util.EmojiCompatFont
import com.keylesspalace.tusky.util.EmojiCompatFont.Companion.FONTS
import io.reactivex.android.schedulers.AndroidSchedulers
import io.reactivex.disposables.Disposable
import okhttp3.OkHttpClient
import kotlin.system.exitProcess
/**
* This Preference lets the user select their preferred emoji font
*/
class EmojiPreference(
context: Context,
private val okHttpClient: OkHttpClient
) : Preference(context) {
private lateinit var selected: EmojiCompatFont
private lateinit var original: EmojiCompatFont
private val radioButtons = mutableListOf<RadioButton>()
private var updated = false
private var currentNeedsUpdate = false
private val downloadDisposables = MutableList<Disposable?>(FONTS.size) { null }
override fun onAttachedToHierarchy(preferenceManager: PreferenceManager) {
super.onAttachedToHierarchy(preferenceManager)
// Find out which font is currently active
selected = EmojiCompatFont.byId(
PreferenceManager.getDefaultSharedPreferences(context).getInt(key, 0)
)
// We'll use this later to determine if anything has changed
original = selected
summary = selected.getDisplay(context)
}
override fun onClick() {
val view = LayoutInflater.from(context).inflate(R.layout.dialog_emojicompat, null)
viewIds.forEachIndexed { index, viewId ->
setupItem(view.findViewById(viewId), FONTS[index])
}
AlertDialog.Builder(context)
.setView(view)
.setPositiveButton(android.R.string.ok) { _, _ -> onDialogOk() }
.setNegativeButton(android.R.string.cancel, null)
.show()
}
private fun setupItem(container: View, font: EmojiCompatFont) {
val title: TextView = container.findViewById(R.id.emojicompat_name)
val caption: TextView = container.findViewById(R.id.emojicompat_caption)
val thumb: ImageView = container.findViewById(R.id.emojicompat_thumb)
val download: ImageButton = container.findViewById(R.id.emojicompat_download)
val cancel: ImageButton = container.findViewById(R.id.emojicompat_download_cancel)
val radio: RadioButton = container.findViewById(R.id.emojicompat_radio)
// Initialize all the views
title.text = font.getDisplay(container.context)
caption.setText(font.caption)
thumb.setImageResource(font.img)
// There needs to be a list of all the radio buttons in order to uncheck them when one is selected
radioButtons.add(radio)
updateItem(font, container)
// Set actions
download.setOnClickListener { startDownload(font, container) }
cancel.setOnClickListener { cancelDownload(font, container) }
radio.setOnClickListener { radioButton: View -> select(font, radioButton as RadioButton) }
container.setOnClickListener { containerView: View ->
select(font, containerView.findViewById(R.id.emojicompat_radio))
}
}
private fun startDownload(font: EmojiCompatFont, container: View) {
val download: ImageButton = container.findViewById(R.id.emojicompat_download)
val caption: TextView = container.findViewById(R.id.emojicompat_caption)
val progressBar: ProgressBar = container.findViewById(R.id.emojicompat_progress)
val cancel: ImageButton = container.findViewById(R.id.emojicompat_download_cancel)
// Switch to downloading style
download.visibility = View.GONE
caption.visibility = View.INVISIBLE
progressBar.visibility = View.VISIBLE
progressBar.progress = 0
cancel.visibility = View.VISIBLE
font.downloadFontFile(context, okHttpClient)
.observeOn(AndroidSchedulers.mainThread())
.subscribe(
{ progress ->
// The progress is returned as a float between 0 and 1, or -1 if it could not determined
if (progress >= 0) {
progressBar.isIndeterminate = false
val max = progressBar.max.toFloat()
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
progressBar.setProgress((max * progress).toInt(), true)
} else {
progressBar.progress = (max * progress).toInt()
}
} else {
progressBar.isIndeterminate = true
}
},
{
Toast.makeText(context, R.string.download_failed, Toast.LENGTH_SHORT).show()
updateItem(font, container)
},
{
finishDownload(font, container)
}
).also { downloadDisposables[font.id] = it }
}
private fun cancelDownload(font: EmojiCompatFont, container: View) {
font.deleteDownloadedFile(container.context)
downloadDisposables[font.id]?.dispose()
downloadDisposables[font.id] = null
updateItem(font, container)
}
private fun finishDownload(font: EmojiCompatFont, container: View) {
select(font, container.findViewById(R.id.emojicompat_radio))
updateItem(font, container)
// Set the flag to restart the app (because an update has been downloaded)
if (selected === original && currentNeedsUpdate) {
updated = true
currentNeedsUpdate = false
}
}
/**
* Select a font both visually and logically
*
* @param font The font to be selected
* @param radio The radio button associated with it's visual item
*/
private fun select(font: EmojiCompatFont, radio: RadioButton) {
selected = font
// Uncheck all the other buttons
for (other in radioButtons) {
if (other !== radio) {
other.isChecked = false
}
}
radio.isChecked = true
}
/**
* Called when a "consistent" state is reached, i.e. it's not downloading the font
*
* @param font The font to be displayed
* @param container The ConstraintLayout containing the item
*/
private fun updateItem(font: EmojiCompatFont, container: View) {
// Assignments
val download: ImageButton = container.findViewById(R.id.emojicompat_download)
val caption: TextView = container.findViewById(R.id.emojicompat_caption)
val progress: ProgressBar = container.findViewById(R.id.emojicompat_progress)
val cancel: ImageButton = container.findViewById(R.id.emojicompat_download_cancel)
val radio: RadioButton = container.findViewById(R.id.emojicompat_radio)
// There's no download going on
progress.visibility = View.GONE
cancel.visibility = View.GONE
caption.visibility = View.VISIBLE
if (font.isDownloaded(context)) {
// Make it selectable
download.visibility = View.GONE
radio.visibility = View.VISIBLE
container.isClickable = true
} else {
// Make it downloadable
download.visibility = View.VISIBLE
radio.visibility = View.GONE
container.isClickable = false
}
// Select it if necessary
if (font === selected) {
radio.isChecked = true
// Update available
if (!font.isDownloaded(context)) {
currentNeedsUpdate = true
}
} else {
radio.isChecked = false
}
}
private fun saveSelectedFont() {
val index = selected.id
Log.i(TAG, "saveSelectedFont: Font ID: $index")
PreferenceManager
.getDefaultSharedPreferences(context)
.edit()
.putInt(key, index)
.apply()
summary = selected.getDisplay(context)
}
/**
* User clicked ok -> save the selected font and offer to restart the app if something changed
*/
private fun onDialogOk() {
saveSelectedFont()
if (selected !== original || updated) {
AlertDialog.Builder(context)
.setTitle(R.string.restart_required)
.setMessage(R.string.restart_emoji)
.setNegativeButton(R.string.later, null)
.setPositiveButton(R.string.restart) { _, _ ->
// Restart the app
// From https://stackoverflow.com/a/17166729/5070653
val launchIntent = Intent(context, SplashActivity::class.java)
val mPendingIntent = PendingIntent.getActivity(
context,
0x1f973, // This is the codepoint of the party face emoji :D
launchIntent,
PendingIntent.FLAG_CANCEL_CURRENT)
val mgr = context.getSystemService(Context.ALARM_SERVICE) as AlarmManager
mgr.set(
AlarmManager.RTC,
System.currentTimeMillis() + 100,
mPendingIntent)
exitProcess(0)
}.show()
}
}
companion object {
private const val TAG = "EmojiPreference"
// Please note that this array must sorted in the same way as the fonts.
private val viewIds = intArrayOf(
R.id.item_nomoji,
R.id.item_blobmoji,
R.id.item_twemoji,
R.id.item_notoemoji
)
}
}

View file

@ -0,0 +1,169 @@
/* Copyright 2018 Conny Duck
*
* This file is a part of Tusky.
*
* This program is free software; you can redistribute it and/or modify it under the terms of the
* GNU General Public License as published by the Free Software Foundation; either version 3 of the
* License, or (at your option) any later version.
*
* Tusky is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even
* the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General
* Public License for more details.
*
* You should have received a copy of the GNU General Public License along with Tusky; if not,
* see <http://www.gnu.org/licenses>. */
package com.keylesspalace.tusky.components.preference
import android.os.Bundle
import androidx.preference.PreferenceFragmentCompat
import com.keylesspalace.tusky.R
import com.keylesspalace.tusky.components.notifications.NotificationHelper
import com.keylesspalace.tusky.db.AccountEntity
import com.keylesspalace.tusky.db.AccountManager
import com.keylesspalace.tusky.di.Injectable
import com.keylesspalace.tusky.settings.PrefKeys
import com.keylesspalace.tusky.settings.makePreferenceScreen
import com.keylesspalace.tusky.settings.preferenceCategory
import com.keylesspalace.tusky.settings.switchPreference
import javax.inject.Inject
class NotificationPreferencesFragment : PreferenceFragmentCompat(), Injectable {
@Inject
lateinit var accountManager: AccountManager
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
val activeAccount = accountManager.activeAccount ?: return
val context = requireContext()
makePreferenceScreen {
switchPreference {
setTitle(R.string.pref_title_notifications_enabled)
key = PrefKeys.NOTIFICATIONS_ENABLED
isIconSpaceReserved = false
isChecked = activeAccount.notificationsEnabled
setOnPreferenceChangeListener { _, newValue ->
updateAccount { it.notificationsEnabled = newValue as Boolean }
if (NotificationHelper.areNotificationsEnabled(context, accountManager)) {
NotificationHelper.enablePullNotifications(context)
} else {
NotificationHelper.disablePullNotifications(context)
}
true
}
}
preferenceCategory(R.string.pref_title_notification_filters) { category ->
category.dependency = PrefKeys.NOTIFICATIONS_ENABLED
category.isIconSpaceReserved = false
switchPreference {
setTitle(R.string.pref_title_notification_filter_follows)
key = PrefKeys.NOTIFICATIONS_FILTER_FOLLOWS
isIconSpaceReserved = false
isChecked = activeAccount.notificationsFollowed
setOnPreferenceChangeListener { _, newValue ->
updateAccount { it.notificationsFollowed = newValue as Boolean }
true
}
}
switchPreference {
setTitle(R.string.pref_title_notification_filter_follow_requests)
key = PrefKeys.NOTIFICATION_FILTER_FOLLOW_REQUESTS
isIconSpaceReserved = false
isChecked = activeAccount.notificationsFollowRequested
setOnPreferenceChangeListener { _, newValue ->
updateAccount { it.notificationsFollowRequested = newValue as Boolean }
true
}
}
switchPreference {
setTitle(R.string.pref_title_notification_filter_reblogs)
key = PrefKeys.NOTIFICATION_FILTER_REBLOGS
isIconSpaceReserved = false
isChecked = activeAccount.notificationsReblogged
setOnPreferenceChangeListener { _, newValue ->
updateAccount { it.notificationsReblogged = newValue as Boolean }
true
}
}
switchPreference {
setTitle(R.string.pref_title_notification_filter_favourites)
key = PrefKeys.NOTIFICATION_FILTER_FAVS
isIconSpaceReserved = false
isChecked = activeAccount.notificationsFavorited
setOnPreferenceChangeListener { _, newValue ->
updateAccount { it.notificationsFavorited = newValue as Boolean }
true
}
}
switchPreference {
setTitle(R.string.pref_title_notification_filter_poll)
key = PrefKeys.NOTIFICATION_FILTER_POLLS
isIconSpaceReserved = false
isChecked = activeAccount.notificationsPolls
setOnPreferenceChangeListener { _, newValue ->
updateAccount { it.notificationsPolls = newValue as Boolean }
true
}
}
}
preferenceCategory(R.string.pref_title_notification_alerts) { category ->
category.dependency = PrefKeys.NOTIFICATIONS_ENABLED
category.isIconSpaceReserved = false
switchPreference {
setTitle(R.string.pref_title_notification_alert_sound)
key = PrefKeys.NOTIFICATION_ALERT_SOUND
isIconSpaceReserved = false
isChecked = activeAccount.notificationSound
setOnPreferenceChangeListener { _, newValue ->
updateAccount { it.notificationSound = newValue as Boolean }
true
}
}
switchPreference {
setTitle(R.string.pref_title_notification_alert_vibrate)
key = PrefKeys.NOTIFICATION_ALERT_VIBRATE
isIconSpaceReserved = false
isChecked = activeAccount.notificationVibration
setOnPreferenceChangeListener { _, newValue ->
updateAccount { it.notificationVibration = newValue as Boolean }
true
}
}
switchPreference {
setTitle(R.string.pref_title_notification_alert_light)
key = PrefKeys.NOTIFICATION_ALERT_LIGHT
isIconSpaceReserved = false
isChecked = activeAccount.notificationLight
setOnPreferenceChangeListener { _, newValue ->
updateAccount { it.notificationLight = newValue as Boolean }
true
}
}
}
}
}
private inline fun updateAccount(changer: (AccountEntity) -> Unit) {
accountManager.activeAccount?.let { account ->
changer(account)
accountManager.saveAccount(account)
}
}
companion object {
fun newInstance(): NotificationPreferencesFragment {
return NotificationPreferencesFragment()
}
}
}

View file

@ -0,0 +1,190 @@
/* Copyright 2018 Conny Duck
*
* This file is a part of Tusky.
*
* This program is free software; you can redistribute it and/or modify it under the terms of the
* GNU General Public License as published by the Free Software Foundation; either version 3 of the
* License, or (at your option) any later version.
*
* Tusky is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even
* the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General
* Public License for more details.
*
* You should have received a copy of the GNU General Public License along with Tusky; if not,
* see <http://www.gnu.org/licenses>. */
package com.keylesspalace.tusky.components.preference
import android.content.Context
import android.content.Intent
import android.content.SharedPreferences
import android.os.Bundle
import android.util.Log
import android.view.MenuItem
import androidx.fragment.app.Fragment
import androidx.preference.PreferenceManager
import com.keylesspalace.tusky.BaseActivity
import com.keylesspalace.tusky.MainActivity
import com.keylesspalace.tusky.R
import com.keylesspalace.tusky.appstore.EventHub
import com.keylesspalace.tusky.appstore.PreferenceChangedEvent
import com.keylesspalace.tusky.settings.PrefKeys
import com.keylesspalace.tusky.util.ThemeUtils
import com.keylesspalace.tusky.util.getNonNullString
import dagger.android.DispatchingAndroidInjector
import dagger.android.HasAndroidInjector
import kotlinx.android.synthetic.main.toolbar_basic.*
import javax.inject.Inject
class PreferencesActivity : BaseActivity(), SharedPreferences.OnSharedPreferenceChangeListener,
HasAndroidInjector {
@Inject
lateinit var eventHub: EventHub
@Inject
lateinit var androidInjector: DispatchingAndroidInjector<Any>
private var restartActivitiesOnExit: Boolean = false
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_preferences)
setSupportActionBar(toolbar)
supportActionBar?.run {
setDisplayHomeAsUpEnabled(true)
setDisplayShowHomeEnabled(true)
}
val fragment: Fragment = when (intent.getIntExtra(EXTRA_PREFERENCE_TYPE, 0)) {
GENERAL_PREFERENCES -> {
setTitle(R.string.action_view_preferences)
PreferencesFragment.newInstance()
}
ACCOUNT_PREFERENCES -> {
setTitle(R.string.action_view_account_preferences)
AccountPreferencesFragment.newInstance()
}
NOTIFICATION_PREFERENCES -> {
setTitle(R.string.pref_title_edit_notification_settings)
NotificationPreferencesFragment.newInstance()
}
TAB_FILTER_PREFERENCES -> {
setTitle(R.string.pref_title_status_tabs)
TabFilterPreferencesFragment.newInstance()
}
PROXY_PREFERENCES -> {
setTitle(R.string.pref_title_http_proxy_settings)
ProxyPreferencesFragment.newInstance()
}
else -> throw IllegalArgumentException("preferenceType not known")
}
supportFragmentManager.beginTransaction()
.replace(R.id.fragment_container, fragment)
.commit()
restartActivitiesOnExit = intent.getBooleanExtra("restart", false)
}
override fun onResume() {
super.onResume()
PreferenceManager.getDefaultSharedPreferences(this).registerOnSharedPreferenceChangeListener(this)
}
override fun onPause() {
super.onPause()
PreferenceManager.getDefaultSharedPreferences(this).unregisterOnSharedPreferenceChangeListener(this)
}
override fun onOptionsItemSelected(item: MenuItem): Boolean {
when (item.itemId) {
android.R.id.home -> {
onBackPressed()
return true
}
}
return super.onOptionsItemSelected(item)
}
private fun saveInstanceState(outState: Bundle) {
outState.putBoolean("restart", restartActivitiesOnExit)
}
override fun onSaveInstanceState(outState: Bundle) {
outState.putBoolean("restart", restartActivitiesOnExit)
super.onSaveInstanceState(outState)
}
override fun onSharedPreferenceChanged(sharedPreferences: SharedPreferences, key: String) {
when (key) {
"appTheme" -> {
val theme = sharedPreferences.getNonNullString("appTheme", ThemeUtils.APP_THEME_DEFAULT)
Log.d("activeTheme", theme)
ThemeUtils.setAppNightMode(theme)
restartActivitiesOnExit = true
this.restartCurrentActivity()
}
"statusTextSize", "absoluteTimeView", "showBotOverlay", "animateGifAvatars",
"useBlurhash", "showCardsInTimelines", "confirmReblogs", "enableSwipeForTabs", "mainNavPosition", PrefKeys.HIDE_TOP_TOOLBAR -> {
restartActivitiesOnExit = true
}
"language" -> {
restartActivitiesOnExit = true
this.restartCurrentActivity()
}
}
eventHub.dispatch(PreferenceChangedEvent(key))
}
private fun restartCurrentActivity() {
intent.flags = Intent.FLAG_ACTIVITY_CLEAR_TOP or Intent.FLAG_ACTIVITY_NEW_TASK
val savedInstanceState = Bundle()
saveInstanceState(savedInstanceState)
intent.putExtras(savedInstanceState)
startActivityWithSlideInAnimation(intent)
finish()
overridePendingTransition(R.anim.fade_in, R.anim.fade_out)
}
override fun onBackPressed() {
/* Switching themes won't actually change the theme of activities on the back stack.
* Either the back stack activities need to all be recreated, or do the easier thing, which
* is hijack the back button press and use it to launch a new MainActivity and clear the
* back stack. */
if (restartActivitiesOnExit) {
val intent = Intent(this, MainActivity::class.java)
intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK
startActivityWithSlideInAnimation(intent)
} else {
super.onBackPressed()
}
}
override fun androidInjector() = androidInjector
companion object {
const val GENERAL_PREFERENCES = 0
const val ACCOUNT_PREFERENCES = 1
const val NOTIFICATION_PREFERENCES = 2
const val TAB_FILTER_PREFERENCES = 3
const val PROXY_PREFERENCES = 4
private const val EXTRA_PREFERENCE_TYPE = "EXTRA_PREFERENCE_TYPE"
@JvmStatic
fun newIntent(context: Context, preferenceType: Int): Intent {
val intent = Intent(context, PreferencesActivity::class.java)
intent.putExtra(EXTRA_PREFERENCE_TYPE, preferenceType)
return intent
}
}
}

View file

@ -0,0 +1,254 @@
/* Copyright 2018 Conny Duck
*
* This file is a part of Tusky.
*
* This program is free software; you can redistribute it and/or modify it under the terms of the
* GNU General Public License as published by the Free Software Foundation; either version 3 of the
* License, or (at your option) any later version.
*
* Tusky is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even
* the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General
* Public License for more details.
*
* You should have received a copy of the GNU General Public License along with Tusky; if not,
* see <http://www.gnu.org/licenses>. */
package com.keylesspalace.tusky.components.preference
import android.os.Bundle
import androidx.preference.Preference
import androidx.preference.PreferenceFragmentCompat
import com.keylesspalace.tusky.R
import com.keylesspalace.tusky.di.Injectable
import com.keylesspalace.tusky.settings.*
import com.keylesspalace.tusky.util.ThemeUtils
import com.keylesspalace.tusky.util.getNonNullString
import com.mikepenz.iconics.IconicsDrawable
import com.mikepenz.iconics.typeface.library.googlematerial.GoogleMaterial
import com.mikepenz.iconics.utils.colorInt
import com.mikepenz.iconics.utils.sizePx
import okhttp3.OkHttpClient
import javax.inject.Inject
class PreferencesFragment : PreferenceFragmentCompat(), Injectable {
@Inject
lateinit var okhttpclient: OkHttpClient
private val iconSize by lazy { resources.getDimensionPixelSize(R.dimen.preference_icon_size) }
private var httpProxyPref: Preference? = null
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
val context = requireContext()
makePreferenceScreen {
preferenceCategory(R.string.pref_title_appearance_settings) {
listPreference {
setDefaultValue(AppTheme.NIGHT.value)
setEntries(R.array.app_theme_names)
entryValues = AppTheme.stringValues()
key = PrefKeys.APP_THEME
setSummaryProvider { entry }
setTitle(R.string.pref_title_app_theme)
icon = makeIcon(GoogleMaterial.Icon.gmd_palette)
}
emojiPreference(okhttpclient) {
setDefaultValue("system_default")
setIcon(R.drawable.ic_emoji_24dp)
key = PrefKeys.EMOJI
setSummary(R.string.system_default)
setTitle(R.string.emoji_style)
icon = makeIcon(GoogleMaterial.Icon.gmd_sentiment_satisfied)
}
listPreference {
setDefaultValue("default")
setEntries(R.array.language_entries)
setEntryValues(R.array.language_values)
key = PrefKeys.LANGUAGE
setSummaryProvider { entry }
setTitle(R.string.pref_title_language)
icon = makeIcon(GoogleMaterial.Icon.gmd_translate)
}
listPreference {
setDefaultValue("medium")
setEntries(R.array.status_text_size_names)
setEntryValues(R.array.status_text_size_values)
key = PrefKeys.STATUS_TEXT_SIZE
setSummaryProvider { entry }
setTitle(R.string.pref_status_text_size)
icon = makeIcon(GoogleMaterial.Icon.gmd_format_size)
}
listPreference {
setDefaultValue("top")
setEntries(R.array.pref_main_nav_position_options)
setEntryValues(R.array.pref_main_nav_position_values)
key = PrefKeys.MAIN_NAV_POSITION
setSummaryProvider { entry }
setTitle(R.string.pref_main_nav_position)
}
switchPreference {
setDefaultValue(false)
key = PrefKeys.HIDE_TOP_TOOLBAR
setTitle(R.string.pref_title_hide_top_toolbar)
}
switchPreference {
setDefaultValue(false)
key = PrefKeys.FAB_HIDE
setTitle(R.string.pref_title_hide_follow_button)
isSingleLineTitle = false
}
switchPreference {
setDefaultValue(false)
key = PrefKeys.ABSOLUTE_TIME_VIEW
setTitle(R.string.pref_title_absolute_time)
isSingleLineTitle = false
}
switchPreference {
setDefaultValue(true)
key = PrefKeys.SHOW_BOT_OVERLAY
setTitle(R.string.pref_title_bot_overlay)
isSingleLineTitle = false
icon = ThemeUtils.getTintedDrawable(
context,
R.drawable.ic_bot_24dp,
R.attr.iconColor
)
}
switchPreference {
setDefaultValue(false)
key = PrefKeys.ANIMATE_GIF_AVATARS
setTitle(R.string.pref_title_animate_gif_avatars)
isSingleLineTitle = false
}
switchPreference {
setDefaultValue(true)
key = PrefKeys.USE_BLURHASH
setTitle(R.string.pref_title_gradient_for_media)
isSingleLineTitle = false
}
switchPreference {
setDefaultValue(false)
key = PrefKeys.SHOW_CARDS_IN_TIMELINES
setTitle(R.string.pref_title_show_cards_in_timelines)
isSingleLineTitle = false
}
switchPreference {
setDefaultValue(true)
key = PrefKeys.SHOW_NOTIFICATIONS_FILTER
setTitle(R.string.pref_title_show_notifications_filter)
isSingleLineTitle = false
setOnPreferenceClickListener {
activity?.let { activity ->
val intent = PreferencesActivity.newIntent(activity, PreferencesActivity.TAB_FILTER_PREFERENCES)
activity.startActivity(intent)
activity.overridePendingTransition(R.anim.slide_from_right, R.anim.slide_to_left)
}
true
}
}
switchPreference {
setDefaultValue(true)
key = PrefKeys.CONFIRM_REBLOGS
setTitle(R.string.pref_title_confirm_reblogs)
isSingleLineTitle = false
}
switchPreference {
setDefaultValue(true)
key = PrefKeys.ENABLE_SWIPE_FOR_TABS
setTitle(R.string.pref_title_enable_swipe_for_tabs)
isSingleLineTitle = false
}
}
preferenceCategory(R.string.pref_title_browser_settings) {
switchPreference {
setDefaultValue(false)
key = PrefKeys.CUSTOM_TABS
setTitle(R.string.pref_title_custom_tabs)
isSingleLineTitle = false
}
}
preferenceCategory(R.string.pref_title_timeline_filters) {
preference {
setTitle(R.string.pref_title_status_tabs)
setOnPreferenceClickListener {
activity?.let { activity ->
val intent = PreferencesActivity.newIntent(activity, PreferencesActivity.TAB_FILTER_PREFERENCES)
activity.startActivity(intent)
activity.overridePendingTransition(R.anim.slide_from_right, R.anim.slide_to_left)
}
true
}
}
}
preferenceCategory(R.string.pref_title_proxy_settings) {
httpProxyPref = preference {
setTitle(R.string.pref_title_http_proxy_settings)
setOnPreferenceClickListener {
activity?.let { activity ->
val intent = PreferencesActivity.newIntent(activity, PreferencesActivity.PROXY_PREFERENCES)
activity.startActivity(intent)
activity.overridePendingTransition(R.anim.slide_from_right, R.anim.slide_to_left)
}
true
}
}
}
}
}
private fun makeIcon(icon: GoogleMaterial.Icon): IconicsDrawable {
val context = requireContext()
return IconicsDrawable(context, icon).apply {
sizePx = iconSize
colorInt = ThemeUtils.getColor(context, R.attr.iconColor)
}
}
override fun onResume() {
super.onResume()
updateHttpProxySummary()
}
private fun updateHttpProxySummary() {
val sharedPreferences = preferenceManager.sharedPreferences
val httpProxyEnabled = sharedPreferences.getBoolean(PrefKeys.HTTP_PROXY_ENABLED, false)
val httpServer = sharedPreferences.getNonNullString(PrefKeys.HTTP_PROXY_SERVER, "")
try {
val httpPort = sharedPreferences.getNonNullString(PrefKeys.HTTP_PROXY_PORT, "-1")
.toInt()
if (httpProxyEnabled && httpServer.isNotBlank() && httpPort > 0 && httpPort < 65535) {
httpProxyPref?.summary = "$httpServer:$httpPort"
return
}
} catch (e: NumberFormatException) {
// user has entered wrong port, fall back to empty summary
}
httpProxyPref?.summary = ""
}
companion object {
fun newInstance(): PreferencesFragment {
return PreferencesFragment()
}
}
}

View file

@ -0,0 +1,69 @@
/* Copyright 2018 Conny Duck
* This file is a part of Tusky.
*
* This program is free software; you can redistribute it and/or modify it under the terms of the
* GNU General Public License as published by the Free Software Foundation; either version 3 of the
* License, or (at your option) any later version.
*
* Tusky is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even
* the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General
* Public License for more details.
*
* You should have received a copy of the GNU General Public License along with Tusky; if not,
* see <http://www.gnu.org/licenses>. */
package com.keylesspalace.tusky.components.preference
import android.os.Bundle
import androidx.preference.PreferenceFragmentCompat
import com.keylesspalace.tusky.R
import com.keylesspalace.tusky.settings.PrefKeys
import com.keylesspalace.tusky.settings.editTextPreference
import com.keylesspalace.tusky.settings.makePreferenceScreen
import com.keylesspalace.tusky.settings.switchPreference
import kotlin.system.exitProcess
class ProxyPreferencesFragment : PreferenceFragmentCompat() {
private var pendingRestart = false
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
makePreferenceScreen {
switchPreference {
setTitle(R.string.pref_title_http_proxy_enable)
isIconSpaceReserved = false
key = PrefKeys.HTTP_PROXY_ENABLED
setDefaultValue(false)
}
editTextPreference {
setTitle(R.string.pref_title_http_proxy_server)
key = PrefKeys.HTTP_PROXY_SERVER
isIconSpaceReserved = false
setSummaryProvider { text }
}
editTextPreference {
setTitle(R.string.pref_title_http_proxy_port)
key = PrefKeys.HTTP_PROXY_PORT
isIconSpaceReserved = false
setSummaryProvider { text }
}
}
}
override fun onPause() {
super.onPause()
if (pendingRestart) {
pendingRestart = false
exitProcess(0)
}
}
companion object {
fun newInstance(): ProxyPreferencesFragment {
return ProxyPreferencesFragment()
}
}
}

View file

@ -0,0 +1,54 @@
/* Copyright 2018 Conny Duck
*
* This file is a part of Tusky.
*
* This program is free software; you can redistribute it and/or modify it under the terms of the
* GNU General Public License as published by the Free Software Foundation; either version 3 of the
* License, or (at your option) any later version.
*
* Tusky is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even
* the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General
* Public License for more details.
*
* You should have received a copy of the GNU General Public License along with Tusky; if not,
* see <http://www.gnu.org/licenses>. */
package com.keylesspalace.tusky.components.preference
import android.os.Bundle
import androidx.preference.PreferenceFragmentCompat
import com.keylesspalace.tusky.R
import com.keylesspalace.tusky.settings.PrefKeys
import com.keylesspalace.tusky.settings.checkBoxPreference
import com.keylesspalace.tusky.settings.makePreferenceScreen
import com.keylesspalace.tusky.settings.preferenceCategory
class TabFilterPreferencesFragment : PreferenceFragmentCompat() {
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
makePreferenceScreen {
preferenceCategory(R.string.title_home) { category ->
category.isIconSpaceReserved = false
checkBoxPreference {
setTitle(R.string.pref_title_show_boosts)
key = PrefKeys.TAB_FILTER_HOME_BOOSTS
setDefaultValue(true)
isIconSpaceReserved = false
}
checkBoxPreference {
setTitle(R.string.pref_title_show_replies)
key = PrefKeys.TAB_FILTER_HOME_REPLIES
setDefaultValue(false)
isIconSpaceReserved = false
}
}
}
}
companion object {
fun newInstance(): TabFilterPreferencesFragment {
return TabFilterPreferencesFragment()
}
}
}