<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