DnsRuleFragment.kt 31.4 KB
Newer Older
1
package com.frostnerd.smokescreen.fragment
2

3
import android.app.Activity.RESULT_OK
4
import android.content.BroadcastReceiver
5
import android.content.Intent
6
import android.content.IntentFilter
7
import android.net.Uri
Daniel Wolf's avatar
Daniel Wolf committed
8
import android.os.Bundle
9
import android.view.*
10
import android.widget.Switch
11
import androidx.appcompat.app.AppCompatActivity
12
import androidx.fragment.app.Fragment
13
import androidx.recyclerview.widget.LinearLayoutManager
Daniel Wolf's avatar
Daniel Wolf committed
14
import androidx.recyclerview.widget.RecyclerView
15
16
17
18
import androidx.work.Constraints
import androidx.work.NetworkType
import androidx.work.PeriodicWorkRequest
import androidx.work.WorkManager
Daniel Wolf's avatar
Daniel Wolf committed
19
20
import com.frostnerd.cacheadapter.ListDataSource
import com.frostnerd.cacheadapter.ModelAdapterBuilder
21
import com.frostnerd.general.service.isServiceRunning
Daniel Wolf's avatar
Daniel Wolf committed
22
import com.frostnerd.lifecyclemanagement.BaseViewHolder
23
import com.frostnerd.lifecyclemanagement.launchWithLifecylce
24
import com.frostnerd.smokescreen.*
25
import com.frostnerd.smokescreen.database.entities.DnsRule
26
27
import com.frostnerd.smokescreen.database.entities.HostSource
import com.frostnerd.smokescreen.database.getDatabase
28
import com.frostnerd.smokescreen.dialog.DnsRuleDialog
29
import com.frostnerd.smokescreen.dialog.ExportDnsRulesDialog
30
import com.frostnerd.smokescreen.dialog.HostSourceRefreshDialog
31
import com.frostnerd.smokescreen.dialog.NewHostSourceDialog
32
import com.frostnerd.smokescreen.service.RuleExportService
33
import com.frostnerd.smokescreen.service.RuleImportService
Daniel Wolf's avatar
Daniel Wolf committed
34
import com.frostnerd.smokescreen.util.SpaceItemDecorator
35
import com.frostnerd.smokescreen.util.worker.RuleImportStartWorker
36
import com.google.android.material.snackbar.Snackbar
Daniel Wolf's avatar
Daniel Wolf committed
37
import kotlinx.android.synthetic.main.activity_dns_rules.*
38
import kotlinx.android.synthetic.main.item_datasource.view.*
39
import kotlinx.android.synthetic.main.item_datasource.view.cardContent
40
import kotlinx.android.synthetic.main.item_datasource.view.delete
41
42
import kotlinx.android.synthetic.main.item_datasource.view.enable
import kotlinx.android.synthetic.main.item_datasource.view.text
43
import kotlinx.android.synthetic.main.item_datasource_rules.view.*
44
import kotlinx.android.synthetic.main.item_dnsrule_host.view.*
45
import kotlinx.coroutines.GlobalScope
46
import kotlinx.coroutines.Job
47
import kotlinx.coroutines.launch
48
import java.util.concurrent.TimeUnit
49
50
51

/*
 * Copyright (C) 2019 Daniel Wolf (Ch4t4r)
52
 *
53
54
55
56
 * 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.
57

58
59
60
61
 * 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.
62

63
64
 * You should have received a copy of the GNU General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
65
 *
66
67
 * You can contact the developer at daniel.wolf@frostnerd.com.
 */
