SettingsFragment.kt 31.6 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
import android.provider.Settings
12
import android.view.View
13
import android.widget.Toast
14
import androidx.annotation.RequiresApi
15
import androidx.appcompat.app.AlertDialog
16
import androidx.core.content.FileProvider
17
import androidx.lifecycle.LifecycleOwner
18
import androidx.preference.*
19
import com.frostnerd.encrypteddnstunnelproxy.AbstractHttpsDNSHandle
20
import com.frostnerd.general.isInt
21
import com.frostnerd.general.service.isServiceRunning
22
import com.frostnerd.lifecyclemanagement.LifecycleCoroutineScope
Daniel Wolf's avatar
Daniel Wolf committed
23
import com.frostnerd.lifecyclemanagement.launchWithLifecycle
24
import com.frostnerd.smokescreen.*
25
import com.frostnerd.smokescreen.R
26
import com.frostnerd.smokescreen.activity.MainActivity
27
import com.frostnerd.smokescreen.activity.SettingsActivity
28
import com.frostnerd.smokescreen.database.getDatabase
29
import com.frostnerd.smokescreen.dialog.AppChoosalDialog
30
import com.frostnerd.smokescreen.dialog.CrashReportingEnableDialog
Daniel Wolf's avatar
Daniel Wolf committed
31
import com.frostnerd.smokescreen.dialog.LoadingDialog
32
import com.frostnerd.smokescreen.dialog.QueryGeneratorDialog
33
import com.frostnerd.smokescreen.service.DnsVpnService
34
import com.frostnerd.smokescreen.util.preferences.Crashreporting
35
import com.frostnerd.smokescreen.util.preferences.Theme
36
import com.google.android.material.snackbar.Snackbar
37
import com.frostnerd.smokescreen.util.processSuCommand
38
39
40
41
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
Daniel Wolf's avatar
Daniel Wolf committed
42

Daniel Wolf's avatar
Daniel Wolf committed
43
44
/*
 * Copyright (C) 2019 Daniel Wolf (Ch4t4r)
Daniel Wolf's avatar
Daniel Wolf committed
45
 *
Daniel Wolf's avatar
Daniel Wolf committed
46
47
48
49
50
51
52
53
54
55
56
57
58
59
 * 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
60
61
 */
class SettingsFragment : PreferenceFragmentCompat() {
62
63
64
    private var werePreferencesAdded = false
    private var preferenceListener: SharedPreferences.OnSharedPreferenceChangeListener =
        SharedPreferences.OnSharedPreferenceChangeListener { _, key ->
65
            context?.getPreferences()?.notifyPreferenceChangedFromExternal(key)
66
        }
67
    private val category:SettingsActivity.Category by lazy(LazyThreadSafetyMode.NONE) {
68
        (arguments?.getSerializable("category") as SettingsActivity.Category?) ?: SettingsActivity.Category.GENERAL
69
    }
70

Daniel Wolf's avatar
Daniel Wolf committed
71
    override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
72
        log("Adding preferences from resources...")
73
74
75
76
77
78
79
80
81
        val resource = when(category) {
            SettingsActivity.Category.GENERAL -> R.xml.preferences_general
            SettingsActivity.Category.NOTIFICATION -> R.xml.preferences_notification
            SettingsActivity.Category.PIN -> R.xml.preferences_pin
            SettingsActivity.Category.CACHE -> R.xml.preferences_cache
            SettingsActivity.Category.LOGGING -> R.xml.preferences_logging
            SettingsActivity.Category.IP -> R.xml.preferences_ip
            SettingsActivity.Category.NETWORK -> R.xml.preferences_network
            SettingsActivity.Category.QUERIES -> R.xml.preferences_queries
Daniel Wolf's avatar
Daniel Wolf committed
82
            SettingsActivity.Category.SERVER_MODE -> R.xml.preferences_nonvpnmode
83
84
        }
        setPreferencesFromResource(resource, rootKey)
85
86
87
        log("Preferences added.")
        werePreferencesAdded = true
        createPreferenceListener()
Daniel Wolf's avatar
Daniel Wolf committed
88
89
    }

