User management works

This commit is contained in:
Philipp Heckel 2022-01-29 22:25:39 -05:00
parent 43757eb7b5
commit c67af7f958
7 changed files with 139 additions and 31 deletions

View file

@ -265,6 +265,9 @@ interface SubscriptionDao {
@Query("DELETE FROM subscription WHERE id = :subscriptionId")
fun remove(subscriptionId: Long)
@Query("UPDATE subscription SET authUserId = null WHERE authUserId = :authUserId")
fun removeAuthUserFromSubscriptions(authUserId: Long)
}
@Dao
@ -311,8 +314,8 @@ interface UserDao {
@Update
suspend fun update(user: User)
@Delete
suspend fun delete(user: User)
@Query("DELETE FROM user WHERE id = :id")
suspend fun delete(id: Long)
}
@Dao

View file

@ -84,6 +84,10 @@ class Repository(private val sharedPrefs: SharedPreferences, private val databas
subscriptionDao.remove(subscriptionId)
}
suspend fun removeAuthUserFromSubscriptions(authUserId: Long) {
subscriptionDao.removeAuthUserFromSubscriptions(authUserId)
}
fun getNotificationsLiveData(subscriptionId: Long): LiveData<List<Notification>> {
return notificationDao.listFlow(subscriptionId).asLiveData()
}
@ -137,13 +141,21 @@ class Repository(private val sharedPrefs: SharedPreferences, private val databas
}
suspend fun addUser(user: User) {
return userDao.insert(user)
userDao.insert(user)
}
suspend fun updateUser(user: User) {
userDao.update(user)
}
suspend fun getUser(userId: Long): User {
return userDao.get(userId)
}
suspend fun deleteUser(userId: Long) {
userDao.delete(userId)
}
fun getPollWorkerVersion(): Int {
return sharedPrefs.getInt(SHARED_PREFS_POLL_WORKER_VERSION, 0)
}

View file

