9ec5d6e3b0
Fixes #793. This is an implementation for push notifications based on UnifiedPush for Tusky. No push gateway (other than UP itself) is needed, since UnifiedPush is simple enough such that it can act as a catch-all endpoint for WebPush messages. When a UnifiedPush distributor is present on-device, we will by default register Tusky as a receiver; if no UnifiedPush distributor is available, then pull notifications are used as a fallback mechanism. Because WebPush messages are encrypted, and Mastodon does not send the keys and IV needed for decryption in the request body, for now the push handler simply acts as a trigger for the pre-existing NotificationWorker which is also used for pull notifications. Nevertheless, I have implemented proper key generation and storage, just in case we would like to implement full decryption support in the future when Mastodon upgrades to the latest WebPush encryption scheme that includes all information in the request body. For users with existing accounts, push notifications will not be enabled until all of the accounts have been re-logged in to grant the new push OAuth scope. A small prompt will be shown (until dismissed) as a Snackbar to explain to the user about this, and an option is added in Account Preferences to facilitate re-login without deleting local drafts and cache.
370 lines
15 KiB
Kotlin
370 lines
15 KiB
Kotlin
/* 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.os.Build
|
|
import android.os.Bundle
|
|
import android.util.Log
|
|
import androidx.annotation.DrawableRes
|
|
import androidx.preference.PreferenceFragmentCompat
|
|
import com.google.android.material.snackbar.Snackbar
|
|
import com.keylesspalace.tusky.AccountListActivity
|
|
import com.keylesspalace.tusky.BaseActivity
|
|
import com.keylesspalace.tusky.BuildConfig
|
|
import com.keylesspalace.tusky.FiltersActivity
|
|
import com.keylesspalace.tusky.R
|
|
import com.keylesspalace.tusky.TabPreferenceActivity
|
|
import com.keylesspalace.tusky.appstore.EventHub
|
|
import com.keylesspalace.tusky.appstore.PreferenceChangedEvent
|
|
import com.keylesspalace.tusky.components.instancemute.InstanceListActivity
|
|
import com.keylesspalace.tusky.components.login.LoginActivity
|
|
import com.keylesspalace.tusky.components.notifications.currentAccountNeedsMigration
|
|
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.PrefKeys
|
|
import com.keylesspalace.tusky.settings.listPreference
|
|
import com.keylesspalace.tusky.settings.makePreferenceScreen
|
|
import com.keylesspalace.tusky.settings.preference
|
|
import com.keylesspalace.tusky.settings.preferenceCategory
|
|
import com.keylesspalace.tusky.settings.switchPreference
|
|
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)
|
|
setIcon(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)
|
|
setIcon(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)
|
|
setIcon(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
|
|
}
|
|
}
|
|
|
|
if (currentAccountNeedsMigration(accountManager)) {
|
|
preference {
|
|
setTitle(R.string.title_migration_relogin)
|
|
setIcon(R.drawable.ic_logout)
|
|
setOnPreferenceClickListener {
|
|
val intent = LoginActivity.getIntent(context, LoginActivity.MODE_MIGRATION)
|
|
(activity as BaseActivity).startActivityWithSlideInAnimation(intent)
|
|
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()
|
|
setIcon(getIconForVisibility(visibility))
|
|
setOnPreferenceChangeListener { _, newValue ->
|
|
setIcon(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)
|
|
setIcon(getIconForSensitivity(sensitivity))
|
|
setOnPreferenceChangeListener { _, newValue ->
|
|
setIcon(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()
|
|
}
|
|
}
|
|
|
|
@DrawableRes
|
|
private fun getIconForVisibility(visibility: Status.Visibility): Int {
|
|
return 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
|
|
}
|
|
}
|
|
|
|
@DrawableRes
|
|
private fun getIconForSensitivity(sensitive: Boolean): Int {
|
|
return if (sensitive) {
|
|
R.drawable.ic_hide_media_24dp
|
|
} else {
|
|
R.drawable.ic_eye_24dp
|
|
}
|
|
}
|
|
|
|
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()
|
|
}
|
|
}
|