68
class DnsRuleFragment : Fragment() {
69
70
71
    private lateinit var sourceAdapter: RecyclerView.Adapter<*>
    private lateinit var sourceAdapterList: MutableList<HostSource>
    private lateinit var adapterDataSource: ListDataSource<HostSource>
72
    private lateinit var userDnsRules:MutableList<DnsRule>
73
74
    private lateinit var sourceRuleCount:MutableMap<HostSource, Int?>
    private var importDoneReceiver:BroadcastReceiver? = null
75
    private var exportDoneReceiver:BroadcastReceiver? = null
76
    private var refreshProgressShown = false
77
    private var exportProgressShown = false
78
79
    private var fileChosenRequestCode = 5
    private var fileChosenCallback: ((Uri) -> Unit)? = null
80
    private var userRulesJob: Job? = null
81
    private var totalRuleCount:Long? = null
Daniel Wolf's avatar
Daniel Wolf committed
82
83
84

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
85
86
87
88
89
90
91
92
93
        setHasOptionsMenu(true)
    }

    override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
        return inflater.inflate(R.layout.activity_dns_rules, container, false)
    }

    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)
94
95
96
        userRulesJob = launchWithLifecylce(false) {
            userDnsRules = getDatabase().dnsRuleDao().getAllUserRules().toMutableList()
            userRulesJob = null
97
98
99
100
            launch {
                totalRuleCount = getDatabase().dnsRuleDao().getNonStagedCount()
                updateRuleCountTitle()
            }
101
        }
Daniel Wolf's avatar
Daniel Wolf committed
102
        addSource.setOnClickListener {
103
            NewHostSourceDialog(context!!, onSourceCreated = { newSource ->
104
                if (!sourceAdapterList.contains(newSource)) {
105
                    val insertPos = sourceAdapterList.indexOfFirst {
106
                        it.name > newSource.name
107
108
109
                    }.let {
                        when (it) {
                            0 -> 0
110
                            -1 -> sourceAdapterList.size
111
112
                            else -> it
                        }
113
                    }
114
                    sourceAdapterList.add(insertPos, newSource)
115
                    sourceAdapter.notifyItemInserted(insertPos)
116
                    list.scrollToPosition(insertPos)
117
                    getDatabase().hostSourceDao().insert(newSource)
118
                }
119
120
121
122
123
124
125
            }, showFileChooser = { callback ->
                fileChosenCallback = callback
                startActivityForResult(Intent(Intent.ACTION_OPEN_DOCUMENT).apply {
                    addCategory(Intent.CATEGORY_OPENABLE)
                    type = "text/*"
                }, fileChosenRequestCode)
            }).show()
Daniel Wolf's avatar
Daniel Wolf committed
126
        }
127
        refresh.setOnClickListener {
128
            HostSourceRefreshDialog(context!!,runRefresh =  {
129
130
131
132
133
134
135
                if(context!!.isServiceRunning(RuleImportService::class.java)) {
                    context!!.startService(Intent(context!!, RuleImportService::class.java).putExtra("abort", true))
                } else {
                    context!!.startService(Intent(context!!, RuleImportService::class.java))
                    refreshProgress.show()
                    refreshProgressShown = true
                }
136
137
            }, refreshConfigChanged = {
                getPreferences().apply {
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
                    val workManager = WorkManager.getInstance(context!!)
                    workManager.cancelAllWorkByTag("hostSourceRefresh")
                    if(automaticHostRefresh) {
                        val constraints = Constraints.Builder()
                            .setRequiresStorageNotLow(true)
                            .setRequiresBatteryNotLow(true)
                            .setRequiredNetworkType(if (this.automaticHostRefreshWifiOnly) NetworkType.UNMETERED else NetworkType.CONNECTED)
                            .build()
                        val mappedTimeAmount = automaticHostRefreshTimeAmount.let {
                            if (automaticHostRefreshTimeUnit == HostSourceRefreshDialog.TimeUnit.WEEKS) it * 7
                            else it
                        }.toLong()
                        val mappedTimeUnit = automaticHostRefreshTimeUnit.let {
                            when (it) {
                                HostSourceRefreshDialog.TimeUnit.WEEKS -> TimeUnit.DAYS
                                HostSourceRefreshDialog.TimeUnit.DAYS -> TimeUnit.DAYS
                                HostSourceRefreshDialog.TimeUnit.HOURS -> TimeUnit.HOURS
                                HostSourceRefreshDialog.TimeUnit.MINUTES -> TimeUnit.MINUTES
                            }
157
                        }
158
159
160
161
162
163
                        val workRequest = PeriodicWorkRequest.Builder(RuleImportStartWorker::class.java,
                            mappedTimeAmount,
                            mappedTimeUnit)
                            .setConstraints(constraints)
                            .setInitialDelay(mappedTimeAmount, mappedTimeUnit)
                            .addTag("hostSourceRefresh")
164

165
166
                        workManager.enqueue(workRequest.build())
                    }
167
168
                }
            }).show()
169
        }
