SettingsFragment.kt 22.3 KB
Newer Older
Daniel Wolf's avatar
Daniel Wolf committed
1
2
package com.frostnerd.smokescreen.fragment

3
import android.annotation.SuppressLint
4
import android.content.Context
5
import android.content.Intent
6
import android.content.SharedPreferences
7
8
import android.content.pm.PackageManager
import android.net.Uri
9
import android.os.Build
Daniel Wolf's avatar
Daniel Wolf committed
10
import android.os.Bundle
11
12
import android.provider.Settings
import androidx.annotation.RequiresApi
13
import androidx.appcompat.app.AlertDialog
14
import androidx.core.content.FileProvider
15
16
import androidx.preference.CheckBoxPreference
import androidx.preference.EditTextPreference
17
import androidx.preference.Preference
Daniel Wolf's avatar
Daniel Wolf committed
18
import androidx.preference.PreferenceFragmentCompat
19
import com.frostnerd.design.dialogs.LoadingDialog
20
import com.frostnerd.general.isInt
21
import com.frostnerd.smokescreen.*
22
import com.frostnerd.smokescreen.activity.MainActivity
23
import com.frostnerd.smokescreen.database.getDatabase
24
import com.frostnerd.smokescreen.dialog.AppChoosalDialog
25
import com.frostnerd.smokescreen.dialog.CrashReportingEnableDialog
26
import com.frostnerd.smokescreen.dialog.QueryGeneratorDialog
27
import com.frostnerd.smokescreen.util.preferences.Theme
28
import io.sentry.Sentry
Daniel Wolf's avatar
Daniel Wolf committed
29

Daniel Wolf's avatar
Daniel Wolf committed
30
31
/*
 * Copyright (C) 2019 Daniel Wolf (Ch4t4r)
Daniel Wolf's avatar
Daniel Wolf committed
32
 *
Daniel Wolf's avatar
Daniel Wolf committed
33
34
35
36
37
38
39
40
41
42
43
44
45
46
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.

 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.

 * You should have received a copy of the GNU General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 *
 * You can contact the developer at daniel.wolf@frostnerd.com.
Daniel Wolf's avatar
Daniel Wolf committed
47
48
 */
class SettingsFragment : PreferenceFragmentCompat() {
49
50
51
    private var werePreferencesAdded = false
    private var preferenceListener: SharedPreferences.OnSharedPreferenceChangeListener =
        SharedPreferences.OnSharedPreferenceChangeListener { _, key ->
52
            context?.getPreferences()?.notifyPreferenceChangedFromExternal(key)
53
        }
54

Daniel Wolf's avatar
Daniel Wolf committed
55
    override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
56
        log("Adding preferences from resources...")
Daniel Wolf's avatar
Daniel Wolf committed
57
        setPreferencesFromResource(R.xml.preferences, rootKey)
58
59
60
        log("Preferences added.")
        werePreferencesAdded = true
        createPreferenceListener()
Daniel Wolf's avatar
Daniel Wolf committed
61
62
    }

63
64
    override fun onPause() {
        super.onPause()
Daniel Wolf's avatar
Daniel Wolf committed
65
        log("Pausing fragment")
66
        removePreferenceListener()
67
68
69
70
    }

    override fun onResume() {
        super.onResume()
Daniel Wolf's avatar
Daniel Wolf committed
71
        log("Resuming fragment")
72
        if (werePreferencesAdded) createPreferenceListener()
73
74
75
76
77
78
79
80
    }

    override fun onDetach() {
        log("Fragment detached.")
        removePreferenceListener()
        super.onDetach()
    }

Daniel Wolf's avatar
Daniel Wolf committed
81
    override fun onAttach(context: Context) {
82
83
        super.onAttach(context)
        log("Fragment attached.")
84
        if (werePreferencesAdded) createPreferenceListener()
85
86
87
    }

    private fun createPreferenceListener() {
88
89
90
        requireContext().getPreferences().sharedPreferences.registerOnSharedPreferenceChangeListener(preferenceListener)
    }