90
91
    override fun onPause() {
        super.onPause()
Daniel Wolf's avatar
Daniel Wolf committed
92
        log("Pausing fragment")
93
        removePreferenceListener()
94
95
96
97
    }

    override fun onResume() {
        super.onResume()
Daniel Wolf's avatar
Daniel Wolf committed
98
        log("Resuming fragment")
99
        if (werePreferencesAdded) createPreferenceListener()
100
101
102
103
104
105
106
107
    }

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

Daniel Wolf's avatar
Daniel Wolf committed
108
    override fun onAttach(context: Context) {
109
110
        super.onAttach(context)
        log("Fragment attached.")
111
        if (werePreferencesAdded) createPreferenceListener()
112
113
114
    }

    private fun createPreferenceListener() {
115
116
117
        requireContext().getPreferences().sharedPreferences.registerOnSharedPreferenceChangeListener(preferenceListener)
    }

118
119
120
121
122
123
    private fun removePreferenceListener() {
        requireContext().getPreferences().sharedPreferences.unregisterOnSharedPreferenceChangeListener(
            preferenceListener
        )
    }

Daniel Wolf's avatar
Daniel Wolf committed
124
    private fun findPreference(key:String): Preference {
Daniel Wolf's avatar
Daniel Wolf committed
125
        return super.findPreference(key)!!
126
127
    }

Daniel Wolf's avatar
Daniel Wolf committed
128
129
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
130
        log("Fragment created")
131
132
133
134
135
136
137
138
139
        when(category) {
            SettingsActivity.Category.GENERAL -> processGeneralCategory()
            SettingsActivity.Category.NOTIFICATION -> processNotificationCategory()
            SettingsActivity.Category.PIN -> processPinCategory()
            SettingsActivity.Category.CACHE -> processCacheCategory()
            SettingsActivity.Category.LOGGING -> processLoggingCategory()
            SettingsActivity.Category.IP -> processIPCategory()
            SettingsActivity.Category.NETWORK -> processNetworkCategory()
            SettingsActivity.Category.QUERIES -> processQueryCategory()
Daniel Wolf's avatar
Daniel Wolf committed
140
            SettingsActivity.Category.SERVER_MODE -> processNonVpnCategory()
141
        }
142
143
144
145
146
147
148
149
    }

    private fun processNotificationCategory() {
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
            val hideIconPreference = findPreference("hide_notification_icon")
            hideIconPreference.isEnabled = false
            hideIconPreference.isVisible = false
        }
150
151
152
153
154
155
    }

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

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

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

175
        generateQueries.isVisible = BuildConfig.DEBUG || BuildConfig.VERSION_NAME.contains("debug", true)
176

177
178
        queryLogging.setOnPreferenceChangeListener { _, newValue ->
            requireContext().getPreferences().queryLoggingEnabled = newValue as Boolean
179
            context?.sendLocalBroadcast(Intent(MainActivity.BROADCAST_RELOAD_MENU))
180
181
            true
        }
182
183
        exportQueries.summary =
            getString(R.string.summary_export_queries, requireContext().getDatabase().dnsQueryDao().getCount())
184
        exportQueries.setOnPreferenceClickListener {
Daniel Wolf's avatar
Daniel Wolf committed
185
186
            val loadingDialog: LoadingDialog? = if (requireContext().getDatabase().dnsQueryDao().getCount() >= 100) {
                LoadingDialog(
187
188
189
190
                    requireContext(),
                    R.string.dialog_query_export_title,
                    R.string.dialog_query_export_message
                )
Daniel Wolf's avatar
Daniel Wolf committed
191
            } else null
192
            loadingDialog?.show()
193
194
195
196
            requireContext().getDatabase().dnsQueryRepository().exportQueriesAsCsvAsync(requireContext(), { file ->
                if (!isDetached && !isRemoving) {
                    val uri =
                        FileProvider.getUriForFile(requireContext(), "com.frostnerd.smokescreen.LogZipProvider", file)
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
                    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)))
214
                }