170
        export.setOnClickListener {
171
172
            if (context!!.isServiceRunning(RuleExportService::class.java)) {
                context!!.startService(Intent(context!!, RuleExportService::class.java).putExtra("abort", true))
173
            } else {
174
                ExportDnsRulesDialog(context!!) { exportFromSources, exportUserRules ->
175
                    fileChosenCallback = {
176
                        val intent = Intent(context!!, RuleExportService::class.java).apply {
177
178
179
180
181
                            putExtra(
                                "params",
                                RuleExportService.Params(exportFromSources, exportUserRules, it.toString())
                            )
                        }
182
                        context!!.startService(intent)
183
184
185
186
187
188
189
190
191
192
193
                        exportProgress.show()
                        exportProgressShown = true
                    }
                    startActivityForResult(Intent(Intent.ACTION_CREATE_DOCUMENT).apply {
                        addCategory(Intent.CATEGORY_OPENABLE)
                        putExtra(Intent.EXTRA_TITLE, "dnsRuleExport.txt")
                        type = "text/*"
                    }, fileChosenRequestCode)
                }.show()
            }
        }
194
        sourceAdapterList = getDatabase().hostSourceDao().getAll().toMutableList()
195
196
197
        sourceRuleCount = sourceAdapterList.map {
            it to (null as Int?)
        }.toMap().toMutableMap()
Daniel Wolf's avatar
Daniel Wolf committed
198
        adapterDataSource = ListDataSource(sourceAdapterList)
199
200
        var showUserRules = false
        var userRuleCount = 0
