From c67af7f958b990846905b314e255d7612ec45e26 Mon Sep 17 00:00:00 2001 From: Philipp Heckel Date: Sat, 29 Jan 2022 22:25:39 -0500 Subject: [PATCH] User management works --- .../main/java/io/heckel/ntfy/db/Database.kt | 7 +- .../main/java/io/heckel/ntfy/db/Repository.kt | 14 ++- .../ntfy/service/SubscriberServiceManager.kt | 6 ++ .../io/heckel/ntfy/ui/SettingsActivity.kt | 97 ++++++++++++++----- .../java/io/heckel/ntfy/ui/UserFragment.kt | 40 +++++++- .../main/res/layout/fragment_user_dialog.xml | 2 +- app/src/main/res/values/strings.xml | 4 +- 7 files changed, 139 insertions(+), 31 deletions(-) diff --git a/app/src/main/java/io/heckel/ntfy/db/Database.kt b/app/src/main/java/io/heckel/ntfy/db/Database.kt index a50d659..e2dcdfc 100644 --- a/app/src/main/java/io/heckel/ntfy/db/Database.kt +++ b/app/src/main/java/io/heckel/ntfy/db/Database.kt @@ -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 diff --git a/app/src/main/java/io/heckel/ntfy/db/Repository.kt b/app/src/main/java/io/heckel/ntfy/db/Repository.kt index 5f6fe74..9988f02 100644 --- a/app/src/main/java/io/heckel/ntfy/db/Repository.kt +++ b/app/src/main/java/io/heckel/ntfy/db/Repository.kt @@ -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> { 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) } diff --git a/app/src/main/java/io/heckel/ntfy/service/SubscriberServiceManager.kt b/app/src/main/java/io/heckel/ntfy/service/SubscriberServiceManager.kt index 952eb0a..8b5b97f 100644 --- a/app/src/main/java/io/heckel/ntfy/service/SubscriberServiceManager.kt +++ b/app/src/main/java/io/heckel/ntfy/service/SubscriberServiceManager.kt @@ -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. diff --git a/app/src/main/java/io/heckel/ntfy/ui/SettingsActivity.kt b/app/src/main/java/io/heckel/ntfy/ui/SettingsActivity.kt index ed8a1ac..6248ce4 100644 --- a/app/src/main/java/io/heckel/ntfy/ui/SettingsActivity.kt +++ b/app/src/main/java/io/heckel/ntfy/ui/SettingsActivity.kt @@ -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 + ) + + 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 - ) - private fun addUserPreferences(usersByBaseUrl: Map>) { 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 { diff --git a/app/src/main/java/io/heckel/ntfy/ui/UserFragment.kt b/app/src/main/java/io/heckel/ntfy/ui/UserFragment.kt index 96e03fa..ced759e 100644 --- a/app/src/main/java/io/heckel/ntfy/ui/UserFragment.kt +++ b/app/src/main/java/io/heckel/ntfy/ui/UserFragment.kt @@ -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() ?: "" diff --git a/app/src/main/res/layout/fragment_user_dialog.xml b/app/src/main/res/layout/fragment_user_dialog.xml index c1905e7..f6ed271 100644 --- a/app/src/main/res/layout/fragment_user_dialog.xml +++ b/app/src/main/res/layout/fragment_user_dialog.xml @@ -7,7 +7,7 @@ android:orientation="horizontal" android:paddingLeft="16dp" android:paddingRight="16dp" - android:visibility="visible"> + android:visibility="visible" android:paddingBottom="10dp"> Not used by any topics Used by topic %1$s Used by topics %1$s - Add user + Add users + Add new user + Create a new user that can be associated to topics. You can also create a new user when adding a topic. UnifiedPush Allows other apps to use ntfy as a message distributor. Find out more at unifiedpush.org. UnifiedPushEnabled