91
92
93
94
95
96
    private fun removePreferenceListener() {
        requireContext().getPreferences().sharedPreferences.unregisterOnSharedPreferenceChangeListener(
            preferenceListener
        )
    }

97
98
99
100
    fun findPreference(key:String): Preference {
        return super.findPreference<Preference>(key)!!
    }

Daniel Wolf's avatar
Daniel Wolf committed
101
102
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
103
        log("Fragment created")
Daniel Wolf's avatar
Daniel Wolf committed
104
105
        findPreference("theme").setOnPreferenceChangeListener { _, newValue ->
            val id = (newValue as? String)?.toInt() ?: newValue as Int
106
            val newTheme = Theme.findById(id)
Daniel Wolf's avatar
Daniel Wolf committed
107

108
            log("Updated theme to $newValue")
109
            if (newTheme != null) {
110
                removePreferenceListener()
111
                requireContext().getPreferences().theme = newTheme
Daniel Wolf's avatar
Daniel Wolf committed
112
                requireActivity().restart()
Daniel Wolf's avatar
Daniel Wolf committed
113
114
115
116
117
                true
            } else {
                false
            }
        }
118
        findPreference("app_exclusion_list").setOnPreferenceClickListener {
119
            showExcludedAppsDialog()
120
121
            true
        }
122
        findPreference("send_logs").setOnPreferenceClickListener {
123
            requireContext().showLogExportDialog()
124
125
            true
        }
126
127
128
129
        findPreference("delete_logs").setOnPreferenceClickListener {
            showLogDeletionDialog()
            true
        }
130
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
131
132
133
134
            val hideIconPreference = findPreference("hide_notification_icon")
            hideIconPreference.isEnabled = false
            hideIconPreference.isVisible = false
        }
135
        if (!requireContext().getPreferences().isUsingKeweon()) {
136
137
138
139
            val terminateKeweonPreference = findPreference("null_terminate_keweon")
            terminateKeweonPreference.isVisible = false
            terminateKeweonPreference.isEnabled = false
        }
140
        processGeneralCategory()
141
        processCacheCategory()
142
        processLoggingCategory()
143
        processIPCategory()
144
        processNetworkCategory()
145
        processQueryCategory()