201
        sourceAdapter = ModelAdapterBuilder.withModelAndViewHolder({ view, type ->
202
203
            when (type) {
                0 -> SourceViewHolder(view, deleteSource = {
204
                    showInfoTextDialog(context!!,
205
206
207
208
209
210
                        getString(R.string.dialog_deletehostsource_title, it.name),
                        getString(R.string.dialog_deletehostsource_message, it.name),
                        getString(R.string.all_yes) to { dialog, _ ->
                            val pos = sourceAdapterList.indexOf(it)
                            sourceAdapterList.removeAt(pos)
                            sourceAdapter.notifyItemRemoved(pos)
211
212
213
214
                            GlobalScope.launch {
                                getDatabase().dnsRuleDao().deleteAllFromSource(it.id)
                                getDatabase().hostSourceDao().delete(it)
                            }
215
216
217
                            dialog.dismiss()
                        }, getString(R.string.all_no) to { dialog, _ ->
                            dialog.dismiss()
218
219
                        }, null
                    )
220
221
                }, changeSourceStatus = { hostSource, enabled ->
                    hostSource.enabled = enabled
222
                    getDatabase().hostSourceDao().setSourceEnabled(hostSource.id, enabled)
223
                }, editSource = { hostSource ->
224
                    NewHostSourceDialog(context!!, onSourceCreated = { newSource ->
225
226
227
228
                        val currentSource = getDatabase().hostSourceDao().findById(hostSource.id)!!.apply {
                            this.name = newSource.name
                            this.source = newSource.source
                            this.whitelistSource = newSource.whitelistSource
229
                        }
230
231
                        getDatabase().hostSourceDao().update(currentSource)

232
                        val index = sourceAdapterList.indexOf(hostSource)
233
234
235
                        sourceAdapterList[index] = currentSource
                        sourceRuleCount[currentSource] = sourceRuleCount[hostSource]
                        sourceRuleCount.remove(hostSource)
236
237
238
239
240
241
242
243
                        sourceAdapter.notifyItemChanged(index)
                    }, showFileChooser = { callback ->
                        fileChosenCallback = callback
                        startActivityForResult(Intent(Intent.ACTION_OPEN_DOCUMENT).apply {
                            addCategory(Intent.CATEGORY_OPENABLE)
                            this.type = "text/*"
                        }, fileChosenRequestCode)
                    }, hostSource = hostSource).show()
244
                })
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
                1 -> CustomRulesViewHolder(
                    view,
                    changeSourceStatus = {
                        getPreferences().customHostsEnabled = it
                    },
                    clearRules = {
                        showInfoTextDialog(context!!,
                            getString(R.string.dialog_clearuserrules_title),
                            getString(R.string.dialog_clearuserrules_message),
                            getString(R.string.all_yes) to { dialog, _ ->
                                getDatabase().dnsRuleRepository().deleteAllUserRulesAsync()
                                userDnsRules = mutableListOf()
                                if (showUserRules) {
                                    sourceAdapter.notifyItemRangeRemoved(sourceAdapterList.size + 1, userRuleCount)
                                }
                                userRuleCount = 0
                                dialog.dismiss()
                            }, getString(R.string.all_no) to { dialog, _ ->
                                dialog.dismiss()
                            }, null
                        )
                    },
                    changeRuleVisibility = {
268
269
270
271
                        val changeVisibility = {
                            showUserRules = !showUserRules
                            if (showUserRules) {
                                userRuleCount = userDnsRules.size
272
                                activity?.runOnUiThread {
273
274
275
276
                                    sourceAdapter.notifyItemRangeInserted(sourceAdapterList.size + 1, userRuleCount)
                                    list.smoothScrollToPosition(sourceAdapterList.size + 1)
                                }
                            } else {
277
278
279
                                activity?.runOnUiThread {
                                    sourceAdapter.notifyItemRangeRemoved(sourceAdapterList.size + 1, userRuleCount)
                                }
280
                                userRuleCount = 0
281
                            }
282
283
284
285
286
                        }
                        if(userRulesJob == null) changeVisibility()
                        else launchWithLifecylce(false) {
                            userRulesJob?.join()
                            changeVisibility()
287
                        }
288
289
290
                    },
                    createRule = {
                        DnsRuleDialog(context!!, onRuleCreated = { newRule ->
291
292
293
294
295
296
297
298
299
                            val insert = {
                                val insertPos = userDnsRules.indexOfFirst {
                                    it.host > newRule.host
                                }.let {
                                    when (it) {
                                        0 -> 0
                                        -1 -> userDnsRules.size
                                        else -> it
                                    }
300
                                }
301
302
303
304
305
306
307
308
309
310
311
312
313
314
                                val id = if (newRule.isWhitelistRule()) {
                                    getDatabase().dnsRuleDao().insertWhitelist(newRule)
                                    if (userDnsRules.any {
                                            println("$it vs $newRule")
                                            it.host == newRule.host && it.type == newRule.type
                                        }) -1L
                                    else 0L
                                } else getDatabase().dnsRuleDao().insertIgnore(newRule)
                                if (id != -1L) {
                                    userDnsRules.add(insertPos, newRule)
                                    val wereRulesShown = showUserRules
                                    showUserRules = true
                                    if (wereRulesShown) {
                                        userRuleCount += 1
315
316
317
                                        activity?.runOnUiThread {
                                            sourceAdapter.notifyItemInserted(sourceAdapterList.size + 1 + insertPos)
                                        }
318
319
                                    } else {
                                        userRuleCount = userDnsRules.size
320
                                        activity?.runOnUiThread {
321
322
323
324
                                            sourceAdapter.notifyItemChanged(sourceAdapterList.size)
                                            sourceAdapter.notifyItemRangeInserted(sourceAdapterList.size + 1, userRuleCount)
                                            list.smoothScrollToPosition(insertPos)
                                        }
325
                                    }
326
327
328
329
330
331
                                } else {
                                    Snackbar.make(
                                        activity!!.findViewById(android.R.id.content),
                                        R.string.window_dnsrules_hostalreadyexists,
                                        Snackbar.LENGTH_LONG
                                    ).show()
332
                                }
333
334
335
336
337
                            }
                            if(userRulesJob == null) insert()
                            else GlobalScope.launch {
                                userRulesJob?.join()
                                insert()
338
                            }
339
340
341
                        }).show()
                    })
                else -> CustomRuleHostViewHolder(view, deleteRule = {
342
343
344
345
346
                    val index = userDnsRules.indexOf(it)
                    userDnsRules.remove(it)
                    getDatabase().dnsRuleRepository().removeAsync(it)
                    userRuleCount -= 1
                    sourceAdapter.notifyItemRemoved(sourceAdapterList.size + 1 + index)
347
348
349
350
                    if(totalRuleCount != null) {
                        totalRuleCount = totalRuleCount!! - 1
                        updateRuleCountTitle()
                    }
351
                }, editRule = {
352
                    DnsRuleDialog(context!!, it) { newRule ->
353
                        val rows = getDatabase().dnsRuleDao().updateIgnore(newRule)
354
                        if (rows > 0) {
355
356
357
358
                            val index = userDnsRules.indexOf(it)
                            userDnsRules[index] = newRule
                            sourceAdapter.notifyItemChanged(sourceAdapterList.size + 1 + index)
                        } else {
359
360
361
362
363
                            Snackbar.make(
                                activity!!.findViewById(android.R.id.content),
                                R.string.window_dnsrules_hostalreadyexists,
                                Snackbar.LENGTH_LONG
                            ).show()
364
                        }
365
366
                    }.show()
                })
367
            }
Daniel Wolf's avatar
Daniel Wolf committed
368
        }, adapterDataSource) {
369
370
            viewBuilder = { parent, type ->
                layoutInflater.inflate(
371
372
373
374
375
                    when (type) {
                        0 -> R.layout.item_datasource
                        1 -> R.layout.item_datasource_rules
                        else -> R.layout.item_dnsrule_host
                    },
376
377
378
379
380
                    parent,
                    false
                )
            }
            getItemCount = {
381
                sourceAdapterList.size + 1 + userRuleCount
382
            }
383
            bindModelView = { viewHolder, position, data ->
384
                (viewHolder as SourceViewHolder).display(data)
385
                when {
386
387
388
389
390
391
392
393
394
395
396
                    sourceRuleCount[data] != null -> {
                        viewHolder.ruleCount.text = getString(R.string.window_dnsrules_customhosts_hostsource_rulecount,
                            sourceRuleCount[data],
                            data.ruleCount.let {
                                when (it) {
                                    null -> 0
                                    0 -> 0
                                    else -> it - sourceRuleCount[data]!!
                                }
                            })
                    }
397
                    data.enabled -> launchWithLifecylce(false) {
398
                        val prev = sourceRuleCount[data]
399
                        sourceRuleCount[data] = getDatabase().dnsRuleDao().getCountForHostSource(data.id)
400
                        if(prev != sourceRuleCount[data]) runOnUiThread {
401
402
403
404
405
                            sourceAdapter.notifyItemChanged(position)
                        }
                    }
                    else -> viewHolder.ruleCount.text = getString(R.string.window_dnsrules_customhosts_hostsource_rulecount_pending)
                }
406
407
            }
            bindNonModelView = { viewHolder, position ->
408
409
                if(viewHolder is CustomRulesViewHolder) {
                    viewHolder.enabled.isChecked = getPreferences().customHostsEnabled
410
411
412
413
                    if(!viewHolder.elementsShown && showUserRules) {
                        viewHolder.animateCaretSpin()
                        viewHolder.elementsShown = true
                    }
414
415
                } else if(viewHolder is CustomRuleHostViewHolder) {
                    viewHolder.display(userDnsRules[position - sourceAdapterList.size - 1])
416
417
418
                }
            }
            getViewType = { position ->
419
420
421
422
423
                when {
                    position < sourceAdapterList.size -> 0
                    position == sourceAdapterList.size -> 1
                    else -> 2
                }
424
425
            }
            runOnUiThread = {
426
                activity?.runOnUiThread(it)
427
            }
Daniel Wolf's avatar
Daniel Wolf committed
428
429

        }.build()