215
                loadingDialog?.dismiss()
216
217
218
219
220
            }, { count, totalCount ->
                activity?.runOnUiThread {
                    loadingDialog?.appendToMessage("\n\n$count/$totalCount")
                }
            })
221
222
            true
        }
223
        generateQueries.setOnPreferenceClickListener {
224
            QueryGeneratorDialog(requireContext())
225
226
            true
        }
227
228
229
230
231
232
        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()
233
                getPreferences().exportedQueryCount = 0
234
235
236
237
                exportQueries.summary =
                    getString(R.string.summary_export_queries, 0)
                d.dismiss()
            }
238
            dialog.setNegativeButton(android.R.string.cancel) { d, _ ->
239
240
241
242
243
                d.dismiss()
            }
            dialog.show()
            true
        }
244
245
    }

Daniel Wolf's avatar
Daniel Wolf committed
246
    private fun processNonVpnCategory() {
247
        val enabled = findPreference("run_without_vpn") as CheckBoxPreference
Daniel Wolf's avatar
Daniel Wolf committed
248
249
        val port = findPreference("non_vpn_server_port") as EditTextPreference
        val connectInfo = findPreference("nonvpn_connect_info")
250
        val iptablesCategory = findPreference("nonvpn_category_iptables")
251
        val checkIpTables = findPreference("check_iptables")
Daniel Wolf's avatar
Daniel Wolf committed
252
        val helpNetguard = findPreference("nonvpn_help_netguard")
Daniel Wolf's avatar
Daniel Wolf committed
253
        val helpGeneric = findPreference("nonvpn_help_generic")
Daniel Wolf's avatar
Daniel Wolf committed
254
        port.setOnPreferenceChangeListener { _, newValue ->
255
            if (newValue.toString().toIntOrNull()?.let { it in 1025..65535 } == true) {
Daniel Wolf's avatar
Daniel Wolf committed
256
257
258
259
260
261
262
                port.summary = getString(R.string.summary_local_server_port, newValue.toString())
                connectInfo.summary = getString(R.string.summary_category_nonvpnmode_forwardinfo, newValue.toString())
                true
            } else {
                false
            }
        }
263
264
265
266
        enabled.setOnPreferenceChangeListener { _, newValue ->
            val value = newValue as Boolean
            val serviceRunning = requireContext().isServiceRunning(DnsVpnService::class.java)
            if(value && serviceRunning) {
267
                DnsVpnService.restartVpn(requireContext(), false)
268
            } else if(!value) {
269
                if(serviceRunning)
270
271
272
273
                    DnsVpnService.restartVpn(requireContext(), false)
            }
            true
        }
Daniel Wolf's avatar
Daniel Wolf committed
274
275
        port.summary = getString(R.string.summary_local_server_port, requireContext().getPreferences().dnsServerModePort.toString())
        connectInfo.summary = getString(R.string.summary_category_nonvpnmode_forwardinfo, requireContext().getPreferences().dnsServerModePort.toString())
276
277
        val rooted = context?.isDeviceRooted() ?: false
        if(!rooted) {
278
            iptablesCategory.isVisible = false
279
280
281
282
283
284
        } else {
            checkIpTables.setOnPreferenceClickListener {
                val context = context
                if(context != null) {
                    val dialog = LoadingDialog(context, R.string.title_check_iptables, R.string.dialog_doh_detect_type_message)
                    dialog.show()
Daniel Wolf's avatar
Daniel Wolf committed
285
                    launchWithLifecycle(false) {
286
287
                        val supported = processSuCommand("iptables -t nat -L OUTPUT", context.logger)
                        val ipv6Supported = processSuCommand("ip6tables -t nat -L PREROUTING", context.logger)
Daniel Wolf's avatar
Daniel Wolf committed
288
                        launchWithLifecycle(true) {
289
                            dialog.dismiss()
290
291
292
293
294
                            val text = if(supported) {
                                if(ipv6Supported) R.string.iptables_supported
                                else R.string.iptables_supported_ipv6_unsupported
                            } else R.string.iptables_not_supported
                            Toast.makeText(context, text, Toast.LENGTH_LONG).show()
295
296
297
298
299
                        }
                    }
                }
                true
            }
300
        }
301
302

        var installedThirdPartyApps = 0
Daniel Wolf's avatar
Daniel Wolf committed
303
304
305
306
307
308
309
310
311
312
313
314
315
        if(isPackageInstalled(requireContext(), "eu.faircode.netguard")) {
            installedThirdPartyApps++
            helpNetguard.setOnPreferenceClickListener {
                AlertDialog.Builder(requireContext(), getPreferences().theme.dialogStyle)
                    .setTitle("NetGuard")
                    .setMessage(getString(R.string.dialog_nonvpn_help_netguard, port.text.toInt()))
                    .setPositiveButton(R.string.all_close, null)
                    .show()
                true
            }
        } else {
            helpNetguard.isVisible = false
        }
316

Daniel Wolf's avatar
Daniel Wolf committed
317
318
        helpGeneric.setOnPreferenceClickListener {
            val portValue = port.text.toInt()
319
320
321
322
323
324
325
            showInfoTextDialog(
                requireContext(),
                getString(R.string.preference_category_nonvpnmode_help),
                getString(R.string.dialog_nonvpn_help_generic, portValue, portValue),
                positiveButton = getString(R.string.all_close) to null,
                neutralButton = null
            )
Daniel Wolf's avatar
Daniel Wolf committed
326
327
            true
        }
328
329
330
331
        findPreference("nonvpn_help_faq").setOnPreferenceClickListener {
            requireContext().tryOpenBrowser("https://nebulo.app/faq#non-vpn-mode")
            true
        }
Daniel Wolf's avatar
Daniel Wolf committed
332
333
    }

