<lambda>null1 package io.casey.musikcube.remote.ui.settings.activity
2
3 import android.app.Dialog
4 import android.content.Context
5 import android.content.Intent
6 import android.net.Uri
7 import android.os.Bundle
8 import android.view.LayoutInflater
9 import android.view.Menu
10 import android.view.MenuItem
11 import android.view.View
12 import android.widget.*
13 import androidx.appcompat.app.AlertDialog
14 import androidx.fragment.app.DialogFragment
15 import com.uacf.taskrunner.Task
16 import com.uacf.taskrunner.Tasks
17 import io.casey.musikcube.remote.R
18 import io.casey.musikcube.remote.service.playback.PlayerWrapper
19 import io.casey.musikcube.remote.service.playback.impl.streaming.StreamProxy
20 import io.casey.musikcube.remote.ui.navigation.Transition
21 import io.casey.musikcube.remote.ui.settings.constants.Prefs
22 import io.casey.musikcube.remote.ui.settings.model.Connection
23 import io.casey.musikcube.remote.ui.settings.model.ConnectionsDb
24 import io.casey.musikcube.remote.ui.shared.activity.BaseActivity
25 import io.casey.musikcube.remote.ui.shared.extension.*
26 import io.casey.musikcube.remote.ui.shared.mixin.MetadataProxyMixin
27 import io.casey.musikcube.remote.ui.shared.mixin.PlaybackMixin
28 import java.util.*
29 import javax.inject.Inject
30 import io.casey.musikcube.remote.ui.settings.constants.Prefs.Default as Defaults
31 import io.casey.musikcube.remote.ui.settings.constants.Prefs.Key as Keys
32
33 class SettingsActivity : BaseActivity() {
34 @Inject lateinit var connectionsDb: ConnectionsDb
35 @Inject lateinit var streamProxy: StreamProxy
36
37 private lateinit var addressText: EditText
38 private lateinit var portText: EditText
39 private lateinit var httpPortText: EditText
40 private lateinit var passwordText: EditText
41 private lateinit var albumArtCheckbox: CheckBox
42 private lateinit var softwareVolume: CheckBox
43 private lateinit var sslCheckbox: CheckBox
44 private lateinit var certCheckbox: CheckBox
45 private lateinit var transferCheckbox: CheckBox
46 private lateinit var bitrateSpinner: Spinner
47 private lateinit var formatSpinner: Spinner
48 private lateinit var cacheSpinner: Spinner
49 private lateinit var titleEllipsisSpinner: Spinner
50 private lateinit var playback: PlaybackMixin
51 private lateinit var data: MetadataProxyMixin
52
53 override fun onCreate(savedInstanceState: Bundle?) {
54 data = mixin(MetadataProxyMixin())
55 playback = mixin(PlaybackMixin())
56 component.inject(this)
57 super.onCreate(savedInstanceState)
58 prefs = this.getSharedPreferences(Prefs.NAME, Context.MODE_PRIVATE)
59 setContentView(R.layout.settings_activity)
60 setTitle(R.string.settings_title)
61 cacheViews()
62 bindListeners()
63 rebindUi()
64 }
65
66 override fun onCreateOptionsMenu(menu: Menu): Boolean {
67 menuInflater.inflate(R.menu.settings_menu, menu)
68 return true
69 }
70
71 override fun onOptionsItemSelected(item: MenuItem): Boolean {
72 when (item.itemId) {
73 android.R.id.home -> {
74 finish()
75 return true
76 }
77 R.id.action_save -> {
78 save()
79 return true
80 }
81 }
82
83 return super.onOptionsItemSelected(item)
84 }
85
86 override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
87 if (requestCode == CONNECTIONS_REQUEST_CODE && resultCode == RESULT_OK) {
88 if (data != null) {
89 val connection = data.getParcelableExtra<Connection>(
90 ConnectionsActivity.EXTRA_SELECTED_CONNECTION)
91
92 if (connection != null) {
93 addressText.setText(connection.hostname)
94 passwordText.setText(connection.password)
95 portText.setText(connection.wssPort.toString())
96 httpPortText.setText(connection.httpPort.toString())
97 sslCheckbox.setCheckWithoutEvent(connection.ssl, sslCheckChanged)
98 certCheckbox.setCheckWithoutEvent(connection.noValidate, certValidationChanged)
99 }
100 }
101 }
102
103 super.onActivityResult(requestCode, resultCode, data)
104 }
105
106 override val transitionType: Transition
107 get() = Transition.Vertical
108
109 private fun rebindSpinner(spinner: Spinner, arrayResourceId: Int, key: String, defaultIndex: Int) {
110 val items = ArrayAdapter.createFromResource(this, arrayResourceId, android.R.layout.simple_spinner_item)
111 items.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item)
112 spinner.adapter = items
113 spinner.setSelection(prefs.getInt(key, defaultIndex))
114 }
115
116 private fun rebindUi() {
117 /* connection info */
118 addressText.setTextAndMoveCursorToEnd(prefs.getString(Keys.ADDRESS) ?: Defaults.ADDRESS)
119
120 portText.setTextAndMoveCursorToEnd(String.format(
121 Locale.ENGLISH, "%d", prefs.getInt(Keys.MAIN_PORT, Defaults.MAIN_PORT)))
122
123 httpPortText.setTextAndMoveCursorToEnd(String.format(
124 Locale.ENGLISH, "%d", prefs.getInt(Keys.AUDIO_PORT, Defaults.AUDIO_PORT)))
125
126 passwordText.setTextAndMoveCursorToEnd(prefs.getString(Keys.PASSWORD) ?: Defaults.PASSWORD)
127
128 /* bitrate */
129 rebindSpinner(
130 bitrateSpinner,
131 R.array.transcode_bitrate_array,
132 Keys.TRANSCODER_BITRATE_INDEX,
133 Defaults.TRANSCODER_BITRATE_INDEX)
134
135 /* format */
136 rebindSpinner(
137 formatSpinner,
138 R.array.transcode_format_array,
139 Keys.TRANSCODER_FORMAT_INDEX,
140 Defaults.TRANSCODER_FORMAT_INDEX)
141
142 /* disk cache */
143 rebindSpinner(
144 cacheSpinner,
145 R.array.disk_cache_array,
146 Keys.DISK_CACHE_SIZE_INDEX,
147 Defaults.DISK_CACHE_SIZE_INDEX)
148
149 /* title ellipsis mode */
150 rebindSpinner(
151 titleEllipsisSpinner,
152 R.array.title_ellipsis_mode_array,
153 Keys.TITLE_ELLIPSIS_MODE_INDEX,
154 Defaults.TITLE_ELLIPSIS_SIZE_INDEX)
155
156 /* advanced */
157 transferCheckbox.isChecked = prefs.getBoolean(
158 Keys.TRANSFER_TO_SERVER_ON_HEADSET_DISCONNECT,
159 Defaults.TRANSFER_TO_SERVER_ON_HEADSET_DISCONNECT)
160
161 albumArtCheckbox.isChecked = prefs.getBoolean(
162 Keys.LASTFM_ENABLED, Defaults.LASTFM_ENABLED)
163
164 softwareVolume.isChecked = prefs.getBoolean(
165 Keys.SOFTWARE_VOLUME, Defaults.SOFTWARE_VOLUME)
166
167 sslCheckbox.setCheckWithoutEvent(
168 this.prefs.getBoolean(Keys.SSL_ENABLED,Defaults.SSL_ENABLED), sslCheckChanged)
169
170 certCheckbox.setCheckWithoutEvent(
171 this.prefs.getBoolean(
172 Keys.CERT_VALIDATION_DISABLED,
173 Defaults.CERT_VALIDATION_DISABLED),
174 certValidationChanged)
175
176 enableUpNavigation()
177 }
178
179 private fun onDisableSslFromDialog() {
180 sslCheckbox.setCheckWithoutEvent(false, sslCheckChanged)
181 }
182
183 private fun onDisableCertValidationFromDialog() {
184 certCheckbox.setCheckWithoutEvent(false, certValidationChanged)
185 }
186
187 private val sslCheckChanged = { _: CompoundButton, value:Boolean ->
188 if (value) {
189 if (!dialogVisible(SslAlertDialog.TAG)) {
190 showDialog(SslAlertDialog.newInstance(), SslAlertDialog.TAG)
191 }
192 }
193 }
194
195 private val certValidationChanged = { _: CompoundButton, value: Boolean ->
196 if (value) {
197 if (!dialogVisible(DisableCertValidationAlertDialog.TAG)) {
198 showDialog(
199 DisableCertValidationAlertDialog.newInstance(),
200 DisableCertValidationAlertDialog.TAG)
201 }
202 }
203 }
204
205 private fun cacheViews() {
206 this.addressText = findViewById(R.id.address)
207 this.portText = findViewById(R.id.port)
208 this.httpPortText = findViewById(R.id.http_port)
209 this.passwordText = findViewById(R.id.password)
210 this.albumArtCheckbox = findViewById(R.id.album_art_checkbox)
211 this.softwareVolume = findViewById(R.id.software_volume)
212 this.bitrateSpinner = findViewById(R.id.transcoder_bitrate_spinner)
213 this.formatSpinner = findViewById(R.id.transcoder_format_spinner)
214 this.cacheSpinner = findViewById(R.id.streaming_disk_cache_spinner)
215 this.titleEllipsisSpinner = findViewById(R.id.title_ellipsis_mode_spinner)
216 this.sslCheckbox = findViewById(R.id.ssl_checkbox)
217 this.certCheckbox = findViewById(R.id.cert_validation)
218 this.transferCheckbox = findViewById(R.id.transfer_on_disconnect_checkbox)
219 }
220
221 private fun bindListeners() {
222 findViewById<View>(R.id.button_save_as).setOnClickListener{
223 showSaveAsDialog()
224 }
225
226 findViewById<View>(R.id.button_load).setOnClickListener{
227 startActivityForResult(
228 ConnectionsActivity.getStartIntent(this),
229 CONNECTIONS_REQUEST_CODE)
230 }
231
232 findViewById<View>(R.id.button_diagnostics).setOnClickListener {
233 startActivity(Intent(this, DiagnosticsActivity::class.java))
234 }
235 }
236
237 private fun showSaveAsDialog() {
238 if (!dialogVisible(SaveAsDialog.TAG)) {
239 showDialog(SaveAsDialog.newInstance(), SaveAsDialog.TAG)
240 }
241 }
242
243 private fun showInvalidConnectionDialog(messageId: Int = R.string.settings_invalid_connection_message) {
244 if (!dialogVisible(InvalidConnectionDialog.TAG)) {
245 showDialog(InvalidConnectionDialog.newInstance(messageId), InvalidConnectionDialog.TAG)
246 }
247 }
248
249 private fun saveAs(name: String) {
250 try {
251 val connection = Connection()
252 connection.name = name
253 connection.hostname = addressText.text.toString()
254 connection.wssPort = portText.text.toString().toInt()
255 connection.httpPort = httpPortText.text.toString().toInt()
256 connection.password = passwordText.text.toString()
257 connection.ssl = sslCheckbox.isChecked
258 connection.noValidate = certCheckbox.isChecked
259
260 if (connection.valid) {
261 runner.run(SaveAsTask.nameFor(connection), SaveAsTask(connectionsDb, connection))
262 }
263 else {
264 showInvalidConnectionDialog()
265 }
266 }
267 catch (ex: NumberFormatException) {
268 showInvalidConnectionDialog()
269 }
270 }
271
272 private fun save() {
273 val addr = addressText.text.toString()
274 val port = portText.text.toString()
275 val httpPort = httpPortText.text.toString()
276 val password = passwordText.text.toString()
277
278 try {
279 prefs.edit()
280 .putString(Keys.ADDRESS, addr)
281 .putInt(Keys.MAIN_PORT, if (port.isNotEmpty()) port.toInt() else 0)
282 .putInt(Keys.AUDIO_PORT, if (httpPort.isNotEmpty()) httpPort.toInt() else 0)
283 .putString(Keys.PASSWORD, password)
284 .putBoolean(Keys.LASTFM_ENABLED, albumArtCheckbox.isChecked)
285 .putBoolean(Keys.SOFTWARE_VOLUME, softwareVolume.isChecked)
286 .putBoolean(Keys.SSL_ENABLED, sslCheckbox.isChecked)
287 .putBoolean(Keys.CERT_VALIDATION_DISABLED, certCheckbox.isChecked)
288 .putBoolean(Keys.TRANSFER_TO_SERVER_ON_HEADSET_DISCONNECT, transferCheckbox.isChecked)
289 .putInt(Keys.TRANSCODER_BITRATE_INDEX, bitrateSpinner.selectedItemPosition)
290 .putInt(Keys.TRANSCODER_FORMAT_INDEX, formatSpinner.selectedItemPosition)
291 .putInt(Keys.DISK_CACHE_SIZE_INDEX, cacheSpinner.selectedItemPosition)
292 .putInt(Keys.TITLE_ELLIPSIS_MODE_INDEX, titleEllipsisSpinner.selectedItemPosition)
293 .apply()
294
295 if (!softwareVolume.isChecked) {
296 PlayerWrapper.setVolume(1.0f)
297 }
298
299 streamProxy.reload()
300 data.wss.disconnect()
301
302 finish()
303 }
304 catch (ex: NumberFormatException) {
305 showInvalidConnectionDialog(R.string.settings_invalid_connection_no_name_message)
306 }
307 }
308
309 override fun onTaskCompleted(taskName: String, taskId: Long, task: Task<*, *>, result: Any) {
310 if (SaveAsTask.match(taskName)) {
311 if ((result as SaveAsTask.Result) == SaveAsTask.Result.Exists) {
312 val connection = (task as SaveAsTask).connection
313 if (!dialogVisible(ConfirmOverwriteDialog.TAG)) {
314 showDialog(
315 ConfirmOverwriteDialog.newInstance(connection),
316 ConfirmOverwriteDialog.TAG)
317 }
318 }
319 else {
320 showSnackbar(
321 findViewById(android.R.id.content),
322 R.string.snackbar_saved_connection_preset)
323 }
324 }
325 }
326
327 class SslAlertDialog : DialogFragment() {
328 override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
329 val dlg = AlertDialog.Builder(activity!!)
330 .setTitle(R.string.settings_ssl_dialog_title)
331 .setMessage(R.string.settings_ssl_dialog_message)
332 .setPositiveButton(R.string.button_enable, null)
333 .setNegativeButton(R.string.button_disable) { _, _ ->
334 (activity as SettingsActivity).onDisableSslFromDialog()
335 }
336 .setNeutralButton(R.string.button_learn_more) { _, _ ->
337 try {
338 val intent = Intent(Intent.ACTION_VIEW, Uri.parse(LEARN_MORE_URL))
339 startActivity(intent)
340 }
341 catch (ex: Exception) {
342 }
343 }
344 .create()
345
346 dlg.setCancelable(false)
347 return dlg
348 }
349
350 companion object {
351 private const val LEARN_MORE_URL = "https://github.com/clangen/musikcube/wiki/ssl-server-setup"
352 const val TAG = "ssl_alert_dialog_tag"
353
354 fun newInstance(): SslAlertDialog {
355 return SslAlertDialog()
356 }
357 }
358 }
359
360 class DisableCertValidationAlertDialog : DialogFragment() {
361 override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
362 val dlg = AlertDialog.Builder(activity!!)
363 .setTitle(R.string.settings_disable_cert_validation_title)
364 .setMessage(R.string.settings_disable_cert_validation_message)
365 .setPositiveButton(R.string.button_enable, null)
366 .setNegativeButton(R.string.button_disable) { _, _ ->
367 (activity as SettingsActivity).onDisableCertValidationFromDialog()
368 }
369 .create()
370
371 dlg.setCancelable(false)
372 return dlg
373 }
374
375 companion object {
376 const val TAG = "disable_cert_verify_dialog"
377
378 fun newInstance(): DisableCertValidationAlertDialog {
379 return DisableCertValidationAlertDialog()
380 }
381 }
382 }
383
384 class InvalidConnectionDialog: DialogFragment() {
385 override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
386 val dlg = AlertDialog.Builder(activity!!)
387 .setTitle(R.string.settings_invalid_connection_title)
388 .setMessage(arguments!!.getInt(EXTRA_MESSAGE_ID))
389 .setNegativeButton(R.string.button_ok, null)
390 .create()
391
392 dlg.setCancelable(false)
393 return dlg
394 }
395
396 companion object {
397 const val TAG = "invalid_connection_dialog"
398 private const val EXTRA_MESSAGE_ID = "extra_message_id"
399 fun newInstance(messageId: Int = R.string.settings_invalid_connection_message): InvalidConnectionDialog {
400 val args = Bundle()
401 args.putInt(EXTRA_MESSAGE_ID, messageId)
402 val result = InvalidConnectionDialog()
403 result.arguments = args
404 return result
405 }
406 }
407 }
408
409 class ConfirmOverwriteDialog : DialogFragment() {
410 override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
411 val dlg = AlertDialog.Builder(activity!!)
412 .setTitle(R.string.settings_confirm_overwrite_title)
413 .setMessage(R.string.settings_confirm_overwrite_message)
414 .setNegativeButton(R.string.button_no, null)
415 .setPositiveButton(R.string.button_yes) { _, _ ->
416 when (val connection = arguments?.getParcelable<Connection>(EXTRA_CONNECTION)) {
417 null -> throw IllegalArgumentException("invalid connection")
418 else -> {
419 val db = (activity as SettingsActivity).connectionsDb
420 val saveAs = SaveAsTask(db, connection, true)
421 (activity as SettingsActivity).runner.run(SaveAsTask.nameFor(connection), saveAs)
422 }
423 }
424 }
425 .create()
426
427 dlg.setCancelable(false)
428 return dlg
429 }
430
431 companion object {
432 const val TAG = "confirm_overwrite_dialog"
433 private const val EXTRA_CONNECTION = "extra_connection"
434
435 fun newInstance(connection: Connection): ConfirmOverwriteDialog {
436 val args = Bundle()
437 args.putParcelable(EXTRA_CONNECTION, connection)
438 val result = ConfirmOverwriteDialog()
439 result.arguments = args
440 return result
441 }
442 }
443 }
444
445 class SaveAsDialog : DialogFragment() {
446 override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
447 val inflater = LayoutInflater.from(context)
448 val view = inflater.inflate(R.layout.dialog_edit, null)
449 val edit = view.findViewById<EditText>(R.id.edit)
450 edit.requestFocus()
451
452 val dlg = AlertDialog.Builder(activity!!)
453 .setTitle(R.string.settings_save_as_title)
454 .setNegativeButton(R.string.button_cancel) { _, _ -> hideKeyboard() }
455 .setOnCancelListener { hideKeyboard() }
456 .setPositiveButton(R.string.button_save) { _, _ ->
457 (activity as SettingsActivity).saveAs(edit.text.toString())
458 }
459 .create()
460
461 dlg.setView(view)
462 dlg.setCancelable(false)
463
464 return dlg
465 }
466
467 override fun onResume() {
468 super.onResume()
469 showKeyboard()
470 }
471
472 override fun onPause() {
473 super.onPause()
474 hideKeyboard()
475 }
476
477 companion object {
478 const val TAG = "save_as_dialog"
479
480 fun newInstance(): SaveAsDialog {
481 return SaveAsDialog()
482 }
483 }
484 }
485
486 companion object {
487 const val CONNECTIONS_REQUEST_CODE = 1000
488
489 fun getStartIntent(context: Context): Intent {
490 return Intent(context, SettingsActivity::class.java)
491 }
492 }
493 }
494
495 private class SaveAsTask(val db: ConnectionsDb,
496 val connection: Connection,
497 val overwrite: Boolean = false)
498 : Tasks.Blocking<SaveAsTask.Result, Exception>()
499 {
500 enum class Result { Exists, Added }
501
execnull502 override fun exec(context: Context?): Result {
503 val dao = db.connectionsDao()
504
505 if (!overwrite) {
506 val existing: Connection? = dao.query(connection.name)
507 if (existing != null) {
508 return Result.Exists
509 }
510 }
511
512 dao.insert(connection)
513 return Result.Added
514 }
515
516 companion object {
517 const val NAME = "SaveAsTask"
518
nameFornull519 fun nameFor(connection: Connection): String {
520 return "$NAME.${connection.name}"
521 }
522
matchnull523 fun match(name: String?): Boolean {
524 return name != null && name.startsWith("$NAME.")
525 }
526 }
527 }
528