Skip to content

Commit

Permalink
Edit remotes in a new activity instead of a dialog
Browse files Browse the repository at this point in the history
This has a much nicer user experience since we can now have proper
switches for toggleable options and include descriptions for all the
actions. Most of the annoying "success" confirmation snackbar messages
have also been removed now that the UI can more intuitively show the
effects of the successful operations.

Signed-off-by: Andrew Gunnerson <[email protected]>
  • Loading branch information
chenxiaolong committed Oct 21, 2024
1 parent d73d2f9 commit efb3b77
Show file tree
Hide file tree
Showing 17 changed files with 1,079 additions and 727 deletions.
4 changes: 4 additions & 0 deletions app/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,10 @@
</intent-filter>
</activity>

<activity
android:name=".settings.EditRemoteActivity"
android:exported="false" />

<service
android:name=".rclone.BackgroundUploadMonitorService"
android:foregroundServiceType="specialUse"
Expand Down
90 changes: 90 additions & 0 deletions app/src/main/java/com/chiller3/rsaf/PreferenceBaseActivity.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
/*
* SPDX-FileCopyrightText: 2023-2024 Andrew Gunnerson
* SPDX-License-Identifier: GPL-3.0-only
*/

package com.chiller3.rsaf

import android.content.Intent
import android.os.Bundle
import android.view.MenuItem
import android.view.ViewGroup
import androidx.activity.enableEdgeToEdge
import androidx.appcompat.app.AppCompatActivity
import androidx.core.view.ViewCompat
import androidx.core.view.WindowInsetsCompat
import androidx.core.view.updateLayoutParams
import com.chiller3.rsaf.databinding.SettingsActivityBinding

abstract class PreferenceBaseActivity : AppCompatActivity() {
protected abstract val actionBarTitle: CharSequence?

protected abstract val showUpButton: Boolean

protected abstract fun createFragment(): PreferenceBaseFragment

override fun onCreate(savedInstanceState: Bundle?) {
enableEdgeToEdge()
super.onCreate(savedInstanceState)

val binding = SettingsActivityBinding.inflate(layoutInflater)
setContentView(binding.root)

val transaction = supportFragmentManager.beginTransaction()

// https://issuetracker.google.com/issues/181805603
val bioFragment = supportFragmentManager
.findFragmentByTag("androidx.biometric.BiometricFragment")
if (bioFragment != null) {
transaction.remove(bioFragment)
}

val fragment: PreferenceBaseFragment

if (savedInstanceState == null) {
fragment = createFragment()
transaction.replace(R.id.settings, fragment)
} else {
fragment = supportFragmentManager.findFragmentById(R.id.settings)
as PreferenceBaseFragment
}

transaction.commit()

supportFragmentManager.setFragmentResultListener(fragment.requestTag, this) { _, result ->
setResult(RESULT_OK, Intent().apply { putExtras(result) })
}

ViewCompat.setOnApplyWindowInsetsListener(binding.toolbar) { v, windowInsets ->
val insets = windowInsets.getInsets(
WindowInsetsCompat.Type.systemBars()
or WindowInsetsCompat.Type.displayCutout()
)

v.updateLayoutParams<ViewGroup.MarginLayoutParams> {
leftMargin = insets.left
topMargin = insets.top
rightMargin = insets.right
}

WindowInsetsCompat.CONSUMED
}

setSupportActionBar(binding.toolbar)
supportActionBar!!.setDisplayHomeAsUpEnabled(showUpButton)

actionBarTitle?.let {
setTitle(it)
}
}

override fun onOptionsItemSelected(item: MenuItem): Boolean {
return when (item.itemId) {
android.R.id.home -> {
onBackPressedDispatcher.onBackPressed()
true
}
else -> super.onOptionsItemSelected(item)
}
}
}
145 changes: 145 additions & 0 deletions app/src/main/java/com/chiller3/rsaf/PreferenceBaseFragment.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,145 @@
/*
* SPDX-FileCopyrightText: 2023-2024 Andrew Gunnerson
* SPDX-License-Identifier: GPL-3.0-only
*/

package com.chiller3.rsaf

import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.Toast
import androidx.biometric.BiometricManager.Authenticators
import androidx.biometric.BiometricPrompt
import androidx.core.view.ViewCompat
import androidx.core.view.WindowInsetsCompat
import androidx.core.view.updatePadding
import androidx.preference.PreferenceFragmentCompat
import androidx.recyclerview.widget.RecyclerView

