User management works
This commit is contained in:
parent
43757eb7b5
commit
c67af7f958
7 changed files with 139 additions and 31 deletions
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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() ?: ""
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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>
|
||||
|
|
Loading…
Reference in a new issue