146
147
148
149
150
151
152
153
        processPinCategory()
    }

    private fun processPinCategory() {
        val pinValue = findPreference("pin") as EditTextPreference

        pinValue.setOnPreferenceChangeListener { _, newValue ->
            println(newValue)
154
            if (newValue.toString().isNotEmpty() && newValue.toString().isInt()) {
155
156
157
158
159
160
                pinValue.summary = getString(R.string.summary_preference_change_pin, newValue.toString())
                true
            } else {
                false
            }
        }
161
162
163
164
        pinValue.summary =
            getString(R.string.summary_preference_change_pin, requireContext().getPreferences().pin.toString())
        if (!requireContext().canUseFingerprintAuthentication()) findPreference("pin_allow_fingerprint").isVisible =
            false
165
166
167
168
169
    }

    private fun processQueryCategory() {
        val queryLogging = findPreference("log_dns_queries")
        val exportQueries = findPreference("export_dns_queries")
170
        val generateQueries = findPreference("generate_queries")
171
        val clearQueries = findPreference("clear_dns_queries")
172

173
        generateQueries.isVisible = BuildConfig.DEBUG || BuildConfig.VERSION_NAME.contains("debug", true)
174

175
176
177
178
179
        queryLogging.setOnPreferenceChangeListener { _, newValue ->
            requireContext().getPreferences().queryLoggingEnabled = newValue as Boolean
            (requireActivity() as MainActivity).reloadMenuItems()
            true
        }
180
181
        exportQueries.summary =
            getString(R.string.summary_export_queries, requireContext().getDatabase().dnsQueryDao().getCount())
182
        exportQueries.setOnPreferenceClickListener {
183
184
185
186
187
188
189
            val loadingDialog: LoadingDialog?
            if (requireContext().getDatabase().dnsQueryDao().getCount() >= 100) {
                loadingDialog = LoadingDialog(
                    requireContext(),
                    R.string.dialog_query_export_title,
                    R.string.dialog_query_export_message
                )
190
191
            } else loadingDialog = null
            loadingDialog?.show()
192
193
194
195
            requireContext().getDatabase().dnsQueryRepository().exportQueriesAsCsvAsync(requireContext(), { file ->
                if (!isDetached && !isRemoving) {
                    val uri =
                        FileProvider.getUriForFile(requireContext(), "com.frostnerd.smokescreen.LogZipProvider", file)
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
                    val exportIntent = Intent(Intent.ACTION_SEND)
                    exportIntent.putExtra(Intent.EXTRA_TEXT, "")
                    exportIntent.type = "text/csv"
                    exportIntent.putExtra(Intent.EXTRA_SUBJECT, getString(R.string.app_name) + " -- Logged Queries")
                    for (receivingApps in requireContext().packageManager.queryIntentActivities(
                        exportIntent,
                        PackageManager.MATCH_DEFAULT_ONLY
                    )) {
                        requireContext().grantUriPermission(
                            receivingApps.activityInfo.packageName,
                            uri,
                            Intent.FLAG_GRANT_READ_URI_PERMISSION
                        )
                    }
                    exportIntent.putExtra(Intent.EXTRA_STREAM, uri)
                    exportIntent.flags = Intent.FLAG_GRANT_READ_URI_PERMISSION
                    startActivity(Intent.createChooser(exportIntent, getString(R.string.title_export_queries)))
213
                }
214
                loadingDialog?.dismiss()
215
216
217
218
219
            }, { count, totalCount ->
                activity?.runOnUiThread {
                    loadingDialog?.appendToMessage("\n\n$count/$totalCount")
                }
            })
220
221
            true
        }
222
        generateQueries.setOnPreferenceClickListener {
223
            QueryGeneratorDialog(requireContext())
224
225
            true
        }
226
227
228
229
230
231
232
233
234
235
        clearQueries.setOnPreferenceClickListener {
            val dialog = AlertDialog.Builder(requireContext(), requireContext().getPreferences().theme.dialogStyle)
            dialog.setMessage(R.string.dialog_clearqueries_message)
            dialog.setTitle(R.string.dialog_clearqueries_title)
            dialog.setPositiveButton(R.string.all_yes) { d, _ ->
                requireContext().getDatabase().dnsQueryDao().deleteAll()
                exportQueries.summary =
                    getString(R.string.summary_export_queries, 0)
                d.dismiss()
            }
236
            dialog.setNegativeButton(android.R.string.cancel) { d, _ ->
237
238
239
240
241
                d.dismiss()
            }
            dialog.show()
            true
        }
242
243
    }

244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
    @SuppressLint("NewApi")
    private fun processGeneralCategory() {
        val startOnBoot = findPreference("start_on_boot") as CheckBoxPreference
        startOnBoot.setOnPreferenceChangeListener { preference, newValue ->
            if (newValue == false) true
            else {
                if (requireContext().isAppBatteryOptimized()) {
                    showBatteryOptimizationDialog {
                        startOnBoot.isChecked = true
                    }
                    false
                } else true
            }
        }
    }

260
    private fun processIPCategory() {
261
262
263
264
        val ipv6 = findPreference("ipv6_enabled") as CheckBoxPreference
        val ipv4 = findPreference("ipv4_enabled") as CheckBoxPreference
        val forceIpv6 = findPreference("force_ipv6") as CheckBoxPreference
        val forceIpv4 = findPreference("force_ipv4") as CheckBoxPreference
265
266
        val allowIpv6Traffic = findPreference("allow_ipv6_traffic") as CheckBoxPreference
        val allowIpv4Traffic = findPreference("allow_ipv4_traffic") as CheckBoxPreference
267

268
        val updateState = { ipv6Enabled: Boolean, ipv4Enabled: Boolean ->
269
270
271
272
            ipv4.isEnabled = ipv6Enabled
            ipv6.isEnabled = ipv4Enabled
            forceIpv6.isEnabled = ipv6Enabled && ipv6.isEnabled
            forceIpv4.isEnabled = ipv4Enabled && ipv4.isEnabled
273
274
            allowIpv6Traffic.isEnabled = !ipv6Enabled
            allowIpv4Traffic.isEnabled = !ipv4Enabled
275
276
            if (!ipv6.isChecked && ipv6Enabled) allowIpv6Traffic.isChecked = true
            if (!ipv4.isChecked && ipv4Enabled) allowIpv4Traffic.isChecked = true
277
278
279
280
281
282
283
284
285
286
        }
        updateState(ipv6.isChecked, ipv4.isChecked)
        ipv6.setOnPreferenceChangeListener { _, newValue ->
            updateState(newValue as Boolean, ipv4.isChecked)
            true
        }
        ipv4.setOnPreferenceChangeListener { _, newValue ->
            updateState(ipv6.isChecked, newValue as Boolean)
            true
        }
287
288
    }