abstract class PreferenceBaseFragment : PreferenceFragmentCompat() {
companion object {
private const val INACTIVE_TIMEOUT_NS = 60_000_000_000L

// These are intentionally global to ensure that the prompt does not appear when navigating
// within the app.
private var bioAuthenticated = false
private var lastPause = 0L
}

abstract val requestTag: String

protected lateinit var prefs: Preferences
private lateinit var bioPrompt: BiometricPrompt

override fun onCreate(savedInstanceState: Bundle?) {
val activity = requireActivity()

prefs = Preferences(activity)

bioPrompt = BiometricPrompt(
this,
activity.mainExecutor,
object : BiometricPrompt.AuthenticationCallback() {
override fun onAuthenticationError(errorCode: Int, errString: CharSequence) {
Toast.makeText(
activity,
getString(R.string.biometric_error, errString),
Toast.LENGTH_LONG,
).show()
activity.finish()
}

override fun onAuthenticationSucceeded(result: BiometricPrompt.AuthenticationResult) {
bioAuthenticated = true
refreshGlobalVisibility()
}

override fun onAuthenticationFailed() {
Toast.makeText(
activity,
R.string.biometric_failure,
Toast.LENGTH_LONG,
).show()
activity.finish()
}
},
)

super.onCreate(savedInstanceState)
}

override fun onCreateRecyclerView(
inflater: LayoutInflater,
parent: ViewGroup,
savedInstanceState: Bundle?
): RecyclerView {
val view = super.onCreateRecyclerView(inflater, parent, savedInstanceState)

view.clipToPadding = false

ViewCompat.setOnApplyWindowInsetsListener(view) { v, windowInsets ->
val insets = windowInsets.getInsets(
WindowInsetsCompat.Type.systemBars()
or WindowInsetsCompat.Type.displayCutout()
)

// This is a little bit ugly in landscape mode because the divider lines for categories
// extend into the inset area. However, it's worth applying the left/right padding here
// anyway because it allows the inset area to be used for scrolling instead of just
// being a useless dead zone.
v.updatePadding(
bottom = insets.bottom,
left = insets.left,
right = insets.right,
)

WindowInsetsCompat.CONSUMED
}

return view
}

override fun onResume() {
super.onResume()

if (bioAuthenticated && (System.nanoTime() - lastPause) >= INACTIVE_TIMEOUT_NS) {
bioAuthenticated = false
}

if (!bioAuthenticated) {
if (!prefs.requireAuth) {
bioAuthenticated = true
} else {
startBiometricAuth()
}
}

refreshGlobalVisibility()
}

override fun onPause() {
super.onPause()

lastPause = System.nanoTime()
}

private fun startBiometricAuth() {
val promptInfo = BiometricPrompt.PromptInfo.Builder()
.setAllowedAuthenticators(Authenticators.BIOMETRIC_STRONG or Authenticators.DEVICE_CREDENTIAL)
.setTitle(getString(R.string.biometric_title))
.build()

bioPrompt.authenticate(promptInfo)
}

private fun refreshGlobalVisibility() {
view?.visibility = if (bioAuthenticated) {
View.VISIBLE
} else {
// Using View.GONE causes noticeable scrolling jank due to relayout.
View.INVISIBLE
}
}
}
12 changes: 11 additions & 1 deletion app/src/main/java/com/chiller3/rsaf/Preferences.kt
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ class Preferences(private val context: Context) {
const val CATEGORY_DEBUG = "debug"
const val CATEGORY_REMOTES = "remotes"

// Main preferences
const val PREF_ADD_FILE_EXTENSION = "add_file_extension"
const val PREF_ALLOW_BACKUP = "allow_backup"
const val PREF_DIALOGS_AT_BOTTOM = "dialogs_at_bottom"
Expand All @@ -26,7 +27,7 @@ class Preferences(private val context: Context) {
const val PREF_REQUIRE_AUTH = "require_auth"
const val PREF_VERBOSE_RCLONE_LOGS = "verbose_rclone_logs"

// UI actions only
// Main UI actions only
const val PREF_INHIBIT_BATTERY_OPT = "inhibit_battery_opt"
const val PREF_MISSING_NOTIFICATIONS = "missing_notifications"
const val PREF_ADD_REMOTE = "add_remote"
Expand All @@ -36,6 +37,15 @@ class Preferences(private val context: Context) {
const val PREF_SAVE_LOGS = "save_logs"
const val PREF_VERSION = "version"

// Edit remote UI actions
const val PREF_OPEN_REMOTE = "open_remote"
const val PREF_CONFIGURE_REMOTE = "configure_remote"
const val PREF_RENAME_REMOTE = "rename_remote"
const val PREF_DUPLICATE_REMOTE = "duplicate_remote"
const val PREF_DELETE_REMOTE = "delete_remote"
const val PREF_ALLOW_EXTERNAL_ACCESS = "allow_external_access"
const val PREF_DYNAMIC_SHORTCUT = "dynamic_shortcut"

// Not associated with a UI preference
const val PREF_DEBUG_MODE = "debug_mode"
private const val PREF_NEXT_NOTIFICATION_ID = "next_notification_id"
Expand Down
111 changes: 0 additions & 111 deletions app/src/main/java/com/chiller3/rsaf/dialog/EditRemoteDialogFragment.kt

This file was deleted.

Loading

0 comments on commit efb3b77

Please sign in to comment.