334
335
336
    @SuppressLint("NewApi")
    private fun processGeneralCategory() {
        val startOnBoot = findPreference("start_on_boot") as CheckBoxPreference
337
        val language = findPreference("language")
338
        val fallbackDns = findPreference("fallback_dns")
339
340
341
342
343
344
345
346
347
348
349
        startOnBoot.setOnPreferenceChangeListener { preference, newValue ->
            if (newValue == false) true
            else {
                if (requireContext().isAppBatteryOptimized()) {
                    showBatteryOptimizationDialog {
                        startOnBoot.isChecked = true
                    }
                    false
                } else true
            }
        }
350
351
352
353
354
355
356
        findPreference("theme").setOnPreferenceChangeListener { _, newValue ->
            val id = (newValue as? String)?.toInt() ?: newValue as Int
            val newTheme = Theme.findById(id)

            log("Updated theme to $newValue")
            if (newTheme != null) {
                requireContext().getPreferences().theme = newTheme
357
                askRestartApp()
358
359
360
361
362
                true
            } else {
                false
            }
        }
363
        language.setOnPreferenceChangeListener { _, _ ->
364
            askRestartApp()
365
366
            true
        }
367
368
369
370
        findPreference("app_exclusion_list").setOnPreferenceClickListener {
            showExcludedAppsDialog()
            true
        }
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
        fallbackDns.setOnPreferenceChangeListener { _, newValue ->
            val pos = newValue.toString().toInt()
            // Right now the fallback DNS is statically bound at these positions.
            // Later on user-added servers can be used. This will only be possible when the IP address
            // can be specified for user-added servers: https://git.frostnerd.com/PublicAndroidApps/smokescreen/-/issues/230
            getPreferences().fallbackDns = when(pos) {
                2 -> AbstractHttpsDNSHandle.waitUntilKnownServersArePopulated {
                    it[0] // The IDs are stable and won't change. 0 == Cloudflare
                }
                3 -> AbstractHttpsDNSHandle.waitUntilKnownServersArePopulated {
                    it[1] // 1 == Google stable
                }
                4 -> AbstractHttpsDNSHandle.waitUntilKnownServersArePopulated {
                    it[3] // 3 == Quad9
                }
                else -> null // 1 or > 4 == ISP
            }
388
            true
389
        }
390
391
    }