289
290
291
292
    private fun processNetworkCategory() {

    }

293
294
    private fun processCacheCategory() {
        val cacheEnabled = findPreference("dnscache_enabled") as CheckBoxPreference
295
        val keepAcrossLaunches = findPreference("dnscache_keepacrosslaunches") as CheckBoxPreference
296
297
        val cacheMaxSize = findPreference("dnscache_maxsize") as EditTextPreference
        val useDefaultTime = findPreference("dnscache_use_default_time") as CheckBoxPreference
298
        val minCacheTime = findPreference("dnscache_minimum_time") as EditTextPreference
299
        val cacheTime = findPreference("dnscache_custom_time") as EditTextPreference
300
        val nxDomainCacheTime = findPreference("dnscache_nxdomain_cachetime") as EditTextPreference
301

302
        val updateState = { isCacheEnabled: Boolean, isUsingDefaultTime: Boolean ->
303
304
305
            cacheMaxSize.isEnabled = isCacheEnabled
            useDefaultTime.isEnabled = isCacheEnabled
            cacheTime.isEnabled = isCacheEnabled && !isUsingDefaultTime
306
            nxDomainCacheTime.isEnabled = cacheTime.isEnabled
307
            keepAcrossLaunches.isEnabled = isCacheEnabled
308
            minCacheTime.isEnabled = isUsingDefaultTime && isCacheEnabled
309
310
        }
        updateState(cacheEnabled.isChecked, useDefaultTime.isChecked)
311
312
313
314
        cacheTime.summary = getString(
            R.string.summary_dnscache_customcachetime,
            requireContext().getPreferences().customDnsCacheTime
        )
315
316
317
318
        nxDomainCacheTime.summary = getString(
            R.string.summary_dnscache_nxdomaincachetime,
            requireContext().getPreferences().nxDomainCacheTime
        )
319
320
321
322
        minCacheTime.summary = getString(
            R.string.summary_dnscache_minimum_cache_time,
            requireContext().getPreferences().minimumCacheTime
        )
323

324
325
326
        cacheMaxSize.setOnPreferenceChangeListener { _, newValue ->
            newValue.toString().isInt()
        }
327
328
329
330
331
332
333
334
335
        cacheEnabled.setOnPreferenceChangeListener { _, newValue ->
            updateState(newValue as Boolean, useDefaultTime.isChecked)
            true
        }
        useDefaultTime.setOnPreferenceChangeListener { _, newValue ->
            updateState(cacheEnabled.isChecked, newValue as Boolean)
            true
        }
        cacheTime.setOnPreferenceChangeListener { _, newValue ->
336
            if (newValue.toString().isInt()) {
337
338
339
340
341
                cacheTime.summary = getString(R.string.summary_dnscache_customcachetime, newValue.toString().toInt())
                true
            } else {
                false
            }
342
        }
343
344
        nxDomainCacheTime.setOnPreferenceChangeListener { _, newValue ->
            if (newValue.toString().isInt()) {
345
346
                nxDomainCacheTime.summary =
                    getString(R.string.summary_dnscache_nxdomaincachetime, newValue.toString().toInt())
347
348
349
350
351
                true
            } else {
                false
            }
        }
352
353
        minCacheTime.setOnPreferenceChangeListener { _, newValue ->
            if (newValue.toString().isInt()) {
354
355
                minCacheTime.summary =
                    getString(R.string.summary_dnscache_minimum_cache_time, newValue.toString().toInt())
356
357
358
359
360
                true
            } else {
                false
            }
        }
361
362
    }