430
        list.layoutManager = LinearLayoutManager(context!!)
431
        list.recycledViewPool.setMaxRecycledViews(1, 1)
432
        list.addItemDecoration(SpaceItemDecorator(context!!))
433
        list.adapter = sourceAdapter
434
        if(context!!.isServiceRunning(RuleImportService::class.java)) {
435
436
            refreshProgress.addOnLayoutChangeListener { _, _, _, _, _, _, _, _, _ ->
                refreshProgress.show()
437
                refreshProgressShown = true
438
439
            }
        }
440
        if(context!!.isServiceRunning(RuleExportService::class.java)) {
441
442
443
444
445
            exportProgress.addOnLayoutChangeListener { _, _, _, _, _, _, _, _, _ ->
                exportProgressShown = true
                exportProgress.show()
            }
        }
446
447
    }

448
449
450
451
452
453
454
455
    override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
        super.onActivityResult(requestCode, resultCode, data)
        if(requestCode == fileChosenRequestCode && resultCode == RESULT_OK){
            if(data?.data != null) fileChosenCallback?.invoke(data.data!!)
            fileChosenCallback = null
        }
    }

456
457
    override fun onResume() {
        super.onResume()
458
        importDoneReceiver = context!!.registerLocalReceiver(IntentFilter(RuleImportService.BROADCAST_IMPORT_DONE)) {
459
            refreshProgress.hide()
460
            refreshProgressShown = false
461

462
            launchWithLifecylce(false) {
463
464
465
466
467
468
469
470
471
472
                sourceRuleCount.keys.forEach {
                    val index = sourceAdapterList.indexOf(it)
                    if(index >= 0 && (sourceAdapterList[index].enabled || sourceRuleCount[it] != null)) {
                        sourceRuleCount[it] = null
                        launchWithLifecylce(true) {
                            sourceAdapter.notifyItemChanged(index)
                        }
                    } else sourceRuleCount[it] = null
                }
                sourceAdapterList = getDatabase().hostSourceDao().getAll().toMutableList()
473
474
475
                totalRuleCount = getDatabase().dnsRuleDao().getNonStagedCount()
                updateRuleCountTitle()
            }
476
        }
477
        exportDoneReceiver = context!!.registerLocalReceiver(IntentFilter(RuleExportService.BROADCAST_EXPORT_DONE)) {
478
479
480
            exportProgress.hide()
            exportProgressShown = false
        }
481
        if(!context!!.isServiceRunning(RuleImportService::class.java) && refreshProgressShown) {
482
            refreshProgress.hide()
483
            refreshProgressShown = false
484
        }
485
        if(!context!!.isServiceRunning(RuleExportService::class.java) && exportProgressShown) {
486
487
488
            exportProgress.show()
            exportProgressShown = false
        }
489
490
    }