392
393
394
395
396
397
398
399
400
401
402
    private fun askRestartApp() {
        val activity = activity ?: return
        activity.findViewById<View>(android.R.id.content)?.apply {
            Snackbar.make(this, R.string.restart_app_for_changes, Snackbar.LENGTH_INDEFINITE)
                .setAction(R.string.restart_app) {
                    removePreferenceListener()
                    activity.restart(MainActivity::class.java, true)
                }.show()
        }
    }

403
    private fun processIPCategory() {
404
405
406
407
        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
408
409
        val allowIpv6Traffic = findPreference("allow_ipv6_traffic") as CheckBoxPreference
        val allowIpv4Traffic = findPreference("allow_ipv4_traffic") as CheckBoxPreference
410

411
        val updateState = { ipv6Enabled: Boolean, ipv4Enabled: Boolean ->
412
413
414
415
            ipv4.isEnabled = ipv6Enabled
            ipv6.isEnabled = ipv4Enabled
            forceIpv6.isEnabled = ipv6Enabled && ipv6.isEnabled
            forceIpv4.isEnabled = ipv4Enabled && ipv4.isEnabled
416
417
            allowIpv6Traffic.isEnabled = !ipv6Enabled
            allowIpv4Traffic.isEnabled = !ipv4Enabled
418
419
            if (!ipv6.isChecked && ipv6Enabled) allowIpv6Traffic.isChecked = true
            if (!ipv4.isChecked && ipv4Enabled) allowIpv4Traffic.isChecked = true
420
421
422
423
424
425
426
427
428
429
        }
        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
        }
430
431
    }

432
433
434
435
    private fun processNetworkCategory() {

    }