363
364
365
    private fun processLoggingCategory() {
        val loggingEnabled = findPreference("logging_enabled") as CheckBoxPreference
        val sendLogs = findPreference("send_logs")
366
        val deleteLogs = findPreference("delete_logs")
367
        val crashReporting = findPreference("enable_sentry") as CheckBoxPreference
368
369
        loggingEnabled.isChecked = requireContext().getPreferences().loggingEnabled
        sendLogs.isEnabled = loggingEnabled.isChecked
370
        deleteLogs.isEnabled = loggingEnabled.isChecked
371
372
373
        loggingEnabled.setOnPreferenceChangeListener { _, newValue ->
            val enabled = newValue as Boolean
            if (!enabled) log("Logging disabled from settings.") // Log before disabling
374
            Logger.setEnabled(enabled)
375
            if (!enabled) requireContext().closeLogger()
376
            if (enabled) log("Logging enabled from settings.") // Log after enabling
377
378
            sendLogs.isEnabled = enabled
            deleteLogs.isEnabled = enabled
379
380
            true
        }
381
382
383
384
385
386
387
388
389
390
        crashReporting.setOnPreferenceClickListener {
            if(requireContext().getPreferences().crashReportingConsent) {
                true
            } else {
                CrashReportingEnableDialog(requireContext(), onConsentGiven = {
                    crashReporting.isChecked = true
                }).show()
                false
            }
        }
391
392
393
394
395
396
397
398
        crashReporting.setOnPreferenceChangeListener { _, newValue ->
            if (!(newValue as Boolean)) {
                Sentry.close()
            } else {
                (requireContext().applicationContext as SmokeScreen).initSentry(true)
            }
            true
        }
399
400
    }

401
402
403
404
405
406
407
408
409
410
411
412
413
    private fun showLogDeletionDialog() {
        AlertDialog.Builder(requireContext(), requireContext().getPreferences().theme.dialogStyle)
            .setTitle(R.string.title_delete_all_logs)
            .setMessage(R.string.dialog_deletelogs_text)
            .setPositiveButton(R.string.all_yes) { dialog, _ ->
                requireContext().deleteAllLogs()
                dialog.dismiss()
            }
            .setNegativeButton(R.string.all_no) { dialog, _ ->
                dialog.dismiss()
            }.setCancelable(true).show()
    }

414
415
    @RequiresApi(Build.VERSION_CODES.M)
    private fun showBatteryOptimizationDialog(enablePreference: () -> Unit) {
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
        val intent = Intent(Settings.ACTION_IGNORE_BATTERY_OPTIMIZATION_SETTINGS)
        if(intent.resolveActivity(context!!.packageManager) == null) {
            enablePreference()
        } else {
            AlertDialog.Builder(requireContext(), requireContext().getPreferences().theme.dialogStyle)
                .setTitle(R.string.dialog_batteryoptimization_title)
                .setMessage(R.string.dialog_batteryoptimization_message)
                .setNegativeButton(android.R.string.cancel) { dialog, _ ->
                    dialog.dismiss()
                }
                .setPositiveButton(R.string.dialog_batteryoptimization_whitelist) { dialog, _ ->
                    startActivity(intent)
                    dialog.dismiss()
                }
                .setNeutralButton(R.string.dialog_batteryoptimization_ignore) { dialog, _ ->
                    enablePreference()
                    dialog.dismiss()
                }.show()
        }
435
436
    }

437
438
439
440
441
442
443
444
    private fun showExcludedAppsDialog() {
        val dialog = AppChoosalDialog(
            requireActivity(),
            requireContext().getPreferences().userBypassPackages,
            defaultChosenUnselectablePackages = requireContext().getPreferences().defaultBypassPackages,
            infoText = getString(
                R.string.dialog_excludedapps_infotext,
                requireContext().getPreferences().defaultBypassPackages.size
445
446
447
448
            ),
            blackList = requireContext().getPreferences().isBypassBlacklist
        ) { selected, isBlacklist ->
            requireContext().getPreferences().isBypassBlacklist = isBlacklist
449
450
451
452
453
454
455
            if (selected.size != requireContext().getPreferences().userBypassPackages.size) {
                log("Updated the list of user bypass packages to $selected")
                requireContext().getPreferences().userBypassPackages = selected
            }
        }.createDialog()
        dialog.setTitle(R.string.title_excluded_apps)
        dialog.show()
456
    }