491
492
    private fun updateRuleCountTitle() {
        activity?.runOnUiThread {
Daniel Wolf's avatar
Daniel Wolf committed
493
            (activity as AppCompatActivity?)?.supportActionBar?.subtitle = resources.getQuantityString(R.plurals.window_dnsrules_subtitle, totalRuleCount!!.toInt(), totalRuleCount)
494
495
496
        }
    }

497
498
    override fun onPause() {
        super.onPause()
499
500
        context!!.unregisterLocalReceiver(importDoneReceiver)
        context!!.unregisterLocalReceiver(exportDoneReceiver)
Daniel Wolf's avatar
Daniel Wolf committed
501
    }
502

503
504
505
    override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
        inflater.inflate(R.menu.menu_dnsrule, menu)
        val switch =  menu.getItem(0)?.actionView?.findViewById<Switch>(R.id.actionbarSwitch)
506
507
508
        switch?.isChecked = getPreferences().dnsRulesEnabled.also {
            overlay.visibility = if(it) View.GONE else View.VISIBLE
        }
509
        switch?.setOnCheckedChangeListener { _, isChecked ->
510
            getPreferences().dnsRulesEnabled = isChecked
511
            overlay.visibility = if(isChecked) View.GONE else View.VISIBLE
512
513
514
        }
    }

