mirror of
https://github.com/binwiederhier/ntfy-android.git
synced 2024-05-17 11:02:36 +12:00
WIP work towards new qr scanner dialog
This commit is contained in:
parent
078728161e
commit
add8f4b6fe
|
@ -53,9 +53,6 @@ class AddFragment : DialogFragment() {
|
|||
private lateinit var appBaseUrl: String
|
||||
private var defaultBaseUrl: String? = null
|
||||
|
||||
private lateinit var cameraExecutor: ExecutorService
|
||||
private lateinit var subscribeCameraPreview: PreviewView
|
||||
|
||||
private lateinit var subscribeView: View
|
||||
private lateinit var loginView: View
|
||||
private lateinit var positiveButton: Button
|
||||
|
@ -74,7 +71,6 @@ class AddFragment : DialogFragment() {
|
|||
private lateinit var subscribeProgress: ProgressBar
|
||||
private lateinit var subscribeErrorText: TextView
|
||||
private lateinit var subscribeErrorTextImage: View
|
||||
private lateinit var subscribeCameraPreviewPermissionText: TextView
|
||||
|
||||
// Login page
|
||||
private lateinit var loginUsernameText: TextInputEditText
|
||||
|
@ -130,8 +126,6 @@ class AddFragment : DialogFragment() {
|
|||
subscribeErrorText.visibility = View.GONE
|
||||
subscribeErrorTextImage = view.findViewById(R.id.add_dialog_subscribe_error_text_image)
|
||||
subscribeErrorTextImage.visibility = View.GONE
|
||||
subscribeCameraPreview = view.findViewById(R.id.add_dialog_subscribe_camera_preview)
|
||||
subscribeCameraPreviewPermissionText = view.findViewById(R.id.add_dialog_subscribe_camera_preview_denied_text)
|
||||
|
||||
// Fields for "login page"
|
||||
loginUsernameText = view.findViewById(R.id.add_dialog_login_username)
|
||||
|
@ -212,109 +206,11 @@ class AddFragment : DialogFragment() {
|
|||
|
||||
// Focus topic text (keyboard is shown too, see above)
|
||||
subscribeTopicText.requestFocus()
|
||||
|
||||
cameraExecutor = Executors.newSingleThreadExecutor()
|
||||
checkIfCameraPermissionIsGranted()
|
||||
}
|
||||
|
||||
return dialog
|
||||
}
|
||||
|
||||
override fun onDestroy() {
|
||||
super.onDestroy()
|
||||
cameraExecutor.shutdown()
|
||||
}
|
||||
|
||||
private fun checkCameraPermissionsRaw(): Boolean {
|
||||
return ContextCompat.checkSelfPermission(requireContext(), Manifest.permission.CAMERA) == PackageManager.PERMISSION_GRANTED
|
||||
}
|
||||
|
||||
private fun checkIfCameraPermissionIsGranted() {
|
||||
if (checkCameraPermissionsRaw()) {
|
||||
startCamera()
|
||||
} else {
|
||||
subscribeCameraPreviewPermissionText.visibility = View.VISIBLE
|
||||
val requiredPermissions = arrayOf(Manifest.permission.CAMERA)
|
||||
requestPermissions(requiredPermissions, 0)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
override fun onRequestPermissionsResult(
|
||||
requestCode: Int,
|
||||
permissions: Array<out String>,
|
||||
grantResults: IntArray
|
||||
) {
|
||||
super.onRequestPermissionsResult(requestCode, permissions, grantResults)
|
||||
|
||||
if (checkCameraPermissionsRaw()) {
|
||||
startCamera()
|
||||
}
|
||||
}
|
||||
|
||||
private fun vibratePhone(context: Context) {
|
||||
// This is deprecated, but we aren't using a high enough version for the new API
|
||||
val vibrator = context.getSystemService(Context.VIBRATOR_SERVICE) as Vibrator
|
||||
if (vibrator.hasVibrator()) {
|
||||
// Vibrate for 500 milliseconds
|
||||
vibrator.vibrate(200)
|
||||
}
|
||||
}
|
||||
|
||||
private fun startCamera() {
|
||||
val cameraProviderFuture = ProcessCameraProvider.getInstance(requireContext())
|
||||
|
||||
subscribeCameraPreviewPermissionText.visibility = View.GONE
|
||||
|
||||
cameraProviderFuture.addListener({
|
||||
val cameraProvider = cameraProviderFuture.get()
|
||||
|
||||
val preview = Preview.Builder()
|
||||
.build()
|
||||
.also {
|
||||
it.setSurfaceProvider(subscribeCameraPreview.surfaceProvider)
|
||||
}
|
||||
|
||||
val imageAnalyzer = ImageAnalysis.Builder()
|
||||
.setBackpressureStrategy(ImageAnalysis.STRATEGY_KEEP_ONLY_LATEST)
|
||||
.build()
|
||||
.also {
|
||||
it.setAnalyzer(cameraExecutor, QrCodeAnalyzer { urlString ->
|
||||
var url = URL(urlString)
|
||||
var baseUrl = "${url.protocol}://${url.host}"
|
||||
var route = url.path.replaceFirst("/", "")
|
||||
|
||||
subscribeUseAnotherServerCheckbox.isChecked = true
|
||||
subscribeBaseUrlText.setText(baseUrl)
|
||||
subscribeTopicText.setText(route)
|
||||
validateInputSubscribeView {
|
||||
if (positiveButton.isEnabled) {
|
||||
positiveButtonClick()
|
||||
vibratePhone(requireContext())
|
||||
}
|
||||
}
|
||||
|
||||
})
|
||||
}
|
||||
|
||||
// Select back camera as a default
|
||||
val cameraSelector = CameraSelector.DEFAULT_BACK_CAMERA
|
||||
|
||||
try {
|
||||
// Unbind use cases before rebinding
|
||||
cameraProvider.unbindAll()
|
||||
|
||||
// Bind use cases to camera
|
||||
cameraProvider.bindToLifecycle(
|
||||
this, cameraSelector, preview, imageAnalyzer
|
||||
)
|
||||
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
}
|
||||
}, ContextCompat.getMainExecutor(requireContext()))
|
||||
}
|
||||
|
||||
private fun positiveButtonClick() {
|
||||
val topic = subscribeTopicText.text.toString()
|
||||
val baseUrl = getBaseUrl()
|
||||
|
|
510
app/src/main/java/io/heckel/ntfy/ui/AddQrFragment.kt
Normal file
510
app/src/main/java/io/heckel/ntfy/ui/AddQrFragment.kt
Normal file
|
@ -0,0 +1,510 @@
|
|||
package io.heckel.ntfy.ui
|
||||
|
||||
import android.Manifest
|
||||
import android.annotation.SuppressLint
|
||||
import android.app.Activity
|
||||
import android.app.AlertDialog
|
||||
import android.app.Dialog
|
||||
import android.content.Context
|
||||
import android.content.DialogInterface
|
||||
import android.content.pm.PackageManager
|
||||
import android.os.Bundle
|
||||
import android.os.VibrationEffect
|
||||
import android.os.Vibrator
|
||||
import android.os.VibratorManager
|
||||
import android.util.SparseArray
|
||||
import android.view.SurfaceHolder
|
||||
import android.view.SurfaceView
|
||||
import android.view.View
|
||||
import android.view.WindowManager
|
||||
import android.view.inputmethod.InputMethodManager
|
||||
import android.widget.*
|
||||
import androidx.camera.core.CameraSelector
|
||||
import androidx.camera.core.ImageAnalysis
|
||||
import androidx.camera.core.Preview
|
||||
import androidx.camera.lifecycle.ProcessCameraProvider
|
||||
import androidx.camera.view.PreviewView
|
||||
import androidx.core.app.ActivityCompat
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.fragment.app.DialogFragment
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
||||
import com.google.android.material.textfield.TextInputEditText
|
||||
import com.google.android.material.textfield.TextInputLayout
|
||||
import io.heckel.ntfy.BuildConfig
|
||||
import io.heckel.ntfy.R
|
||||
import io.heckel.ntfy.db.Repository
|
||||
import io.heckel.ntfy.db.User
|
||||
import io.heckel.ntfy.msg.ApiService
|
||||
import io.heckel.ntfy.util.*
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import java.io.IOException
|
||||
import java.net.URL
|
||||
import java.util.concurrent.ExecutorService
|
||||
import java.util.concurrent.Executors
|
||||
|
||||
|
||||
class AddQrFragment : DialogFragment() {
|
||||
private val api = ApiService()
|
||||
|
||||
private lateinit var repository: Repository
|
||||
private lateinit var subscribeListener: SubscribeListener
|
||||
private lateinit var appBaseUrl: String
|
||||
private var defaultBaseUrl: String? = null
|
||||
|
||||
private lateinit var cameraExecutor: ExecutorService
|
||||
private lateinit var subscribeCameraPreview: PreviewView
|
||||
|
||||
private lateinit var subscribeView: View
|
||||
private lateinit var loginView: View
|
||||
private lateinit var positiveButton: Button
|
||||
private lateinit var negativeButton: Button
|
||||
|
||||
// Subscribe page
|
||||
private lateinit var subscribeInstantDeliveryBox: View
|
||||
private lateinit var subscribeInstantDeliveryCheckbox: CheckBox
|
||||
private lateinit var subscribeInstantDeliveryDescription: View
|
||||
private lateinit var subscribeForegroundDescription: TextView
|
||||
private lateinit var subscribeProgress: ProgressBar
|
||||
private lateinit var subscribeErrorText: TextView
|
||||
private lateinit var subscribeErrorTextImage: View
|
||||
private lateinit var subscribeCameraPreviewPermissionText: TextView
|
||||
|
||||
// Login page
|
||||
private lateinit var loginUsernameText: TextInputEditText
|
||||
private lateinit var loginPasswordText: TextInputEditText
|
||||
private lateinit var loginProgress: ProgressBar
|
||||
private lateinit var loginErrorText: TextView
|
||||
private lateinit var loginErrorTextImage: View
|
||||
|
||||
interface SubscribeListener {
|
||||
fun onSubscribe(topic: String, baseUrl: String, instant: Boolean)
|
||||
}
|
||||
|
||||
override fun onAttach(context: Context) {
|
||||
super.onAttach(context)
|
||||
subscribeListener = activity as SubscribeListener
|
||||
}
|
||||
|
||||
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
|
||||
if (activity == null) {
|
||||
throw IllegalStateException("Activity cannot be null")
|
||||
}
|
||||
|
||||
// Dependencies (Fragments need a default constructor)
|
||||
repository = Repository.getInstance(requireActivity())
|
||||
appBaseUrl = getString(R.string.app_base_url)
|
||||
defaultBaseUrl = repository.getDefaultBaseUrl()
|
||||
|
||||
// Build root view
|
||||
val view = requireActivity().layoutInflater.inflate(R.layout.fragment_add_qr_dialog, null)
|
||||
|
||||
// Main "pages"
|
||||
subscribeView = view.findViewById(R.id.add_dialog_subscribe_view)
|
||||
subscribeView.visibility = View.VISIBLE
|
||||
loginView = view.findViewById(R.id.add_dialog_login_view)
|
||||
loginView.visibility = View.GONE
|
||||
|
||||
// Fields for "subscribe page"
|
||||
subscribeInstantDeliveryBox = view.findViewById(R.id.add_dialog_subscribe_instant_delivery_box)
|
||||
subscribeInstantDeliveryCheckbox = view.findViewById(R.id.add_dialog_subscribe_instant_delivery_checkbox)
|
||||
subscribeInstantDeliveryDescription = view.findViewById(R.id.add_dialog_subscribe_instant_delivery_description)
|
||||
subscribeForegroundDescription = view.findViewById(R.id.add_dialog_subscribe_foreground_description)
|
||||
subscribeProgress = view.findViewById(R.id.add_dialog_subscribe_progress)
|
||||
subscribeErrorText = view.findViewById(R.id.add_dialog_subscribe_error_text)
|
||||
subscribeErrorText.visibility = View.GONE
|
||||
subscribeErrorTextImage = view.findViewById(R.id.add_dialog_subscribe_error_text_image)
|
||||
subscribeErrorTextImage.visibility = View.GONE
|
||||
subscribeCameraPreview = view.findViewById(R.id.add_dialog_subscribe_camera_preview)
|
||||
subscribeCameraPreviewPermissionText = view.findViewById(R.id.add_dialog_subscribe_camera_preview_denied_text)
|
||||
|
||||
// Fields for "login page"
|
||||
loginUsernameText = view.findViewById(R.id.add_dialog_login_username)
|
||||
loginPasswordText = view.findViewById(R.id.add_dialog_login_password)
|
||||
loginProgress = view.findViewById(R.id.add_dialog_login_progress)
|
||||
loginErrorText = view.findViewById(R.id.add_dialog_login_error_text)
|
||||
loginErrorTextImage = view.findViewById(R.id.add_dialog_login_error_text_image)
|
||||
|
||||
// Set foreground description text
|
||||
subscribeForegroundDescription.text = getString(R.string.add_dialog_foreground_description, shortUrl(appBaseUrl))
|
||||
|
||||
// Show/hide based on flavor (faster shortcut for validateInputSubscribeView, which can only run onShow)
|
||||
if (!BuildConfig.FIREBASE_AVAILABLE) {
|
||||
subscribeInstantDeliveryBox.visibility = View.GONE
|
||||
}
|
||||
|
||||
// Username/password validation on type
|
||||
val loginTextWatcher = AfterChangedTextWatcher {
|
||||
validateInputLoginView()
|
||||
}
|
||||
loginUsernameText.addTextChangedListener(loginTextWatcher)
|
||||
loginPasswordText.addTextChangedListener(loginTextWatcher)
|
||||
|
||||
// Build dialog
|
||||
val dialog = AlertDialog.Builder(activity)
|
||||
.setView(view)
|
||||
.setPositiveButton(R.string.add_dialog_button_subscribe) { _, _ ->
|
||||
// This will be overridden below to avoid closing the dialog immediately
|
||||
}
|
||||
.setNegativeButton(R.string.add_dialog_button_cancel) { _, _ ->
|
||||
// This will be overridden below
|
||||
}
|
||||
.create()
|
||||
|
||||
// Show keyboard when the dialog is shown (see https://stackoverflow.com/a/19573049/1440785)
|
||||
dialog.window?.setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_STATE_VISIBLE)
|
||||
|
||||
// Add logic to disable "Subscribe" button on invalid input
|
||||
dialog.setOnShowListener {
|
||||
positiveButton = dialog.getButton(AlertDialog.BUTTON_POSITIVE)
|
||||
positiveButton.isEnabled = false
|
||||
positiveButton.setOnClickListener {
|
||||
positiveButtonClick()
|
||||
}
|
||||
negativeButton = dialog.getButton(AlertDialog.BUTTON_NEGATIVE)
|
||||
negativeButton.setOnClickListener {
|
||||
negativeButtonClick()
|
||||
}
|
||||
|
||||
subscribeInstantDeliveryCheckbox.setOnCheckedChangeListener { _, _ ->
|
||||
validateInputSubscribeView()
|
||||
}
|
||||
|
||||
cameraExecutor = Executors.newSingleThreadExecutor()
|
||||
checkIfCameraPermissionIsGranted()
|
||||
}
|
||||
|
||||
return dialog
|
||||
}
|
||||
|
||||
override fun onDestroy() {
|
||||
super.onDestroy()
|
||||
cameraExecutor.shutdown()
|
||||
}
|
||||
|
||||
private fun checkCameraPermissionsRaw(): Boolean {
|
||||
return ContextCompat.checkSelfPermission(requireContext(), Manifest.permission.CAMERA) == PackageManager.PERMISSION_GRANTED
|
||||
}
|
||||
|
||||
private fun checkIfCameraPermissionIsGranted() {
|
||||
if (checkCameraPermissionsRaw()) {
|
||||
startCamera()
|
||||
} else {
|
||||
subscribeCameraPreviewPermissionText.visibility = View.VISIBLE
|
||||
val requiredPermissions = arrayOf(Manifest.permission.CAMERA)
|
||||
requestPermissions(requiredPermissions, 0)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
override fun onRequestPermissionsResult(
|
||||
requestCode: Int,
|
||||
permissions: Array<out String>,
|
||||
grantResults: IntArray
|
||||
) {
|
||||
super.onRequestPermissionsResult(requestCode, permissions, grantResults)
|
||||
|
||||
if (checkCameraPermissionsRaw()) {
|
||||
startCamera()
|
||||
}
|
||||
}
|
||||
|
||||
private fun vibratePhone(context: Context) {
|
||||
// This is deprecated, but we aren't using a high enough version for the new API
|
||||
val vibrator = context.getSystemService(Context.VIBRATOR_SERVICE) as Vibrator
|
||||
if (vibrator.hasVibrator()) {
|
||||
// Vibrate for 500 milliseconds
|
||||
vibrator.vibrate(200)
|
||||
}
|
||||
}
|
||||
|
||||
private fun startCamera() {
|
||||
val cameraProviderFuture = ProcessCameraProvider.getInstance(requireContext())
|
||||
|
||||
subscribeCameraPreviewPermissionText.visibility = View.GONE
|
||||
|
||||
cameraProviderFuture.addListener({
|
||||
val cameraProvider = cameraProviderFuture.get()
|
||||
|
||||
val preview = Preview.Builder()
|
||||
.build()
|
||||
.also {
|
||||
it.setSurfaceProvider(subscribeCameraPreview.surfaceProvider)
|
||||
}
|
||||
|
||||
val imageAnalyzer = ImageAnalysis.Builder()
|
||||
.setBackpressureStrategy(ImageAnalysis.STRATEGY_KEEP_ONLY_LATEST)
|
||||
.build()
|
||||
.also {
|
||||
it.setAnalyzer(cameraExecutor, QrCodeAnalyzer { urlString ->
|
||||
var url = URL(urlString)
|
||||
var baseUrl = "${url.protocol}://${url.host}"
|
||||
var route = url.path.replaceFirst("/", "")
|
||||
|
||||
subscribeBaseUrlText.setText(baseUrl)
|
||||
subscribeTopicText.setText(route)
|
||||
validateInputSubscribeView {
|
||||
if (positiveButton.isEnabled) {
|
||||
positiveButtonClick()
|
||||
vibratePhone(requireContext())
|
||||
}
|
||||
}
|
||||
|
||||
})
|
||||
}
|
||||
|
||||
// Select back camera as a default
|
||||
val cameraSelector = CameraSelector.DEFAULT_BACK_CAMERA
|
||||
|
||||
try {
|
||||
// Unbind use cases before rebinding
|
||||
cameraProvider.unbindAll()
|
||||
|
||||
// Bind use cases to camera
|
||||
cameraProvider.bindToLifecycle(
|
||||
this, cameraSelector, preview, imageAnalyzer
|
||||
)
|
||||
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
}
|
||||
}, ContextCompat.getMainExecutor(requireContext()))
|
||||
}
|
||||
|
||||
private fun positiveButtonClick() {
|
||||
val topic = subscribeTopicText.text.toString()
|
||||
val baseUrl = getBaseUrl()
|
||||
if (subscribeView.visibility == View.VISIBLE) {
|
||||
checkReadAndMaybeShowLogin(baseUrl, topic)
|
||||
} else if (loginView.visibility == View.VISIBLE) {
|
||||
loginAndMaybeDismiss(baseUrl, topic)
|
||||
}
|
||||
}
|
||||
|
||||
private fun checkReadAndMaybeShowLogin(baseUrl: String, topic: String) {
|
||||
subscribeProgress.visibility = View.VISIBLE
|
||||
subscribeErrorText.visibility = View.GONE
|
||||
subscribeErrorTextImage.visibility = View.GONE
|
||||
enableSubscribeView(false)
|
||||
lifecycleScope.launch(Dispatchers.IO) {
|
||||
try {
|
||||
val user = repository.getUser(baseUrl) // May be null
|
||||
val authorized = api.checkAuth(baseUrl, topic, user)
|
||||
if (authorized) {
|
||||
Log.d(TAG, "Access granted to topic ${topicUrl(baseUrl, topic)}")
|
||||
dismissDialog()
|
||||
} else {
|
||||
if (user != null) {
|
||||
Log.w(TAG, "Access not allowed to topic ${topicUrl(baseUrl, topic)}, but user already exists")
|
||||
showErrorAndReenableSubscribeView(getString(R.string.add_dialog_login_error_not_authorized, user.username))
|
||||
} else {
|
||||
Log.w(TAG, "Access not allowed to topic ${topicUrl(baseUrl, topic)}, showing login dialog")
|
||||
val activity = activity ?: return@launch // We may have pressed "Cancel"
|
||||
activity.runOnUiThread {
|
||||
showLoginView(activity)
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Log.w(TAG, "Connection to topic failed: ${e.message}", e)
|
||||
showErrorAndReenableSubscribeView(e.message)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun showErrorAndReenableSubscribeView(message: String?) {
|
||||
val activity = activity ?: return // We may have pressed "Cancel"
|
||||
activity.runOnUiThread {
|
||||
subscribeProgress.visibility = View.GONE
|
||||
subscribeErrorText.visibility = View.VISIBLE
|
||||
subscribeErrorText.text = message
|
||||
subscribeErrorTextImage.visibility = View.VISIBLE
|
||||
enableSubscribeView(true)
|
||||
}
|
||||
}
|
||||
|
||||
private fun loginAndMaybeDismiss(baseUrl: String, topic: String) {
|
||||
loginProgress.visibility = View.VISIBLE
|
||||
loginErrorText.visibility = View.GONE
|
||||
loginErrorTextImage.visibility = View.GONE
|
||||
enableLoginView(false)
|
||||
val user = User(
|
||||
baseUrl = baseUrl,
|
||||
username = loginUsernameText.text.toString(),
|
||||
password = loginPasswordText.text.toString()
|
||||
)
|
||||
lifecycleScope.launch(Dispatchers.IO) {
|
||||
Log.d(TAG, "Checking read access for user ${user.username} to topic ${topicUrl(baseUrl, topic)}")
|
||||
try {
|
||||
val authorized = api.checkAuth(baseUrl, topic, user)
|
||||
if (authorized) {
|
||||
Log.d(TAG, "Access granted for user ${user.username} to topic ${topicUrl(baseUrl, topic)}, adding to database")
|
||||
repository.addUser(user)
|
||||
dismissDialog()
|
||||
} else {
|
||||
Log.w(TAG, "Access not allowed for user ${user.username} to topic ${topicUrl(baseUrl, topic)}")
|
||||
showErrorAndReenableLoginView(getString(R.string.add_dialog_login_error_not_authorized, user.username))
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Log.w(TAG, "Connection to topic failed during login: ${e.message}", e)
|
||||
showErrorAndReenableLoginView(e.message)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun showErrorAndReenableLoginView(message: String?) {
|
||||
val activity = activity ?: return // We may have pressed "Cancel"
|
||||
activity.runOnUiThread {
|
||||
loginProgress.visibility = View.GONE
|
||||
loginErrorText.visibility = View.VISIBLE
|
||||
loginErrorText.text = message
|
||||
loginErrorTextImage.visibility = View.VISIBLE
|
||||
enableLoginView(true)
|
||||
}
|
||||
}
|
||||
|
||||
private fun negativeButtonClick() {
|
||||
if (subscribeView.visibility == View.VISIBLE) {
|
||||
dialog?.cancel()
|
||||
} else if (loginView.visibility == View.VISIBLE) {
|
||||
showSubscribeView()
|
||||
}
|
||||
}
|
||||
|
||||
private fun validateInputSubscribeView(onCompletion: () -> Unit = {}) {
|
||||
if (!this::positiveButton.isInitialized) return // As per crash seen in Google Play
|
||||
|
||||
// Show/hide things: This logic is intentionally kept simple. Do not simplify "just because it's pretty".
|
||||
//TODO: Phil check this
|
||||
val instantToggleAllowed = if (!BuildConfig.FIREBASE_AVAILABLE) {
|
||||
false
|
||||
} else if (defaultBaseUrl == null) {
|
||||
true
|
||||
} else {
|
||||
false
|
||||
}
|
||||
|
||||
if (instantToggleAllowed) {
|
||||
subscribeInstantDeliveryBox.visibility = View.VISIBLE
|
||||
subscribeInstantDeliveryDescription.visibility = if (subscribeInstantDeliveryCheckbox.isChecked) View.VISIBLE else View.GONE
|
||||
subscribeForegroundDescription.visibility = View.GONE
|
||||
} else {
|
||||
subscribeInstantDeliveryBox.visibility = View.GONE
|
||||
subscribeInstantDeliveryDescription.visibility = View.GONE
|
||||
subscribeForegroundDescription.visibility = if (BuildConfig.FIREBASE_AVAILABLE) View.VISIBLE else View.GONE
|
||||
}
|
||||
|
||||
// Enable/disable "Subscribe" button
|
||||
lifecycleScope.launch(Dispatchers.IO) {
|
||||
val baseUrl = getBaseUrl()
|
||||
val topic = subscribeTopicText.text.toString()
|
||||
val subscription = repository.getSubscription(baseUrl, topic)
|
||||
|
||||
activity?.let {
|
||||
it.runOnUiThread {
|
||||
if (subscription != null || DISALLOWED_TOPICS.contains(topic)) {
|
||||
positiveButton.isEnabled = false
|
||||
} else if (subscribeUseAnotherServerCheckbox.isChecked) {
|
||||
positiveButton.isEnabled = validTopic(topic) && validUrl(baseUrl)
|
||||
} else {
|
||||
positiveButton.isEnabled = validTopic(topic)
|
||||
}
|
||||
|
||||
onCompletion()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun validateInputLoginView() {
|
||||
if (!this::positiveButton.isInitialized || !this::loginUsernameText.isInitialized || !this::loginPasswordText.isInitialized) {
|
||||
return // As per crash seen in Google Play
|
||||
}
|
||||
if (loginUsernameText.visibility == View.GONE) {
|
||||
positiveButton.isEnabled = true
|
||||
} else {
|
||||
positiveButton.isEnabled = (loginUsernameText.text?.isNotEmpty() ?: false)
|
||||
&& (loginPasswordText.text?.isNotEmpty() ?: false)
|
||||
}
|
||||
}
|
||||
|
||||
private fun dismissDialog() {
|
||||
Log.d(TAG, "Closing dialog and calling onSubscribe handler")
|
||||
val activity = activity?: return // We may have pressed "Cancel"
|
||||
activity.runOnUiThread {
|
||||
val topic = subscribeTopicText.text.toString()
|
||||
val baseUrl = getBaseUrl()
|
||||
val instant = !BuildConfig.FIREBASE_AVAILABLE || baseUrl != appBaseUrl || subscribeInstantDeliveryCheckbox.isChecked
|
||||
subscribeListener.onSubscribe(topic, baseUrl, instant)
|
||||
dialog?.dismiss()
|
||||
}
|
||||
}
|
||||
|
||||
private fun getBaseUrl(): String {
|
||||
return if (subscribeUseAnotherServerCheckbox.isChecked) {
|
||||
subscribeBaseUrlText.text.toString()
|
||||
} else {
|
||||
return defaultBaseUrl ?: appBaseUrl
|
||||
}
|
||||
}
|
||||
|
||||
private fun showSubscribeView() {
|
||||
resetSubscribeView()
|
||||
positiveButton.text = getString(R.string.add_dialog_button_subscribe)
|
||||
negativeButton.text = getString(R.string.add_dialog_button_cancel)
|
||||
loginView.visibility = View.GONE
|
||||
subscribeView.visibility = View.VISIBLE
|
||||
}
|
||||
|
||||
private fun showLoginView(activity: Activity) {
|
||||
resetLoginView()
|
||||
loginProgress.visibility = View.INVISIBLE
|
||||
positiveButton.text = getString(R.string.add_dialog_button_login)
|
||||
negativeButton.text = getString(R.string.add_dialog_button_back)
|
||||
subscribeView.visibility = View.GONE
|
||||
loginView.visibility = View.VISIBLE
|
||||
if (loginUsernameText.requestFocus()) {
|
||||
val imm = activity.getSystemService(Context.INPUT_METHOD_SERVICE) as? InputMethodManager
|
||||
imm?.showSoftInput(loginUsernameText, InputMethodManager.SHOW_IMPLICIT)
|
||||
}
|
||||
}
|
||||
|
||||
private fun enableSubscribeView(enable: Boolean) {
|
||||
subscribeInstantDeliveryCheckbox.isEnabled = enable
|
||||
positiveButton.isEnabled = enable
|
||||
}
|
||||
|
||||
private fun resetSubscribeView() {
|
||||
subscribeProgress.visibility = View.GONE
|
||||
subscribeErrorText.visibility = View.GONE
|
||||
subscribeErrorTextImage.visibility = View.GONE
|
||||
enableSubscribeView(true)
|
||||
}
|
||||
|
||||
private fun enableLoginView(enable: Boolean) {
|
||||
loginUsernameText.isEnabled = enable
|
||||
loginPasswordText.isEnabled = enable
|
||||
positiveButton.isEnabled = enable
|
||||
if (enable && loginUsernameText.requestFocus()) {
|
||||
val imm = activity?.getSystemService(Context.INPUT_METHOD_SERVICE) as? InputMethodManager
|
||||
imm?.showSoftInput(loginUsernameText, InputMethodManager.SHOW_IMPLICIT)
|
||||
}
|
||||
}
|
||||
|
||||
private fun resetLoginView() {
|
||||
loginProgress.visibility = View.GONE
|
||||
loginErrorText.visibility = View.GONE
|
||||
loginErrorTextImage.visibility = View.GONE
|
||||
loginUsernameText.visibility = View.VISIBLE
|
||||
loginUsernameText.text?.clear()
|
||||
loginPasswordText.visibility = View.VISIBLE
|
||||
loginPasswordText.text?.clear()
|
||||
enableLoginView(true)
|
||||
}
|
||||
|
||||
companion object {
|
||||
const val TAG = "NtfyAddFragment"
|
||||
private val DISALLOWED_TOPICS = listOf("docs", "static", "file") // If updated, also update in server
|
||||
}
|
||||
}
|
|
@ -114,8 +114,7 @@ class MainActivity : AppCompatActivity(), ActionMode.Callback, AddFragment.Subsc
|
|||
}
|
||||
|
||||
fabAddQr.setOnClickListener {
|
||||
//TODO: QR dialog instead
|
||||
onSubscribeButtonClick()
|
||||
onSubscribeQrButtonClick()
|
||||
}
|
||||
|
||||
// Swipe to refresh
|
||||
|
@ -514,6 +513,11 @@ class MainActivity : AppCompatActivity(), ActionMode.Callback, AddFragment.Subsc
|
|||
newFragment.show(supportFragmentManager, AddFragment.TAG)
|
||||
}
|
||||
|
||||
private fun onSubscribeQrButtonClick() {
|
||||
val newFragment = AddQrFragment()
|
||||
newFragment.show(supportFragmentManager, AddFragment.TAG)
|
||||
}
|
||||
|
||||
override fun onSubscribe(topic: String, baseUrl: String, instant: Boolean) {
|
||||
Log.d(TAG, "Adding subscription ${topicShortUrl(baseUrl, topic)} (instant = $instant)")
|
||||
|
||||
|
|
|
@ -154,31 +154,6 @@
|
|||
app:layout_constraintStart_toEndOf="@id/add_dialog_subscribe_error_text_image"
|
||||
android:layout_marginTop="5dp"
|
||||
tools:visibility="gone"/>
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical"
|
||||
android:paddingTop="16dp"
|
||||
android:paddingBottom="16dp"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@id/add_dialog_subscribe_error_text">
|
||||
<TextView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="Or Scan ntfy QR code:" />
|
||||
<androidx.camera.view.PreviewView
|
||||
android:id="@+id/add_dialog_subscribe_camera_preview"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="300dp"
|
||||
android:background="#000" />
|
||||
<TextView
|
||||
android:id="@+id/add_dialog_subscribe_camera_preview_denied_text"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="Camera permission is required to scan QR codes."
|
||||
android:visibility="gone" />
|
||||
</LinearLayout>
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
</ScrollView>
|
||||
<ScrollView
|
||||
|
|
189
app/src/main/res/layout/fragment_add_qr_dialog.xml
Normal file
189
app/src/main/res/layout/fragment_add_qr_dialog.xml
Normal file
|
@ -0,0 +1,189 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<LinearLayout
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:orientation="vertical"
|
||||
android:paddingLeft="16dp"
|
||||
android:paddingRight="16dp">
|
||||
<ScrollView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:id="@+id/add_dialog_subscribe_view">
|
||||
<androidx.constraintlayout.widget.ConstraintLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="horizontal">
|
||||
<TextView
|
||||
android:id="@+id/add_dialog_subscribe_title"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:paddingTop="16dp"
|
||||
android:paddingBottom="3dp"
|
||||
android:text="@string/add_dialog_title"
|
||||
android:textAlignment="viewStart"
|
||||
android:textAppearance="@style/TextAppearance.AppCompat.Large" android:paddingStart="4dp"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
/>
|
||||
<ProgressBar
|
||||
style="?android:attr/progressBarStyle"
|
||||
android:layout_width="24dp"
|
||||
android:layout_height="24dp"
|
||||
android:id="@+id/add_dialog_subscribe_progress"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintBottom_toTopOf="@+id/add_dialog_subscribe_title"
|
||||
android:indeterminate="true" android:layout_marginBottom="5dp" android:visibility="gone"/>
|
||||
<LinearLayout
|
||||
android:orientation="horizontal"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content" android:id="@+id/add_dialog_subscribe_instant_delivery_box"
|
||||
app:layout_constraintStart_toStartOf="parent" app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@id/add_dialog_subscribe_title" android:layout_marginTop="6dp">
|
||||
<CheckBox
|
||||
android:text="@string/add_dialog_instant_delivery"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content" android:id="@+id/add_dialog_subscribe_instant_delivery_checkbox"
|
||||
android:layout_marginTop="-8dp" android:layout_marginBottom="-5dp"
|
||||
android:layout_marginStart="-3dp"/>
|
||||
<ImageView
|
||||
android:layout_width="24dp"
|
||||
android:layout_height="24dp" app:srcCompat="@drawable/ic_bolt_gray_24dp"
|
||||
android:id="@+id/add_dialog_subscribe_instant_image"
|
||||
app:layout_constraintTop_toTopOf="@+id/main_item_text"
|
||||
app:layout_constraintEnd_toStartOf="@+id/main_item_date" android:paddingTop="3dp"
|
||||
android:layout_marginTop="3dp"/>
|
||||
</LinearLayout>
|
||||
<TextView
|
||||
android:text="@string/add_dialog_instant_delivery_description"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content" android:id="@+id/add_dialog_subscribe_instant_delivery_description"
|
||||
android:paddingStart="4dp" android:paddingTop="0dp"
|
||||
android:visibility="gone" app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@id/add_dialog_subscribe_instant_delivery_box"/>
|
||||
<TextView
|
||||
android:text="@string/add_dialog_foreground_description"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content" android:id="@+id/add_dialog_subscribe_foreground_description"
|
||||
android:paddingStart="4dp" app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@id/add_dialog_subscribe_instant_delivery_description"/>
|
||||
<ImageView
|
||||
android:layout_width="20dp"
|
||||
android:layout_height="20dp" app:srcCompat="@drawable/ic_error_red_24dp"
|
||||
android:id="@+id/add_dialog_subscribe_error_text_image"
|
||||
android:visibility="gone"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="@id/add_dialog_subscribe_error_text"
|
||||
android:layout_marginTop="1dp"/>
|
||||
<TextView
|
||||
android:text="Unable to resolve host example.com"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content" android:id="@+id/add_dialog_subscribe_error_text"
|
||||
android:paddingStart="4dp"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@id/add_dialog_subscribe_foreground_description"
|
||||
android:paddingEnd="4dp"
|
||||
android:textAppearance="@style/DangerText"
|
||||
app:layout_constraintStart_toEndOf="@id/add_dialog_subscribe_error_text_image"
|
||||
android:layout_marginTop="5dp"
|
||||
tools:visibility="gone"/>
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical"
|
||||
android:paddingTop="16dp"
|
||||
android:paddingBottom="16dp"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@id/add_dialog_subscribe_error_text">
|
||||
<TextView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="Scan ntfy QR code:" />
|
||||
<androidx.camera.view.PreviewView
|
||||
android:id="@+id/add_dialog_subscribe_camera_preview"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="300dp"
|
||||
android:background="#000" />
|
||||
<TextView
|
||||
android:id="@+id/add_dialog_subscribe_camera_preview_denied_text"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="Camera permission is required to scan QR codes."
|
||||
android:visibility="gone" />
|
||||
</LinearLayout>
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
</ScrollView>
|
||||
<ScrollView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:id="@+id/add_dialog_login_view">
|
||||
<androidx.constraintlayout.widget.ConstraintLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="horizontal">
|
||||
<TextView
|
||||
android:id="@+id/add_dialog_login_title"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:paddingTop="16dp"
|
||||
android:paddingBottom="3dp"
|
||||
android:text="@string/add_dialog_login_title"
|
||||
android:textAlignment="viewStart"
|
||||
android:textAppearance="@style/TextAppearance.AppCompat.Large" android:paddingStart="4dp"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
/>
|
||||
<TextView
|
||||
android:text="@string/add_dialog_login_description"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content" android:id="@+id/add_dialog_login_description"
|
||||
android:paddingStart="4dp" android:paddingTop="3dp" app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@id/add_dialog_login_title" android:paddingEnd="4dp"/>
|
||||
<com.google.android.material.textfield.TextInputEditText
|
||||
android:id="@+id/add_dialog_login_username"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content" android:hint="@string/add_dialog_login_username_hint"
|
||||
android:importantForAutofill="no"
|
||||
android:maxLines="1" android:inputType="text" android:maxLength="64"
|
||||
app:layout_constraintStart_toStartOf="parent" app:layout_constraintEnd_toEndOf="parent"
|
||||
android:layout_marginTop="10dp" app:layout_constraintTop_toBottomOf="@+id/add_dialog_login_description"/>
|
||||
<com.google.android.material.textfield.TextInputEditText
|
||||
android:id="@+id/add_dialog_login_password"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content" android:hint="@string/add_dialog_login_password_hint"
|
||||
android:importantForAutofill="no"
|
||||
android:maxLines="1" android:inputType="textPassword" app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@id/add_dialog_login_username"/>
|
||||
<ImageView
|
||||
android:layout_width="20dp"
|
||||
android:layout_height="20dp" app:srcCompat="@drawable/ic_error_red_24dp"
|
||||
android:id="@+id/add_dialog_login_error_text_image"
|
||||
android:visibility="visible"
|
||||
app:layout_constraintStart_toStartOf="parent" app:layout_constraintBottom_toBottomOf="@+id/add_dialog_login_error_text" app:layout_constraintTop_toTopOf="@+id/add_dialog_login_error_text"/>
|
||||
<TextView
|
||||
android:text="Login failed. User not authorized."
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content" android:id="@+id/add_dialog_login_error_text"
|
||||
android:paddingStart="4dp"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@id/add_dialog_login_password"
|
||||
android:paddingEnd="4dp"
|
||||
android:textAppearance="@style/DangerText"
|
||||
app:layout_constraintStart_toEndOf="@id/add_dialog_login_error_text_image"/>
|
||||
<ProgressBar
|
||||
style="?android:attr/progressBarStyle"
|
||||
android:layout_width="25dp"
|
||||
android:layout_height="25dp"
|
||||
android:id="@+id/add_dialog_login_progress"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintBottom_toTopOf="@+id/add_dialog_login_description"
|
||||
android:indeterminate="true" android:layout_marginBottom="5dp"/>
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
</ScrollView>
|
||||
</LinearLayout>
|
Loading…
Reference in a new issue