457
}
458

459
460
461
462
463
464
465
466
467
fun Context.showLogExportDialog(onDismiss: (() -> Unit)? = null) {
    log("Trying to send logs..")
    val zipFile = this.zipAllLogFiles()
    if (zipFile != null) {
        val zipUri =
            FileProvider.getUriForFile(this, "com.frostnerd.smokescreen.LogZipProvider", zipFile)
        showLogExportDialog(zipUri, onDismiss)
    } else log("Cannot send, zip file is null.")
}
Daniel Wolf's avatar
Daniel Wolf committed
468

469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
private fun Context.showLogExportDialog(zipUri: Uri, onDismiss: (() -> Unit)? = null) {
    val dialog = AlertDialog.Builder(this, this.getPreferences().theme.dialogStyle)
        .setTitle(R.string.title_send_logs)
        .setMessage(R.string.dialog_logexport_text)
        .setPositiveButton(R.string.dialog_logexport_email) { dialog, _ ->
            log("User choose to send logs over E-Mail")
            val emailIntent = Intent(Intent.ACTION_SENDTO, Uri.fromParts("mailto", "support@frostnerd.com", null))
            emailIntent.putExtra(Intent.EXTRA_SUBJECT, getString(R.string.app_name) + " -- logs")
            emailIntent.putExtra(Intent.EXTRA_TEXT, "")
            emailIntent.putExtra(Intent.EXTRA_EMAIL, "support@frostnerd.com")
            for (receivingApps in this.packageManager.queryIntentActivities(
                emailIntent,
                PackageManager.MATCH_DEFAULT_ONLY
            )) {
                grantUriPermission(
                    receivingApps.activityInfo.packageName,
                    zipUri,
                    Intent.FLAG_GRANT_READ_URI_PERMISSION
                )
            }
            emailIntent.putExtra(Intent.EXTRA_STREAM, zipUri)
            emailIntent.flags = Intent.FLAG_GRANT_READ_URI_PERMISSION
            startActivity(Intent.createChooser(emailIntent, getString(R.string.title_send_logs)))
            log("Now choosing chooser for E-Mail intent")
            dialog.dismiss()
        }
        .setNeutralButton(R.string.dialog_logexport_general) { dialog, _ ->
            log("User choose to send logs via general export")
            val generalIntent = Intent(Intent.ACTION_SEND)
            generalIntent.putExtra(Intent.EXTRA_TEXT, "")
            generalIntent.type = "application/zip"
            generalIntent.putExtra(Intent.EXTRA_SUBJECT, getString(R.string.app_name) + " -- logs")
            for (receivingApps in packageManager.queryIntentActivities(
                generalIntent,
                PackageManager.MATCH_DEFAULT_ONLY
            )) {
                grantUriPermission(
                    receivingApps.activityInfo.packageName,
                    zipUri,
                    Intent.FLAG_GRANT_READ_URI_PERMISSION
                )
            }
            generalIntent.putExtra(Intent.EXTRA_STREAM, zipUri)
            generalIntent.flags = Intent.FLAG_GRANT_READ_URI_PERMISSION
            log("Now choosing chooser for general export")
            startActivity(Intent.createChooser(generalIntent, getString(R.string.title_send_logs)))
515
516
517
            dialog.dismiss()
        }
        .setOnDismissListener {
518
            onDismiss?.invoke()
519
        }
520
        .setNegativeButton(android.R.string.cancel) { dialog, _ ->
521
            dialog.dismiss()
522
523
        }
        .create()
524
525
    dialog.setCanceledOnTouchOutside(false)
    dialog.show()
Daniel Wolf's avatar
Daniel Wolf committed
526
}