@ -24,6 +24,12 @@ class SubscriberServiceManager(private val context: Context) {
workManager.enqueue(startServiceRequest)
}
fun stop() {
Intent(context, SubscriberService::class.java).also { intent ->
context.stopService(intent) // Service will auto-restart
}
}
/**
* Starts or stops the foreground service by figuring out how many instant delivery subscriptions
* exist. If there's > 0, then we need a foreground service.

View file

@ -15,6 +15,7 @@ import androidx.appcompat.app.AppCompatActivity
import androidx.appcompat.app.AppCompatDelegate
import androidx.core.app.ActivityCompat
import androidx.core.content.ContextCompat
import androidx.fragment.app.DialogFragment
import androidx.lifecycle.lifecycleScope
import androidx.preference.*
import androidx.preference.Preference.OnPreferenceClickListener
@ -25,6 +26,7 @@ import io.heckel.ntfy.db.Repository
import io.heckel.ntfy.db.User
import io.heckel.ntfy.log.Log
import io.heckel.ntfy.service.SubscriberService
import io.heckel.ntfy.service.SubscriberServiceManager
import io.heckel.ntfy.util.formatBytes
import io.heckel.ntfy.util.formatDateShort
import io.heckel.ntfy.util.shortUrl
@ -43,8 +45,13 @@ import java.util.concurrent.TimeUnit
* The "nested screen" navigation stuff (for user management) has been taken from
* https://github.com/googlearchive/android-preferences/blob/master/app/src/main/java/com/example/androidx/preference/sample/MainActivity.kt
*/
class SettingsActivity : AppCompatActivity(), PreferenceFragmentCompat.OnPreferenceStartFragmentCallback {
private lateinit var fragment: SettingsFragment
class SettingsActivity : AppCompatActivity(), PreferenceFragmentCompat.OnPreferenceStartFragmentCallback,
UserFragment.UserDialogListener {
private lateinit var settingsFragment: SettingsFragment
private lateinit var userSettingsFragment: UserSettingsFragment
private lateinit var repository: Repository
private lateinit var serviceManager: SubscriberServiceManager
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
@ -52,11 +59,14 @@ class SettingsActivity : AppCompatActivity(), PreferenceFragmentCompat.OnPrefere
Log.d(TAG, "Create $this")
repository = Repository.getInstance(this)
serviceManager = SubscriberServiceManager(this)
if (savedInstanceState == null) {
fragment = SettingsFragment() // Empty constructor!
settingsFragment = SettingsFragment() // Empty constructor!
supportFragmentManager
.beginTransaction()
.replace(R.id.settings_layout, fragment)
.replace(R.id.settings_layout, settingsFragment)
.commit()
} else {
title = savedInstanceState.getCharSequence(TITLE_TAG)
@ -84,7 +94,6 @@ class SettingsActivity : AppCompatActivity(), PreferenceFragmentCompat.OnPrefere
return super.onSupportNavigateUp()
}
override fun onPreferenceStartFragment(
caller: PreferenceFragmentCompat,
pref: Preference
@ -98,17 +107,25 @@ class SettingsActivity : AppCompatActivity(), PreferenceFragmentCompat.OnPrefere
arguments = args
setTargetFragment(caller, 0)
}
// Replace the existing Fragment with the new Fragment
supportFragmentManager.beginTransaction()
.replace(R.id.settings_layout, fragment)
.addToBackStack(null)
.commit()
title = pref.title
// Save user settings fragment for later
if (fragment is UserSettingsFragment) {
userSettingsFragment = fragment
}
return true
}
class SettingsFragment : PreferenceFragmentCompat() {
private lateinit var repository: Repository
private lateinit var serviceManager: SubscriberServiceManager
private var autoDownloadSelection = AUTO_DOWNLOAD_SELECTION_NOT_SET
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
@ -116,6 +133,7 @@ class SettingsActivity : AppCompatActivity(), PreferenceFragmentCompat.OnPrefere
// Dependencies (Fragments need a default constructor)
repository = Repository.getInstance(requireActivity())
serviceManager = SubscriberServiceManager(requireActivity())
autoDownloadSelection = repository.getAutoDownloadMaxSize() // Only used for <= Android P, due to permissions request
// Important note: We do not use the default shared prefs to store settings. Every
@ -421,10 +439,7 @@ class SettingsActivity : AppCompatActivity(), PreferenceFragmentCompat.OnPrefere
}
private fun restartService() {
val context = this@SettingsFragment.context
Intent(context, SubscriberService::class.java).also { intent ->
context?.stopService(intent) // Service will auto-restart
}
serviceManager.stop() // Service will auto-restart
}
private fun copyLogsToClipboard() {
@ -516,10 +531,17 @@ class SettingsActivity : AppCompatActivity(), PreferenceFragmentCompat.OnPrefere
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
setPreferencesFromResource(R.xml.user_preferences, rootKey)
// Dependencies (Fragments need a default constructor)
repository = Repository.getInstance(requireActivity())
reload()
}
data class UserWithMetadata(
val user: User,
val topics: List<String>
)
fun reload() {
preferenceScreen.removeAll()
lifecycleScope.launch(Dispatchers.IO) {
val userIdsWithTopics = repository.getSubscriptions()
.groupBy { it.authUserId }
@ -530,18 +552,12 @@ class SettingsActivity : AppCompatActivity(), PreferenceFragmentCompat.OnPrefere
UserWithMetadata(user, topics)
}
.groupBy { it.user.baseUrl }
activity?.runOnUiThread {
addUserPreferences(usersByBaseUrl)
}
}
}
data class UserWithMetadata(
val user: User,
val topics: List<String>
)
private fun addUserPreferences(usersByBaseUrl: Map<String, List<UserWithMetadata>>) {
usersByBaseUrl.forEach { entry ->
val baseUrl = entry.key
@ -574,9 +590,14 @@ class SettingsActivity : AppCompatActivity(), PreferenceFragmentCompat.OnPrefere
}
// Add user
val preference = Preference(preferenceScreen.context)
preference.title = getString(R.string.settings_users_prefs_user_add)
preference.onPreferenceClickListener = OnPreferenceClickListener { _ ->
val userAddCategory = PreferenceCategory(preferenceScreen.context)
userAddCategory.title = getString(R.string.settings_users_prefs_user_add)
preferenceScreen.addPreference(userAddCategory)
val userAddPref = Preference(preferenceScreen.context)
userAddPref.title = getString(R.string.settings_users_prefs_user_add_title)
userAddPref.summary = getString(R.string.settings_users_prefs_user_add_summary)
userAddPref.onPreferenceClickListener = OnPreferenceClickListener { _ ->
activity?.let {
UserFragment
.newInstance(user = null)
@ -584,7 +605,7 @@ class SettingsActivity : AppCompatActivity(), PreferenceFragmentCompat.OnPrefere
}
true
}
preferenceScreen.addPreference(preference)
userAddCategory.addPreference(userAddPref)
}
}
@ -597,9 +618,39 @@ class SettingsActivity : AppCompatActivity(), PreferenceFragmentCompat.OnPrefere
}
}
override fun onAddUser(dialog: DialogFragment, user: User) {
lifecycleScope.launch(Dispatchers.IO) {
repository.addUser(user) // New users are not used, so no service refresh required
runOnUiThread {
userSettingsFragment.reload()
}
}
}
override fun onUpdateUser(dialog: DialogFragment, user: User) {
lifecycleScope.launch(Dispatchers.IO) {
repository.updateUser(user)
serviceManager.stop() // Editing does not change the user ID
runOnUiThread {
userSettingsFragment.reload()
}
}
}
override fun onDeleteUser(dialog: DialogFragment, authUserId: Long) {
lifecycleScope.launch(Dispatchers.IO) {
repository.removeAuthUserFromSubscriptions(authUserId)
repository.deleteUser(authUserId)
serviceManager.refresh() // authUserId changed, so refresh is enough
runOnUiThread {
userSettingsFragment.reload()
}
}
}
private fun setAutoDownload() {
if (!this::fragment.isInitialized) return
fragment.setAutoDownload()
if (!this::settingsFragment.isInitialized) return
settingsFragment.setAutoDownload()
}
companion object {

View file

@ -2,6 +2,7 @@ package io.heckel.ntfy.ui
import android.app.AlertDialog
import android.app.Dialog
import android.content.Context
import android.os.Bundle
import android.text.Editable
import android.text.TextWatcher
@ -14,15 +15,28 @@ import androidx.fragment.app.DialogFragment
import com.google.android.material.textfield.TextInputEditText
import io.heckel.ntfy.R
import io.heckel.ntfy.db.User
import kotlin.random.Random
class UserFragment : DialogFragment() {
private var user: User? = null
private lateinit var listener: UserDialogListener
private lateinit var baseUrlView: TextInputEditText
private lateinit var usernameView: TextInputEditText
private lateinit var passwordView: TextInputEditText
private lateinit var positiveButton: Button
interface UserDialogListener {
fun onAddUser(dialog: DialogFragment, user: User)
fun onUpdateUser(dialog: DialogFragment, user: User)
fun onDeleteUser(dialog: DialogFragment, authUserId: Long)
}
override fun onAttach(context: Context) {
super.onAttach(context)
listener = activity as UserDialogListener
}
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
// Reconstruct user (if it is present in the bundle)
val userId = arguments?.getLong(BUNDLE_USER_ID)
@ -62,14 +76,16 @@ class UserFragment : DialogFragment() {
val builder = AlertDialog.Builder(activity)
.setView(view)
.setPositiveButton(positiveButtonTextResId) { _, _ ->
// This will be overridden below to avoid closing the dialog immediately
saveClicked()
}
.setNegativeButton(R.string.user_dialog_button_cancel) { _, _ ->
// This will be overridden below
// Do nothing
}
if (user != null) {
builder.setNeutralButton(R.string.user_dialog_button_delete) { _, _ ->
// This will be overridden below
if (this::listener.isInitialized && userId != null) {
listener.onDeleteUser(this, userId)
}
}
}
val dialog = builder.create()
@ -109,6 +125,24 @@ class UserFragment : DialogFragment() {
return dialog
}
private fun saveClicked() {
if (!this::listener.isInitialized) return
val baseUrl = baseUrlView.text?.toString() ?: ""
val username = usernameView.text?.toString() ?: ""
val password = passwordView.text?.toString() ?: ""
if (user == null) {
user = User(Random.nextLong(), baseUrl, username, password)
listener.onAddUser(this, user!!)
} else {
user = if (password.isNotEmpty()) {
user!!.copy(username = username, password = password)
} else {
user!!.copy(username = username)
}
listener.onUpdateUser(this, user!!)
}
}
private fun validateInput() {
val baseUrl = baseUrlView.text?.toString() ?: ""
val username = usernameView.text?.toString() ?: ""

View file

@ -7,7 +7,7 @@
android:orientation="horizontal"
android:paddingLeft="16dp"
android:paddingRight="16dp"
android:visibility="visible">
android:visibility="visible" android:paddingBottom="10dp">
<TextView
android:text="This topic requires you to login. Please pick an existing user or type in a username and password."
android:layout_width="match_parent"

View file

@ -246,7 +246,9 @@
<string name="settings_users_prefs_user_not_used">Not used by any topics</string>
<string name="settings_users_prefs_user_used_by_one">Used by topic %1$s</string>
<string name="settings_users_prefs_user_used_by_many">Used by topics %1$s</string>
<string name="settings_users_prefs_user_add">Add user</string>
<string name="settings_users_prefs_user_add">Add users</string>
<string name="settings_users_prefs_user_add_title">Add new user</string>
<string name="settings_users_prefs_user_add_summary">Create a new user that can be associated to topics. You can also create a new user when adding a topic.</string>
<string name="settings_unified_push_header">UnifiedPush</string>
<string name="settings_unified_push_header_summary">Allows other apps to use ntfy as a message distributor. Find out more at unifiedpush.org.</string>
<string name="settings_unified_push_enabled_key">UnifiedPushEnabled</string>