515
516
517
    private class SourceViewHolder(
        view: View,
        deleteSource: (HostSource) -> Unit,
518
519
        changeSourceStatus: (HostSource, enabled: Boolean) -> Unit,
        editSource: (HostSource) -> Unit
520
521
522
523
524
    ) : BaseViewHolder(view) {
        val text = view.text
        val subText = view.subText
        val enabled = view.enable
        val delete = view.delete
525
        val ruleCount = view.ruleCount
526
        val whitelistIndicator = view.sourceWhitelistIndicator
527
528
529
530
531
532
533
534
535
536
        lateinit var source: HostSource

        init {
            delete.setOnClickListener {
                deleteSource(source)
            }
            enabled.setOnCheckedChangeListener { _, isChecked ->
                changeSourceStatus(source, isChecked)
            }
            view.cardContent.setOnClickListener {
537
                editSource(source)
538
539
540
541
542
543
544
545
            }
        }

        fun display(source: HostSource) {
            this.source = source
            text.text = source.name
            enabled.isChecked = source.enabled
            subText.text = source.source
546
            whitelistIndicator.visibility = if(source.whitelistSource) View.VISIBLE else View.GONE
547
548
549
550
551
        }

        override fun destroy() {}
    }

552
553
554
    private class CustomRulesViewHolder(view: View,
                                        changeSourceStatus: (Boolean) -> Unit,
                                        clearRules: () -> Unit,
555
556
                                        changeRuleVisibility:(showRules:Boolean) -> Unit,
                                        createRule:() -> Unit) :
557
        BaseViewHolder(view) {
558
        val clear = view.clear
559
        val enabled = view.enable
560
        val openList = view.openList
561
        val add = view.add
562
        var elementsShown = false
563
564
565
566
567
568
569
570

        init {
            enabled.setOnCheckedChangeListener { _, isChecked ->
                changeSourceStatus(isChecked)
            }
            clear.setOnClickListener {
                clearRules()
            }
571
            openList.setOnClickListener {
572
                animateCaretSpin()
573
574
575
                changeRuleVisibility(!elementsShown)
                elementsShown = !elementsShown
            }
576
577
578
            view.cardContent.setOnClickListener {
                enabled.isChecked = !enabled.isChecked
            }
579
580
581
            add.setOnClickListener {
                createRule()
            }
582
583
        }

584
585
586
587
588
589
590
591
        fun animateCaretSpin(){
            if(elementsShown) {
                openList.animate().rotationBy(-90f).setDuration(350).start()
            } else {
                openList.animate().rotationBy(90f).setDuration(350).start()
            }
        }

Daniel Wolf's avatar
Daniel Wolf committed
592
593
        override fun destroy() {}
    }
