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") @Query("DELETE FROM subscription WHERE id = :subscriptionId")
fun remove(subscriptionId: Long) fun remove(subscriptionId: Long)
@Query("UPDATE subscription SET authUserId = null WHERE authUserId = :authUserId")
fun removeAuthUserFromSubscriptions(authUserId: Long)
} }
@Dao @Dao
@ -311,8 +314,8 @@ interface UserDao {
@Update @Update
suspend fun update(user: User) suspend fun update(user: User)
@Delete @Query("DELETE FROM user WHERE id = :id")
suspend fun delete(user: User) suspend fun delete(id: Long)
} }
@Dao @Dao

View file

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

View file

@ -24,6 +24,12 @@ class SubscriberServiceManager(private val context: Context) {
workManager.enqueue(startServiceRequest) 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 * 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. * 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.appcompat.app.AppCompatDelegate
import androidx.core.app.ActivityCompat import androidx.core.app.ActivityCompat
import androidx.core.content.ContextCompat import androidx.core.content.ContextCompat
import androidx.fragment.app.DialogFragment
import androidx.lifecycle.lifecycleScope import androidx.lifecycle.lifecycleScope
import androidx.preference.* import androidx.preference.*
import androidx.preference.Preference.OnPreferenceClickListener 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.db.User
import io.heckel.ntfy.log.Log import io.heckel.ntfy.log.Log
import io.heckel.ntfy.service.SubscriberService import io.heckel.ntfy.service.SubscriberService
import io.heckel.ntfy.service.SubscriberServiceManager
import io.heckel.ntfy.util.formatBytes import io.heckel.ntfy.util.formatBytes
import io.heckel.ntfy.util.formatDateShort import io.heckel.ntfy.util.formatDateShort
import io.heckel.ntfy.util.shortUrl 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 * 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 * https://github.com/googlearchive/android-preferences/blob/master/app/src/main/java/com/example/androidx/preference/sample/MainActivity.kt
*/ */
class SettingsActivity : AppCompatActivity(), PreferenceFragmentCompat.OnPreferenceStartFragmentCallback { class SettingsActivity : AppCompatActivity(), PreferenceFragmentCompat.OnPreferenceStartFragmentCallback,
private lateinit var fragment: SettingsFragment 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?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
@ -52,11 +59,14 @@ class SettingsActivity : AppCompatActivity(), PreferenceFragmentCompat.OnPrefere
Log.d(TAG, "Create $this") Log.d(TAG, "Create $this")
repository = Repository.getInstance(this)
serviceManager = SubscriberServiceManager(this)
if (savedInstanceState == null) { if (savedInstanceState == null) {
fragment = SettingsFragment() // Empty constructor! settingsFragment = SettingsFragment() // Empty constructor!
supportFragmentManager supportFragmentManager
.beginTransaction() .beginTransaction()
.replace(R.id.settings_layout, fragment) .replace(R.id.settings_layout, settingsFragment)
.commit() .commit()
} else { } else {
title = savedInstanceState.getCharSequence(TITLE_TAG) title = savedInstanceState.getCharSequence(TITLE_TAG)
@ -84,7 +94,6 @@ class SettingsActivity : AppCompatActivity(), PreferenceFragmentCompat.OnPrefere
return super.onSupportNavigateUp() return super.onSupportNavigateUp()
} }
override fun onPreferenceStartFragment( override fun onPreferenceStartFragment(
caller: PreferenceFragmentCompat, caller: PreferenceFragmentCompat,
pref: Preference pref: Preference
@ -98,17 +107,25 @@ class SettingsActivity : AppCompatActivity(), PreferenceFragmentCompat.OnPrefere
arguments = args arguments = args
setTargetFragment(caller, 0) setTargetFragment(caller, 0)
} }
// Replace the existing Fragment with the new Fragment // Replace the existing Fragment with the new Fragment
supportFragmentManager.beginTransaction() supportFragmentManager.beginTransaction()
.replace(R.id.settings_layout, fragment) .replace(R.id.settings_layout, fragment)
.addToBackStack(null) .addToBackStack(null)
.commit() .commit()
title = pref.title title = pref.title
// Save user settings fragment for later
if (fragment is UserSettingsFragment) {
userSettingsFragment = fragment
}
return true return true
} }
class SettingsFragment : PreferenceFragmentCompat() { class SettingsFragment : PreferenceFragmentCompat() {
private lateinit var repository: Repository private lateinit var repository: Repository
private lateinit var serviceManager: SubscriberServiceManager
private var autoDownloadSelection = AUTO_DOWNLOAD_SELECTION_NOT_SET private var autoDownloadSelection = AUTO_DOWNLOAD_SELECTION_NOT_SET
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) { override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
@ -116,6 +133,7 @@ class SettingsActivity : AppCompatActivity(), PreferenceFragmentCompat.OnPrefere
// Dependencies (Fragments need a default constructor) // Dependencies (Fragments need a default constructor)
repository = Repository.getInstance(requireActivity()) repository = Repository.getInstance(requireActivity())
serviceManager = SubscriberServiceManager(requireActivity())
autoDownloadSelection = repository.getAutoDownloadMaxSize() // Only used for <= Android P, due to permissions request 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 // 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() { private fun restartService() {
val context = this@SettingsFragment.context serviceManager.stop() // Service will auto-restart
Intent(context, SubscriberService::class.java).also { intent ->
context?.stopService(intent) // Service will auto-restart
}
} }
private fun copyLogsToClipboard() { private fun copyLogsToClipboard() {
@ -516,10 +531,17 @@ class SettingsActivity : AppCompatActivity(), PreferenceFragmentCompat.OnPrefere
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) { override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
setPreferencesFromResource(R.xml.user_preferences, rootKey) setPreferencesFromResource(R.xml.user_preferences, rootKey)
// Dependencies (Fragments need a default constructor)
repository = Repository.getInstance(requireActivity()) repository = Repository.getInstance(requireActivity())
reload()
}
data class UserWithMetadata(
val user: User,
val topics: List<String>
)
fun reload() {
preferenceScreen.removeAll()
lifecycleScope.launch(Dispatchers.IO) { lifecycleScope.launch(Dispatchers.IO) {
val userIdsWithTopics = repository.getSubscriptions() val userIdsWithTopics = repository.getSubscriptions()
.groupBy { it.authUserId } .groupBy { it.authUserId }
@ -530,18 +552,12 @@ class SettingsActivity : AppCompatActivity(), PreferenceFragmentCompat.OnPrefere
UserWithMetadata(user, topics) UserWithMetadata(user, topics)
} }
.groupBy { it.user.baseUrl } .groupBy { it.user.baseUrl }
activity?.runOnUiThread { activity?.runOnUiThread {
addUserPreferences(usersByBaseUrl) addUserPreferences(usersByBaseUrl)
} }
} }
} }
data class UserWithMetadata(
val user: User,
val topics: List<String>
)
private fun addUserPreferences(usersByBaseUrl: Map<String, List<UserWithMetadata>>) { private fun addUserPreferences(usersByBaseUrl: Map<String, List<UserWithMetadata>>) {
usersByBaseUrl.forEach { entry -> usersByBaseUrl.forEach { entry ->
val baseUrl = entry.key val baseUrl = entry.key
@ -574,9 +590,14 @@ class SettingsActivity : AppCompatActivity(), PreferenceFragmentCompat.OnPrefere
} }
// Add user // Add user
val preference = Preference(preferenceScreen.context) val userAddCategory = PreferenceCategory(preferenceScreen.context)
preference.title = getString(R.string.settings_users_prefs_user_add) userAddCategory.title = getString(R.string.settings_users_prefs_user_add)
preference.onPreferenceClickListener = OnPreferenceClickListener { _ -> 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 { activity?.let {
UserFragment UserFragment
.newInstance(user = null) .newInstance(user = null)
@ -584,7 +605,7 @@ class SettingsActivity : AppCompatActivity(), PreferenceFragmentCompat.OnPrefere
} }
true 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() { private fun setAutoDownload() {
if (!this::fragment.isInitialized) return if (!this::settingsFragment.isInitialized) return
fragment.setAutoDownload() settingsFragment.setAutoDownload()
} }
companion object { companion object {

View file

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

View file

@ -7,7 +7,7 @@
android:orientation="horizontal" android:orientation="horizontal"
android:paddingLeft="16dp" android:paddingLeft="16dp"
android:paddingRight="16dp" android:paddingRight="16dp"
android:visibility="visible"> android:visibility="visible" android:paddingBottom="10dp">
<TextView <TextView
android:text="This topic requires you to login. Please pick an existing user or type in a username and password." 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" 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_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_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_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">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_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> <string name="settings_unified_push_enabled_key">UnifiedPushEnabled</string>