436
437
    private fun processCacheCategory() {
        val cacheEnabled = findPreference("dnscache_enabled") as CheckBoxPreference
438
        val keepAcrossLaunches = findPreference("dnscache_keepacrosslaunches") as CheckBoxPreference
439
440
        val cacheMaxSize = findPreference("dnscache_maxsize") as EditTextPreference
        val useDefaultTime = findPreference("dnscache_use_default_time") as CheckBoxPreference
441
        val minCacheTime = findPreference("dnscache_minimum_time") as EditTextPreference
442
        val cacheTime = findPreference("dnscache_custom_time") as EditTextPreference
443
        val nxDomainCacheTime = findPreference("dnscache_nxdomain_cachetime") as EditTextPreference
444
        val clearCache = findPreference("clear_dns_cache")
445

446
        val updateState = { isCacheEnabled: Boolean, isUsingDefaultTime: Boolean ->
447
            cacheMaxSize.isEnabled = isCacheEnabled
448
            clearCache.isEnabled = isCacheEnabled
449
450
            useDefaultTime.isEnabled = isCacheEnabled
            cacheTime.isEnabled = isCacheEnabled && !isUsingDefaultTime
451
            nxDomainCacheTime.isEnabled = cacheTime.isEnabled
452
            keepAcrossLaunches.isEnabled = isCacheEnabled
453
            minCacheTime.isEnabled = isUsingDefaultTime && isCacheEnabled
454
455
        }
        updateState(cacheEnabled.isChecked, useDefaultTime.isChecked)
456
457
458
459
        cacheTime.summary = getString(
            R.string.summary_dnscache_customcachetime,
            requireContext().getPreferences().customDnsCacheTime
        )
460
461
462
463
        nxDomainCacheTime.summary = getString(
            R.string.summary_dnscache_nxdomaincachetime,
            requireContext().getPreferences().nxDomainCacheTime
        )
464
465
466
467
        minCacheTime.summary = getString(
            R.string.summary_dnscache_minimum_cache_time,
            requireContext().getPreferences().minimumCacheTime
        )
468

469
470
471
        cacheMaxSize.setOnPreferenceChangeListener { _, newValue ->
            newValue.toString().isInt()
        }
472
473
474
475
476
477
478
479
480
        cacheEnabled.setOnPreferenceChangeListener { _, newValue ->
            updateState(newValue as Boolean, useDefaultTime.isChecked)
            true
        }
        useDefaultTime.setOnPreferenceChangeListener { _, newValue ->
            updateState(cacheEnabled.isChecked, newValue as Boolean)
            true
        }
        cacheTime.setOnPreferenceChangeListener { _, newValue ->
481
            if (newValue.toString().isInt()) {
482
483
484
485
486
                cacheTime.summary = getString(R.string.summary_dnscache_customcachetime, newValue.toString().toInt())
                true
            } else {
                false
            }
487
        }
488
489
        nxDomainCacheTime.setOnPreferenceChangeListener { _, newValue ->
            if (newValue.toString().isInt()) {
490
491
                nxDomainCacheTime.summary =
                    getString(R.string.summary_dnscache_nxdomaincachetime, newValue.toString().toInt())
492
493
494
495
496
                true
            } else {
                false
            }
        }
497
498
        minCacheTime.setOnPreferenceChangeListener { _, newValue ->
            if (newValue.toString().isInt()) {
499
500
                minCacheTime.summary =
                    getString(R.string.summary_dnscache_minimum_cache_time, newValue.toString().toInt())
501
502
503
504
505
                true
            } else {
                false
            }
        }
506
        clearCache.setOnPreferenceClickListener {
507
            showInfoTextDialog(requireContext(),
508
509
510
                getString(R.string.title_clear_dnscache),
                getString(R.string.dialog_cleardnscache_message),
                getString(R.string.all_yes) to { dialog, _ ->
511
512
513
                    GlobalScope.launch(Dispatchers.IO) {
                        context?.also {
                            getDatabase().cachedResponseDao().deleteAll()
514
                            DnsVpnService.invalidateDNSCache(requireContext())
515
                        }
516
517
                    }
                    dialog.dismiss()
518
519
520
521
                },
                neutralButton = null,
                negativeButton = getString(android.R.string.no) to { dialog, _ ->
                    dialog.dismiss()
522
523
524
                }, withDialog = {
                    show()
                })
525
526
            true
        }
527
528
    }

529
530
531
    private fun processLoggingCategory() {
        val loggingEnabled = findPreference("logging_enabled") as CheckBoxPreference
        val sendLogs = findPreference("send_logs")
532
        val deleteLogs = findPreference("delete_logs")
533
        val crashReportingType = findPreference("crashreporting_type") as ListPreference
534
535
        loggingEnabled.isChecked = requireContext().getPreferences().loggingEnabled
        sendLogs.isEnabled = loggingEnabled.isChecked
536
        deleteLogs.isEnabled = loggingEnabled.isChecked
537
538
539
        loggingEnabled.setOnPreferenceChangeListener { _, newValue ->
            val enabled = newValue as Boolean
            if (!enabled) log("Logging disabled from settings.") // Log before disabling
540
            Logger.setEnabled(enabled)
541
            if (!enabled) requireContext().closeLogger()
542
            if (enabled) log("Logging enabled from settings.") // Log after enabling
543
544
            sendLogs.isEnabled = enabled
            deleteLogs.isEnabled = enabled
545
546
            true
        }
547
548
549
        crashReportingType.setOnPreferenceChangeListener { preference, newValue ->
            newValue as String
            if(newValue == Crashreporting.FULL.value && !getPreferences().crashReportingConsent) {
550
                CrashReportingEnableDialog(requireContext(), onConsentGiven = {
551
                    crashReportingType.value = Crashreporting.FULL.value
552
553
                }).show()
                false
554
            } else {
555
                if(newValue == Crashreporting.MINIMAL.value) {
556
                    (requireContext().applicationContext as SmokeScreen).closeSentry()
557
558
                    (requireContext().applicationContext as SmokeScreen).initSentry(Status.DATASAVING)
                } else if(newValue == Crashreporting.OFF.value) {
559
                    (requireContext().applicationContext as SmokeScreen).closeSentry()
560
561
                }
                true
562
563
            }
        }
564
565
566
567
568
569
570
571
        findPreference("send_logs").setOnPreferenceClickListener {
            requireContext().showLogExportDialog()
            true
        }
        findPreference("delete_logs").setOnPreferenceClickListener {
            showLogDeletionDialog()
            true
        }
572
573
    }

