aboutsummaryrefslogtreecommitdiffstats
path: root/app/src/main/java/foundation/e/advancedprivacy/common
diff options
context:
space:
mode:
authorGuillaume Jacquart <guillaume.jacquart@hoodbrains.com>2023-05-02 21:25:17 +0200
committerGuillaume Jacquart <guillaume.jacquart@hoodbrains.com>2023-05-02 22:00:35 +0200
commita8874167f663885f2d3371801cf03681576ac817 (patch)
tree5be07b8768142efeade536d4135f2250c1ac9071 /app/src/main/java/foundation/e/advancedprivacy/common
parenta0ee04ea9dbc0802c828afdf660eb37dc6fa350f (diff)
downloadadvanced-privacy-a8874167f663885f2d3371801cf03681576ac817.tar.gz
1200: rename everything to AdvancedPrivacy
Diffstat (limited to 'app/src/main/java/foundation/e/advancedprivacy/common')
-rw-r--r--app/src/main/java/foundation/e/advancedprivacy/common/AppsAdapter.kt71
-rw-r--r--app/src/main/java/foundation/e/advancedprivacy/common/BootCompletedReceiver.kt36
-rw-r--r--app/src/main/java/foundation/e/advancedprivacy/common/Factory.kt23
-rw-r--r--app/src/main/java/foundation/e/advancedprivacy/common/GraphHolder.kt333
-rw-r--r--app/src/main/java/foundation/e/advancedprivacy/common/NavToolbarFragment.kt33
-rw-r--r--app/src/main/java/foundation/e/advancedprivacy/common/RightRadioButton.kt43
-rw-r--r--app/src/main/java/foundation/e/advancedprivacy/common/TextViewHelpers.kt63
-rw-r--r--app/src/main/java/foundation/e/advancedprivacy/common/ThrottleFlow.kt36
-rw-r--r--app/src/main/java/foundation/e/advancedprivacy/common/ToggleAppsAdapter.kt76
-rw-r--r--app/src/main/java/foundation/e/advancedprivacy/common/ToolbarFragment.kt45
-rw-r--r--app/src/main/java/foundation/e/advancedprivacy/common/WarningDialog.kt130
-rw-r--r--app/src/main/java/foundation/e/advancedprivacy/common/extensions/AnyExtension.kt22
12 files changed, 911 insertions, 0 deletions
diff --git a/app/src/main/java/foundation/e/advancedprivacy/common/AppsAdapter.kt b/app/src/main/java/foundation/e/advancedprivacy/common/AppsAdapter.kt
new file mode 100644
index 0000000..aee1890
--- /dev/null
+++ b/app/src/main/java/foundation/e/advancedprivacy/common/AppsAdapter.kt
@@ -0,0 +1,71 @@
+/*
+ * Copyright (C) 2021 E FOUNDATION, 2022 - 2023 MURENA SAS
+ *
+ * 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 <https://www.gnu.org/licenses/>.
+ */
+
+package foundation.e.advancedprivacy.common
+
+import android.view.LayoutInflater
+import android.view.View
+import android.view.ViewGroup
+import android.widget.ImageView
+import android.widget.TextView
+import androidx.recyclerview.widget.RecyclerView
+import foundation.e.advancedprivacy.R
+import foundation.e.advancedprivacy.domain.entities.AppWithCounts
+
+class AppsAdapter(
+ private val itemsLayout: Int,
+ private val listener: (Int) -> Unit
+) :
+ RecyclerView.Adapter<AppsAdapter.ViewHolder>() {
+
+ class ViewHolder(view: View, private val listener: (Int) -> Unit) : RecyclerView.ViewHolder(view) {
+ val appName: TextView = view.findViewById(R.id.title)
+ val counts: TextView = view.findViewById(R.id.counts)
+ val icon: ImageView = view.findViewById(R.id.icon)
+ fun bind(item: AppWithCounts) {
+ appName.text = item.label
+ counts.text = if (item.trackersCount > 0) itemView.context.getString(
+ R.string.trackers_app_trackers_counts,
+ item.blockedTrackersCount,
+ item.trackersCount,
+ item.leaks
+ ) else ""
+ icon.setImageDrawable(item.icon)
+
+ itemView.setOnClickListener { listener(item.uid) }
+ }
+ }
+
+ var dataSet: List<AppWithCounts> = emptyList()
+ set(value) {
+ field = value
+ notifyDataSetChanged()
+ }
+
+ override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
+ val view = LayoutInflater.from(parent.context)
+ .inflate(itemsLayout, parent, false)
+ return ViewHolder(view, listener)
+ }
+
+ override fun onBindViewHolder(holder: ViewHolder, position: Int) {
+ val app = dataSet[position]
+ holder.bind(app)
+ }
+
+ override fun getItemCount(): Int = dataSet.size
+}
diff --git a/app/src/main/java/foundation/e/advancedprivacy/common/BootCompletedReceiver.kt b/app/src/main/java/foundation/e/advancedprivacy/common/BootCompletedReceiver.kt
new file mode 100644
index 0000000..d73f770
--- /dev/null
+++ b/app/src/main/java/foundation/e/advancedprivacy/common/BootCompletedReceiver.kt
@@ -0,0 +1,36 @@
+/*
+ * Copyright (C) 2022 E FOUNDATION
+ *
+ * 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 <https://www.gnu.org/licenses/>.
+ */
+
+package foundation.e.advancedprivacy.common
+
+import android.content.BroadcastReceiver
+import android.content.Context
+import android.content.Intent
+import foundation.e.advancedprivacy.Notifications
+import foundation.e.advancedprivacy.data.repositories.LocalStateRepository
+
+class BootCompletedReceiver : BroadcastReceiver() {
+ override fun onReceive(context: Context, intent: Intent?) {
+ if (intent?.action == Intent.ACTION_BOOT_COMPLETED) {
+ val localStateRepository = LocalStateRepository(context)
+ if (localStateRepository.firstBoot) {
+ Notifications.showFirstBootNotification(context)
+ localStateRepository.firstBoot = false
+ }
+ }
+ }
+}
diff --git a/app/src/main/java/foundation/e/advancedprivacy/common/Factory.kt b/app/src/main/java/foundation/e/advancedprivacy/common/Factory.kt
new file mode 100644
index 0000000..3af0b37
--- /dev/null
+++ b/app/src/main/java/foundation/e/advancedprivacy/common/Factory.kt
@@ -0,0 +1,23 @@
+/*
+ * Copyright (C) 2021 E FOUNDATION
+ *
+ * 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 <https://www.gnu.org/licenses/>.
+ */
+
+package foundation.e.advancedprivacy.common
+
+// Definition of a Factory interface with a function to create objects of a type
+interface Factory<T> {
+ fun create(): T
+}
diff --git a/app/src/main/java/foundation/e/advancedprivacy/common/GraphHolder.kt b/app/src/main/java/foundation/e/advancedprivacy/common/GraphHolder.kt
new file mode 100644
index 0000000..ca4fcb6
--- /dev/null
+++ b/app/src/main/java/foundation/e/advancedprivacy/common/GraphHolder.kt
@@ -0,0 +1,333 @@
+/*
+ * Copyright (C) 2021 E FOUNDATION
+ *
+ * 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 <https://www.gnu.org/licenses/>.
+ */
+
+package foundation.e.advancedprivacy.common
+
+import android.content.Context
+import android.graphics.Canvas
+import android.text.Spannable
+import android.text.SpannableStringBuilder
+import android.text.style.DynamicDrawableSpan
+import android.text.style.ImageSpan
+import android.view.View
+import android.widget.TextView
+import androidx.core.content.ContextCompat
+import androidx.core.text.toSpannable
+import androidx.core.view.isVisible
+import com.github.mikephil.charting.charts.BarChart
+import com.github.mikephil.charting.components.AxisBase
+import com.github.mikephil.charting.components.MarkerView
+import com.github.mikephil.charting.components.XAxis
+import com.github.mikephil.charting.components.YAxis
+import com.github.mikephil.charting.components.YAxis.AxisDependency
+import com.github.mikephil.charting.data.BarData
+import com.github.mikephil.charting.data.BarDataSet
+import com.github.mikephil.charting.data.BarEntry
+import com.github.mikephil.charting.data.Entry
+import com.github.mikephil.charting.formatter.ValueFormatter
+import com.github.mikephil.charting.highlight.Highlight
+import com.github.mikephil.charting.listener.OnChartValueSelectedListener
+import com.github.mikephil.charting.renderer.XAxisRenderer
+import com.github.mikephil.charting.utils.MPPointF
+import foundation.e.advancedprivacy.R
+import foundation.e.advancedprivacy.common.extensions.dpToPxF
+import kotlin.math.floor
+
+class GraphHolder(val barChart: BarChart, val context: Context, val isMarkerAbove: Boolean = true) {
+ var data = emptyList<Pair<Int, Int>>()
+ set(value) {
+ field = value
+ refreshDataSet()
+ }
+ var labels = emptyList<String>()
+
+ var graduations: List<String?>? = null
+
+ private var isHighlighted = false
+
+ init {
+ barChart.description = null
+ barChart.setTouchEnabled(true)
+ barChart.setScaleEnabled(false)
+
+ barChart.setDrawGridBackground(false)
+ barChart.setDrawBorders(false)
+ barChart.axisLeft.isEnabled = false
+ barChart.axisRight.isEnabled = false
+
+ barChart.legend.isEnabled = false
+
+ if (isMarkerAbove) prepareXAxisDashboardDay() else prepareXAxisMarkersBelow()
+
+ val periodMarker = PeriodMarkerView(context, isMarkerAbove)
+ periodMarker.chartView = barChart
+ barChart.marker = periodMarker
+
+ barChart.setOnChartValueSelectedListener(object : OnChartValueSelectedListener {
+ override fun onValueSelected(e: Entry?, h: Highlight?) {
+ h?.let {
+ val index = it.x.toInt()
+ if (index >= 0 &&
+ index < labels.size &&
+ index < this@GraphHolder.data.size
+ ) {
+ val period = labels[index]
+ val (blocked, leaked) = this@GraphHolder.data[index]
+ periodMarker.setLabel(period, blocked, leaked)
+ }
+ }
+ isHighlighted = true
+ }
+
+ override fun onNothingSelected() {
+ isHighlighted = false
+ }
+ })
+ }
+
+ private fun prepareXAxisDashboardDay() {
+ barChart.extraTopOffset = 44f
+
+ barChart.offsetTopAndBottom(0)
+
+ barChart.setXAxisRenderer(object : XAxisRenderer(barChart.viewPortHandler, barChart.xAxis, barChart.getTransformer(AxisDependency.LEFT)) {
+ override fun renderAxisLine(c: Canvas) {
+ mAxisLinePaint.color = mXAxis.axisLineColor
+ mAxisLinePaint.strokeWidth = mXAxis.axisLineWidth
+ mAxisLinePaint.pathEffect = mXAxis.axisLineDashPathEffect
+
+ // Top line
+ c.drawLine(
+ mViewPortHandler.contentLeft(),
+ mViewPortHandler.contentTop(), mViewPortHandler.contentRight(),
+ mViewPortHandler.contentTop(), mAxisLinePaint
+ )
+
+ // Bottom line
+ c.drawLine(
+ mViewPortHandler.contentLeft(),
+ mViewPortHandler.contentBottom() - 7.dpToPxF(context),
+ mViewPortHandler.contentRight(),
+ mViewPortHandler.contentBottom() - 7.dpToPxF(context),
+ mAxisLinePaint
+ )
+ }
+
+ override fun renderGridLines(c: Canvas) {
+ if (!mXAxis.isDrawGridLinesEnabled || !mXAxis.isEnabled) return
+ val clipRestoreCount = c.save()
+ c.clipRect(gridClippingRect)
+ if (mRenderGridLinesBuffer.size != mAxis.mEntryCount * 2) {
+ mRenderGridLinesBuffer = FloatArray(mXAxis.mEntryCount * 2)
+ }
+ val positions = mRenderGridLinesBuffer
+ run {
+ var i = 0
+ while (i < positions.size) {
+ positions[i] = mXAxis.mEntries[i / 2]
+ positions[i + 1] = mXAxis.mEntries[i / 2]
+ i += 2
+ }
+ }
+
+ mTrans.pointValuesToPixel(positions)
+ setupGridPaint()
+ val gridLinePath = mRenderGridLinesPath
+ gridLinePath.reset()
+ var i = 0
+ while (i < positions.size) {
+ val bottomY = if (graduations?.getOrNull(i / 2) != null) 0 else 3
+ val x = positions[i]
+ gridLinePath.moveTo(x, mViewPortHandler.contentBottom() - 7.dpToPxF(context))
+ gridLinePath.lineTo(x, mViewPortHandler.contentBottom() - bottomY.dpToPxF(context))
+
+ c.drawPath(gridLinePath, mGridPaint)
+
+ gridLinePath.reset()
+
+ i += 2
+ }
+ c.restoreToCount(clipRestoreCount)
+ }
+ })
+
+ barChart.setDrawValueAboveBar(false)
+ barChart.xAxis.apply {
+ isEnabled = true
+ position = XAxis.XAxisPosition.BOTTOM
+
+ setDrawGridLines(true)
+ setDrawLabels(true)
+ setCenterAxisLabels(false)
+ setLabelCount(25, true)
+ textColor = context.getColor(R.color.primary_text)
+ valueFormatter = object : ValueFormatter() {
+ override fun getAxisLabel(value: Float, axis: AxisBase?): String {
+ return graduations?.getOrNull(floor(value).toInt() + 1) ?: ""
+ }
+ }
+ }
+ }
+
+ private fun prepareXAxisMarkersBelow() {
+ barChart.extraBottomOffset = 44f
+
+ barChart.offsetTopAndBottom(0)
+ barChart.setDrawValueAboveBar(false)
+
+ barChart.xAxis.apply {
+ isEnabled = true
+ position = XAxis.XAxisPosition.BOTH_SIDED
+ setDrawGridLines(false)
+ setDrawLabels(false)
+ }
+ }
+
+ fun highlightIndex(index: Int) {
+ if (index >= 0 && index < data.size) {
+ val xPx = barChart.getTransformer(YAxis.AxisDependency.LEFT)
+ .getPixelForValues(index.toFloat(), 0f)
+ .x
+ val highlight = Highlight(
+ index.toFloat(), 0f,
+ xPx.toFloat(), 0f,
+ 0, YAxis.AxisDependency.LEFT
+ )
+
+ barChart.highlightValue(highlight, true)
+ }
+ }
+
+ private fun refreshDataSet() {
+ val trackersDataSet = BarDataSet(
+ data.mapIndexed { index, value ->
+ BarEntry(
+ index.toFloat(),
+ floatArrayOf(value.first.toFloat(), value.second.toFloat())
+ )
+ },
+ ""
+ ).apply {
+
+ val blockedColor = ContextCompat.getColor(context, R.color.accent)
+ val leakedColor = ContextCompat.getColor(context, R.color.red_off)
+
+ colors = listOf(
+ blockedColor,
+ leakedColor
+ )
+
+ setDrawValues(false)
+ }
+
+ barChart.data = BarData(trackersDataSet)
+ barChart.invalidate()
+ }
+}
+
+class PeriodMarkerView(context: Context, private val isMarkerAbove: Boolean = true) : MarkerView(context, R.layout.chart_tooltip) {
+ enum class ArrowPosition { LEFT, CENTER, RIGHT }
+
+ private val arrowMargins = 10.dpToPxF(context)
+ private val mOffset2 = MPPointF(0f, 0f)
+
+ private fun getArrowPosition(posX: Float): ArrowPosition {
+ val halfWidth = width / 2
+
+ return chartView?.let { chart ->
+ if (posX < halfWidth) {
+ ArrowPosition.LEFT
+ } else if (chart.width - posX < halfWidth) {
+ ArrowPosition.RIGHT
+ } else {
+ ArrowPosition.CENTER
+ }
+ } ?: ArrowPosition.CENTER
+ }
+
+ private fun showArrow(position: ArrowPosition?) {
+ val ids = listOf(
+ R.id.arrow_top_left, R.id.arrow_top_center, R.id.arrow_top_right,
+ R.id.arrow_bottom_left, R.id.arrow_bottom_center, R.id.arrow_bottom_right
+ )
+
+ val toShow = if (isMarkerAbove) when (position) {
+ ArrowPosition.LEFT -> R.id.arrow_bottom_left
+ ArrowPosition.CENTER -> R.id.arrow_bottom_center
+ ArrowPosition.RIGHT -> R.id.arrow_bottom_right
+ else -> null
+ } else when (position) {
+ ArrowPosition.LEFT -> R.id.arrow_top_left
+ ArrowPosition.CENTER -> R.id.arrow_top_center
+ ArrowPosition.RIGHT -> R.id.arrow_top_right
+ else -> null
+ }
+
+ ids.forEach { id ->
+ val showIt = id == toShow
+ findViewById<View>(id)?.let {
+ if (it.isVisible != showIt) {
+ it.isVisible = showIt
+ }
+ }
+ }
+ }
+
+ fun setLabel(period: String, blocked: Int, leaked: Int) {
+ val span = SpannableStringBuilder(period)
+ span.append(": $blocked ")
+ span.setSpan(
+ ImageSpan(context, R.drawable.ic_legend_blocked, DynamicDrawableSpan.ALIGN_BASELINE),
+ span.length - 1,
+ span.length,
+ Spannable.SPAN_EXCLUSIVE_EXCLUSIVE
+ )
+ span.append(" $leaked ")
+ span.setSpan(
+ ImageSpan(context, R.drawable.ic_legend_leaked, DynamicDrawableSpan.ALIGN_BASELINE),
+ span.length - 1,
+ span.length,
+ Spannable.SPAN_EXCLUSIVE_EXCLUSIVE
+ )
+ findViewById<TextView>(R.id.label).text = span.toSpannable()
+ }
+
+ override fun refreshContent(e: Entry?, highlight: Highlight?) {
+ highlight?.let {
+ showArrow(getArrowPosition(highlight.xPx))
+ }
+ super.refreshContent(e, highlight)
+ }
+
+ override fun getOffsetForDrawingAtPoint(posX: Float, posY: Float): MPPointF {
+ val x = when (getArrowPosition(posX)) {
+ ArrowPosition.LEFT -> -arrowMargins
+ ArrowPosition.RIGHT -> -width + arrowMargins
+ ArrowPosition.CENTER -> -width.toFloat() / 2
+ }
+
+ mOffset2.x = x
+ mOffset2.y = if (isMarkerAbove) -posY
+ else -posY + (chartView?.height?.toFloat() ?: 0f) - height
+
+ return mOffset2
+ }
+
+ override fun draw(canvas: Canvas?, posX: Float, posY: Float) {
+ super.draw(canvas, posX, posY)
+ }
+}
diff --git a/app/src/main/java/foundation/e/advancedprivacy/common/NavToolbarFragment.kt b/app/src/main/java/foundation/e/advancedprivacy/common/NavToolbarFragment.kt
new file mode 100644
index 0000000..1417977
--- /dev/null
+++ b/app/src/main/java/foundation/e/advancedprivacy/common/NavToolbarFragment.kt
@@ -0,0 +1,33 @@
+/*
+ * Copyright (C) 2021 E FOUNDATION
+ *
+ * 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 <https://www.gnu.org/licenses/>.
+ */
+
+package foundation.e.advancedprivacy.common
+
+import androidx.annotation.LayoutRes
+import com.google.android.material.appbar.MaterialToolbar
+
+abstract class NavToolbarFragment(@LayoutRes contentLayoutId: Int) : ToolbarFragment(contentLayoutId) {
+
+ override fun setupToolbar(toolbar: MaterialToolbar) {
+ super.setupToolbar(toolbar)
+ toolbar.apply {
+ setNavigationOnClickListener {
+ requireActivity().onBackPressed()
+ }
+ }
+ }
+}
diff --git a/app/src/main/java/foundation/e/advancedprivacy/common/RightRadioButton.kt b/app/src/main/java/foundation/e/advancedprivacy/common/RightRadioButton.kt
new file mode 100644
index 0000000..c10d755
--- /dev/null
+++ b/app/src/main/java/foundation/e/advancedprivacy/common/RightRadioButton.kt
@@ -0,0 +1,43 @@
+/*
+ * Copyright (C) 2021 E FOUNDATION
+ *
+ * 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 <https://www.gnu.org/licenses/>.
+ */
+
+package foundation.e.advancedprivacy.common
+
+import android.annotation.SuppressLint
+import android.content.Context
+import android.util.AttributeSet
+import android.widget.RadioButton
+
+/**
+ * A custom [RadioButton] which displays the radio drawable on the right side.
+ */
+@SuppressLint("AppCompatCustomView")
+class RightRadioButton : RadioButton {
+
+ constructor(context: Context) : super(context)
+ constructor(context: Context, attrs: AttributeSet?) : super(context, attrs)
+ constructor(context: Context, attrs: AttributeSet?, defStyleAttr: Int) : super(
+ context,
+ attrs,
+ defStyleAttr
+ )
+
+ // Returns layout direction as right-to-left to draw the compound button on right side.
+ override fun getLayoutDirection(): Int {
+ return LAYOUT_DIRECTION_RTL
+ }
+}
diff --git a/app/src/main/java/foundation/e/advancedprivacy/common/TextViewHelpers.kt b/app/src/main/java/foundation/e/advancedprivacy/common/TextViewHelpers.kt
new file mode 100644
index 0000000..f87834a
--- /dev/null
+++ b/app/src/main/java/foundation/e/advancedprivacy/common/TextViewHelpers.kt
@@ -0,0 +1,63 @@
+/*
+ * Copyright (C) 2022 E FOUNDATION
+ *
+ * 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 <https://www.gnu.org/licenses/>.
+ */
+
+package foundation.e.advancedprivacy.common
+
+import android.content.Context
+import android.content.res.ColorStateList
+import android.text.Spannable
+import android.text.SpannableString
+import android.text.style.DynamicDrawableSpan
+import android.text.style.ImageSpan
+import android.widget.TextView
+import androidx.annotation.StringRes
+import androidx.appcompat.content.res.AppCompatResources
+import androidx.appcompat.widget.TooltipCompat
+import foundation.e.advancedprivacy.R
+
+fun setToolTipForAsterisk(
+ textView: TextView,
+ @StringRes textId: Int,
+ @StringRes tooltipTextId: Int
+) {
+ textView.text = asteriskAsInfoIconSpannable(textView.context, textId, textView.textColors)
+ TooltipCompat.setTooltipText(textView, textView.context.getString(tooltipTextId))
+
+ textView.setOnClickListener { it.performLongClick() }
+}
+
+private fun asteriskAsInfoIconSpannable(
+ context: Context,
+ @StringRes textId: Int,
+ tint: ColorStateList
+): Spannable {
+ val spannable = SpannableString(context.getString(textId))
+ val index = spannable.lastIndexOf("*")
+ if (index != -1) {
+ AppCompatResources.getDrawable(context, R.drawable.ic_info_16dp)?.let {
+ it.setTintList(tint)
+ it.setBounds(0, 0, it.intrinsicWidth, it.intrinsicHeight)
+ spannable.setSpan(
+ ImageSpan(it, DynamicDrawableSpan.ALIGN_CENTER),
+ index,
+ index + 1,
+ Spannable.SPAN_INCLUSIVE_INCLUSIVE
+ )
+ }
+ }
+ return spannable
+}
diff --git a/app/src/main/java/foundation/e/advancedprivacy/common/ThrottleFlow.kt b/app/src/main/java/foundation/e/advancedprivacy/common/ThrottleFlow.kt
new file mode 100644
index 0000000..e9ec060
--- /dev/null
+++ b/app/src/main/java/foundation/e/advancedprivacy/common/ThrottleFlow.kt
@@ -0,0 +1,36 @@
+/*
+ * Copyright (C) 2022 E FOUNDATION
+ *
+ * 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 <https://www.gnu.org/licenses/>.
+ */
+
+package foundation.e.advancedprivacy.common
+
+import kotlinx.coroutines.FlowPreview
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.flow
+import kotlin.time.Duration
+
+@FlowPreview
+fun <T> Flow<T>.throttleFirst(windowDuration: Duration): Flow<T> = flow {
+ var lastEmissionTime = 0L
+ collect { upstream ->
+ val currentTime = System.currentTimeMillis()
+ val mayEmit = currentTime - lastEmissionTime > windowDuration.inWholeMilliseconds
+ if (mayEmit) {
+ lastEmissionTime = currentTime
+ emit(upstream)
+ }
+ }
+}
diff --git a/app/src/main/java/foundation/e/advancedprivacy/common/ToggleAppsAdapter.kt b/app/src/main/java/foundation/e/advancedprivacy/common/ToggleAppsAdapter.kt
new file mode 100644
index 0000000..d8ee8ea
--- /dev/null
+++ b/app/src/main/java/foundation/e/advancedprivacy/common/ToggleAppsAdapter.kt
@@ -0,0 +1,76 @@
+/*
+ * Copyright (C) 2021 E FOUNDATION
+ *
+ * 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 <https://www.gnu.org/licenses/>.
+ */
+
+package foundation.e.advancedprivacy.common
+
+import android.view.LayoutInflater
+import android.view.View
+import android.view.ViewGroup
+import android.widget.CheckBox
+import android.widget.ImageView
+import android.widget.TextView
+import androidx.recyclerview.widget.RecyclerView
+import foundation.e.advancedprivacy.R
+import foundation.e.privacymodules.permissions.data.ApplicationDescription
+
+class ToggleAppsAdapter(
+ private val itemsLayout: Int,
+ private val listener: (String) -> Unit
+) :
+ RecyclerView.Adapter<ToggleAppsAdapter.ViewHolder>() {
+
+ class ViewHolder(view: View, private val listener: (String) -> Unit) : RecyclerView.ViewHolder(view) {
+ val appName: TextView = view.findViewById(R.id.title)
+
+ val togglePermission: CheckBox = view.findViewById(R.id.toggle)
+
+ fun bind(item: Pair<ApplicationDescription, Boolean>, isEnabled: Boolean) {
+ appName.text = item.first.label
+ togglePermission.isChecked = item.second
+ togglePermission.isEnabled = isEnabled
+
+ itemView.findViewById<ImageView>(R.id.icon).setImageDrawable(item.first.icon)
+ togglePermission.setOnClickListener { listener(item.first.packageName) }
+ }
+ }
+
+ var dataSet: List<Pair<ApplicationDescription, Boolean>> = emptyList()
+ set(value) {
+ field = value
+ notifyDataSetChanged()
+ }
+
+ var isEnabled: Boolean = true
+
+ fun setData(list: List<Pair<ApplicationDescription, Boolean>>, isEnabled: Boolean = true) {
+ this.isEnabled = isEnabled
+ dataSet = list
+ }
+
+ override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
+ val view = LayoutInflater.from(parent.context)
+ .inflate(itemsLayout, parent, false)
+ return ViewHolder(view, listener)
+ }
+
+ override fun onBindViewHolder(holder: ViewHolder, position: Int) {
+ val permission = dataSet[position]
+ holder.bind(permission, isEnabled)
+ }
+
+ override fun getItemCount(): Int = dataSet.size
+}
diff --git a/app/src/main/java/foundation/e/advancedprivacy/common/ToolbarFragment.kt b/app/src/main/java/foundation/e/advancedprivacy/common/ToolbarFragment.kt
new file mode 100644
index 0000000..fb3ea14
--- /dev/null
+++ b/app/src/main/java/foundation/e/advancedprivacy/common/ToolbarFragment.kt
@@ -0,0 +1,45 @@
+/*
+ * Copyright (C) 2021 E FOUNDATION
+ *
+ * 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 <https://www.gnu.org/licenses/>.
+ */
+
+package foundation.e.advancedprivacy.common
+
+import android.os.Bundle
+import android.view.View
+import androidx.annotation.LayoutRes
+import androidx.fragment.app.Fragment
+import com.google.android.material.appbar.MaterialToolbar
+import foundation.e.advancedprivacy.R
+
+abstract class ToolbarFragment(@LayoutRes contentLayoutId: Int) : Fragment(contentLayoutId) {
+
+ /**
+ * @return title to be used in toolbar
+ */
+ abstract fun getTitle(): String
+
+ override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
+ super.onViewCreated(view, savedInstanceState)
+
+ setupToolbar(view.findViewById(R.id.toolbar))
+ }
+
+ open fun setupToolbar(toolbar: MaterialToolbar) {
+ toolbar.title = getTitle()
+ }
+
+ fun getToolbar(): MaterialToolbar? = view?.findViewById(R.id.toolbar)
+}
diff --git a/app/src/main/java/foundation/e/advancedprivacy/common/WarningDialog.kt b/app/src/main/java/foundation/e/advancedprivacy/common/WarningDialog.kt
new file mode 100644
index 0000000..98deeb1
--- /dev/null
+++ b/app/src/main/java/foundation/e/advancedprivacy/common/WarningDialog.kt
@@ -0,0 +1,130 @@
+/*
+ * Copyright (C) 2022 E FOUNDATION
+ *
+ * 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 <https://www.gnu.org/licenses/>.
+ */
+
+package foundation.e.advancedprivacy.common
+
+import android.app.Activity
+import android.content.Context
+import android.content.Intent
+import android.graphics.drawable.ColorDrawable
+import android.os.Bundle
+import android.util.Log
+import android.view.View
+import android.widget.CheckBox
+import androidx.appcompat.app.AlertDialog
+import foundation.e.advancedprivacy.AdvancedPrivacyApplication
+import foundation.e.advancedprivacy.R
+import foundation.e.advancedprivacy.domain.entities.MainFeatures
+import foundation.e.advancedprivacy.domain.entities.MainFeatures.FAKE_LOCATION
+import foundation.e.advancedprivacy.domain.entities.MainFeatures.IP_SCRAMBLING
+import foundation.e.advancedprivacy.domain.entities.MainFeatures.TRACKERS_CONTROL
+import foundation.e.advancedprivacy.domain.usecases.ShowFeaturesWarningUseCase
+import foundation.e.advancedprivacy.main.MainActivity
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.flow.launchIn
+import kotlinx.coroutines.flow.map
+
+class WarningDialog : Activity() {
+ companion object {
+ private const val PARAM_FEATURE = "feature"
+
+ fun startListening(
+ showFeaturesWarningUseCase: ShowFeaturesWarningUseCase,
+ appScope: CoroutineScope,
+ appContext: Context
+ ) {
+ showFeaturesWarningUseCase.showWarning().map { feature ->
+ appContext.startActivity(
+ createIntent(context = appContext, feature = feature)
+ )
+ }.launchIn(appScope)
+ }
+
+ private fun createIntent(
+ context: Context,
+ feature: MainFeatures,
+ ): Intent {
+ val intent = Intent(context, WarningDialog::class.java)
+ intent.putExtra(PARAM_FEATURE, feature.name)
+ intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK
+ return intent
+ }
+ }
+
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+ getWindow().setBackgroundDrawable(ColorDrawable(0))
+
+ val feature = try {
+ MainFeatures.valueOf(intent.getStringExtra(PARAM_FEATURE) ?: "")
+ } catch (e: Exception) {
+ Log.e("WarningDialog", "Missing mandatory activity parameter", e)
+ finish()
+ return
+ }
+
+ showWarningDialog(feature)
+ }
+
+ private fun showWarningDialog(feature: MainFeatures) {
+ val builder = AlertDialog.Builder(this)
+ builder.setOnDismissListener { finish() }
+
+ val content: View = layoutInflater.inflate(R.layout.alertdialog_do_not_show_again, null)
+ val checkbox = content.findViewById<CheckBox>(R.id.checkbox)
+ builder.setView(content)
+
+ builder.setMessage(
+ when (feature) {
+ TRACKERS_CONTROL -> R.string.warningdialog_trackers_message
+ FAKE_LOCATION -> R.string.warningdialog_location_message
+ IP_SCRAMBLING -> R.string.warningdialog_ipscrambling_message
+ }
+ )
+
+ builder.setTitle(
+ when (feature) {
+ TRACKERS_CONTROL -> R.string.warningdialog_trackers_title
+ FAKE_LOCATION -> R.string.warningdialog_location_title
+ IP_SCRAMBLING -> R.string.warningdialog_ipscrambling_title
+ }
+ )
+
+ builder.setPositiveButton(
+ when (feature) {
+ IP_SCRAMBLING -> R.string.warningdialog_ipscrambling_cta
+ else -> R.string.ok
+ }
+ ) { _, _ ->
+ if (checkbox.isChecked()) {
+ (application as AdvancedPrivacyApplication)
+ .dependencyContainer.showFeaturesWarningUseCase
+ .doNotShowAgain(feature)
+ }
+ finish()
+ }
+
+ if (feature == TRACKERS_CONTROL) {
+ builder.setNeutralButton(R.string.warningdialog_trackers_secondary_cta) { _, _ ->
+ startActivity(MainActivity.createTrackersIntent(this))
+ finish()
+ }
+ }
+
+ builder.show()
+ }
+}
diff --git a/app/src/main/java/foundation/e/advancedprivacy/common/extensions/AnyExtension.kt b/app/src/main/java/foundation/e/advancedprivacy/common/extensions/AnyExtension.kt
new file mode 100644
index 0000000..652aefd
--- /dev/null
+++ b/app/src/main/java/foundation/e/advancedprivacy/common/extensions/AnyExtension.kt
@@ -0,0 +1,22 @@
+/*
+ * Copyright (C) 2022 E FOUNDATION
+ *
+ * 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 <https://www.gnu.org/licenses/>.
+ */
+
+package foundation.e.advancedprivacy.common.extensions
+
+import android.content.Context
+
+fun Int.dpToPxF(context: Context): Float = this.toFloat() * context.resources.displayMetrics.density