594
595

    private class CustomRuleHostViewHolder(view:View,
596
                                           deleteRule:(DnsRule) -> Unit,
597
                                           editRule:(DnsRule) -> Unit):BaseViewHolder(view) {
598
599
        val text = view.text
        val delete = view.delete
600
        val cardContent = view.cardContent
601
        val whitelistIndicator = view.whitelistIndicator
602
603
604
605
606
607
        lateinit var dnsRule:DnsRule

        init {
            delete.setOnClickListener {
                deleteRule(dnsRule)
            }
608
609
610
            cardContent.setOnClickListener {
                editRule(dnsRule)
            }
611
612
613
614
        }

        fun display(rule:DnsRule) {
            dnsRule = rule
615
            text.text = DnsRuleDialog.printableHost(rule.host)
616
            whitelistIndicator.visibility = if(rule.isWhitelistRule()) View.VISIBLE else View.GONE
617
618
        }
        override fun destroy() {}
619
    }
620

621
    companion object {
622
        const val latestSourcesVersion = 2
623
        private val defaultHostSources:Map<Int, List<HostSource>> by lazy(LazyThreadSafetyMode.NONE) {
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
            mutableMapOf<Int, List<HostSource>>().apply {
                put(1, mutableListOf(
                    HostSource("Energized Basic", "https://raw.githubusercontent.com/EnergizedProtection/block/master/basic/formats/domains.txt"),
                    HostSource("Energized Blu", "https://raw.githubusercontent.com/EnergizedProtection/block/master/blu/formats/domains.txt"),
                    HostSource("Energized Spark", "https://raw.githubusercontent.com/EnergizedProtection/block/master/spark/formats/domains.txt"),
                    HostSource("Energized Porn", "https://raw.githubusercontent.com/EnergizedProtection/block/master/porn/formats/domains.txt"),
                    HostSource("Energized Ultimate", "https://raw.githubusercontent.com/EnergizedProtection/block/master/ultimate/formats/domains.txt"),
                    HostSource("AdAway", "https://adaway.org/hosts.txt"),
                    HostSource("StevenBlack unified", "https://raw.githubusercontent.com/StevenBlack/hosts/master/hosts"),
                    HostSource("CoinBlockerList", "https://zerodot1.gitlab.io/CoinBlockerLists/hosts"),
                    HostSource("Malewaredomainlist.com", "https://www.malwaredomainlist.com/hostslist/hosts.txt"),
                    HostSource("PiHoleBlocklist Android tracking", "https://raw.githubusercontent.com/Perflyst/PiHoleBlocklist/master/android-tracking.txt"),
                    HostSource("Quidsup NoTrack Tracker Blocklist", "https://gitlab.com/quidsup/notrack-blocklists/raw/master/notrack-blocklist.txt"),
                    HostSource("someonewhocares.org", "https://someonewhocares.org/hosts/zero/hosts")
                ).apply {
                    forEach { it.enabled = false }
                })
Daniel Wolf's avatar
Daniel Wolf committed
641
642
643
                put(2, mutableListOf(
                    HostSource("Energized unblock", "https://raw.githubusercontent.com/EnergizedProtection/unblock/master/basic/formats/domains.txt", true),
                    HostSource("hblock", "https://hblock.molinero.dev/hosts")
644
645
646
                ).apply {
                    forEach { it.enabled = false }
                })
647
648
            }
        }
649
650
651
652
653

        fun getDefaultHostSources(versionStart:Int):List<HostSource> {
            return getDefaultHostSources(versionStart..Integer.MAX_VALUE)
        }

Daniel Wolf's avatar
Daniel Wolf committed
654
        private fun getDefaultHostSources(versionRange:IntRange): List<HostSource> {
Daniel Wolf's avatar
Daniel Wolf committed
655
            if(versionRange.first > latestSourcesVersion) return emptyList()
656
657
658
659
            return defaultHostSources.filter {
                it.key in versionRange
            }.values.flatten()
        }
660
    }
661
}