574
575
576
577
578
579
580
581
582
583
584
585
586
    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()
    }

587
588
    @RequiresApi(Build.VERSION_CODES.M)
    private fun showBatteryOptimizationDialog(enablePreference: () -> Unit) {
589
        val intent = Intent(Settings.ACTION_IGNORE_BATTERY_OPTIMIZATION_SETTINGS)
590
        if(intent.resolveActivity(requireContext().packageManager) == null) {
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
            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()
        }
608
609
    }

610
611
612
613
614
    private fun showExcludedAppsDialog() {
        val dialog = AppChoosalDialog(
            requireActivity(),
            requireContext().getPreferences().userBypassPackages,
            defaultChosenUnselectablePackages = requireContext().getPreferences().defaultBypassPackages,
615
616
617
618
619
620
621
            infoText =  requireContext().getPreferences().defaultBypassPackages.size.let {
                resources.getQuantityString(
                    R.plurals.dialog_excludedapps_infotext,
                    it,
                    it
                )
            },
622
623
624
            blackList = requireContext().getPreferences().isBypassBlacklist
        ) { selected, isBlacklist ->
            requireContext().getPreferences().isBypassBlacklist = isBlacklist
625
626
            log("Updated the list of user bypass packages to $selected")
            requireContext().getPreferences().userBypassPackages = selected
627
628
629
        }.createDialog()
        dialog.setTitle(R.string.title_excluded_apps)
        dialog.show()
630
    }
631
}
632

633
634
fun Context.showLogExportDialog(onDismiss: (() -> Unit)? = null) {
    log("Trying to send logs..")
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
    val scope = if (this is LifecycleOwner) LifecycleCoroutineScope(this, ui = false)
    else GlobalScope

    val loadingDialog = LoadingDialog(this, R.string.dialog_logexport_loading_title).also {
        it.show()
    }
    scope.launch {
        val zipFile = this@showLogExportDialog.zipAllLogFiles()
        if (zipFile != null) {
            val zipUri =
                FileProvider.getUriForFile(
                    this@showLogExportDialog,
                    "com.frostnerd.smokescreen.LogZipProvider",
                    zipFile
                )
            withContext(Dispatchers.Main) {
                loadingDialog.dismiss()
                showLogExportDialog(zipUri, onDismiss)
            }
        } else log("Cannot send, zip file is null.")
    }
656
}
Daniel Wolf's avatar
Daniel Wolf committed
657

658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
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)))
704
705
706
            dialog.dismiss()
        }
        .setOnDismissListener {
707
            onDismiss?.invoke()
708
        }
709
        .setNegativeButton(android.R.string.cancel) { dialog, _ ->
710
            dialog.dismiss()
711
712
        }
        .create()
713
714
    dialog.setCanceledOnTouchOutside(false)
    dialog.show()
Daniel Wolf's avatar
Daniel Wolf committed
715
}