summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--app/src/main/java/foundation/e/privacycentralapp/dummy/DummyDataSource.kt68
-rw-r--r--app/src/main/java/foundation/e/privacycentralapp/dummy/Extensions.kt29
-rw-r--r--app/src/main/java/foundation/e/privacycentralapp/features/dashboard/DashboardFeature.kt186
-rw-r--r--app/src/main/java/foundation/e/privacycentralapp/features/dashboard/DashboardFragment.kt110
-rw-r--r--app/src/main/java/foundation/e/privacycentralapp/features/dashboard/DashboardViewModel.kt8
-rw-r--r--app/src/main/java/foundation/e/privacycentralapp/features/internetprivacy/InternetPrivacyFeature.kt110
-rw-r--r--app/src/main/java/foundation/e/privacycentralapp/features/internetprivacy/InternetPrivacyFragment.kt80
-rw-r--r--app/src/main/java/foundation/e/privacycentralapp/features/internetprivacy/InternetPrivacyViewModel.kt40
-rw-r--r--app/src/main/java/foundation/e/privacycentralapp/features/location/FakeLocationFeature.kt152
-rw-r--r--app/src/main/java/foundation/e/privacycentralapp/features/location/FakeLocationFragment.kt176
-rw-r--r--app/src/main/java/foundation/e/privacycentralapp/features/location/FakeLocationViewModel.kt (renamed from app/src/main/java/foundation/e/privacycentralapp/features/location/FakeMyLocationFragment.kt)30
-rw-r--r--app/src/main/java/foundation/e/privacycentralapp/main/MainActivity.kt4
-rw-r--r--app/src/main/res/drawable/ic_my_location.xml22
-rw-r--r--app/src/main/res/layout/fragment_dashboard.xml412
-rw-r--r--app/src/main/res/layout/fragment_fake_location.xml1
-rw-r--r--app/src/main/res/layout/fragment_internet_activity_policy.xml4
-rw-r--r--app/src/main/res/values/strings.xml8
-rw-r--r--build.gradle1
-rw-r--r--flow-mvi/src/main/java/foundation/e/flowmvi/feature/BaseFeature.kt20
19 files changed, 1174 insertions, 287 deletions
diff --git a/app/src/main/java/foundation/e/privacycentralapp/dummy/DummyDataSource.kt b/app/src/main/java/foundation/e/privacycentralapp/dummy/DummyDataSource.kt
index 65d072a..3f2dc1e 100644
--- a/app/src/main/java/foundation/e/privacycentralapp/dummy/DummyDataSource.kt
+++ b/app/src/main/java/foundation/e/privacycentralapp/dummy/DummyDataSource.kt
@@ -18,17 +18,21 @@
package foundation.e.privacycentralapp.dummy
import foundation.e.privacycentralapp.R
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.asStateFlow
+import kotlin.random.Random
// ======================================================//
//
-// ============ ==== ==== ============
-// ============ ===== ===== ==== ====
-// ==== ====== ====== ==== ====
-// ==== ======= ======= ============
-// ==== ================ ====
-// ==== ==== ====== ==== ====
-// ============ ==== ==== ==== ====
-// ============ ==== == ==== ====
+// ================ ==== ==== ===============
+// ================ ====== ====== ================
+// ==== ======== ======== ==== ====
+// ==== ========= ========= ==== ====
+// ==== ==================== ================
+// ==== ==== ======== ==== ===============
+// ==== ==== ==== ==== ====
+// ================ ==== == ==== ====
+// ================ ==== ==== ====
//
// ======================================================//
@@ -47,7 +51,30 @@ data class Permission(
val packagesAllowed: List<String> = emptyList()
)
+enum class LocationMode {
+ REAL_LOCATION, RANDOM_LOCATION, CUSTOM_LOCATION
+}
+
+enum class InternetPrivacyMode {
+ REAL_IP, HIDE_IP
+}
+
+data class Location(val mode: LocationMode, val latitude: Double, val longitude: Double)
+
object DummyDataSource {
+ private val _appsUsingLocationPerm = MutableStateFlow<List<String>>(emptyList())
+ val appsUsingLocationPerm = _appsUsingLocationPerm.asStateFlow()
+
+ const val trackersCount = 77
+ private val _activeTrackersCount = MutableStateFlow(10)
+ val activeTrackersCount = _activeTrackersCount.asStateFlow()
+
+ private val _location = MutableStateFlow(Location(LocationMode.REAL_LOCATION, 0.0, 0.0))
+ val location = _location.asStateFlow()
+
+ private val _internetActivityMode = MutableStateFlow(InternetPrivacyMode.REAL_IP)
+ val internetActivityMode = _internetActivityMode.asStateFlow()
+
val permissions = arrayOf("Body Sensor", "Calendar", "Call Logs", "Location")
val icons = arrayOf(
R.drawable.ic_body_monitor,
@@ -138,4 +165,29 @@ object DummyDataSource {
fun getPermission(permissionId: Int): Permission {
return populatedPermission.get(permissionId)
}
+
+ fun setLocationMode(locationMode: LocationMode, location: Location? = null): Boolean {
+ when (locationMode) {
+ LocationMode.REAL_LOCATION ->
+ _location.value =
+ Location(LocationMode.REAL_LOCATION, 24.39, 71.80)
+ LocationMode.RANDOM_LOCATION -> _location.value = randomLocation()
+ LocationMode.CUSTOM_LOCATION -> {
+ requireNotNull(location) { "Custom location should be null" }
+ _location.value = location.copy(mode = LocationMode.CUSTOM_LOCATION)
+ }
+ }
+ return true
+ }
+
+ private fun randomLocation(): Location = Location(
+ LocationMode.RANDOM_LOCATION,
+ Random.nextDouble(-90.0, 90.0),
+ Random.nextDouble(-180.0, 180.0)
+ )
+
+ fun setInternetPrivacyMode(mode: InternetPrivacyMode): Boolean {
+ _internetActivityMode.value = mode
+ return true
+ }
}
diff --git a/app/src/main/java/foundation/e/privacycentralapp/dummy/Extensions.kt b/app/src/main/java/foundation/e/privacycentralapp/dummy/Extensions.kt
new file mode 100644
index 0000000..c872012
--- /dev/null
+++ b/app/src/main/java/foundation/e/privacycentralapp/dummy/Extensions.kt
@@ -0,0 +1,29 @@
+/*
+ * 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.privacycentralapp.dummy
+
+fun LocationMode.mapToString(): String = when (this) {
+ LocationMode.REAL_LOCATION -> "Real location mode"
+ LocationMode.RANDOM_LOCATION -> "Random location mode"
+ LocationMode.CUSTOM_LOCATION -> "Fake location mode"
+}
+
+fun InternetPrivacyMode.mapToString(): String = when (this) {
+ InternetPrivacyMode.REAL_IP -> "I'm exposing my real IP address"
+ InternetPrivacyMode.HIDE_IP -> "I'm anonymous on the internet"
+}
diff --git a/app/src/main/java/foundation/e/privacycentralapp/features/dashboard/DashboardFeature.kt b/app/src/main/java/foundation/e/privacycentralapp/features/dashboard/DashboardFeature.kt
index ecadea1..dd4f0ff 100644
--- a/app/src/main/java/foundation/e/privacycentralapp/features/dashboard/DashboardFeature.kt
+++ b/app/src/main/java/foundation/e/privacycentralapp/features/dashboard/DashboardFeature.kt
@@ -17,59 +17,189 @@
package foundation.e.privacycentralapp.features.dashboard
+import android.util.Log
import foundation.e.flowmvi.Actor
import foundation.e.flowmvi.Reducer
+import foundation.e.flowmvi.SingleEventProducer
import foundation.e.flowmvi.feature.BaseFeature
+import foundation.e.privacycentralapp.dummy.DummyDataSource
+import foundation.e.privacycentralapp.dummy.InternetPrivacyMode
+import foundation.e.privacycentralapp.dummy.LocationMode
import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.flow.flow
import kotlinx.coroutines.flow.flowOf
+import kotlinx.coroutines.flow.map
+import kotlinx.coroutines.flow.merge
// Define a state machine for Dashboard Feature
-object DashboardFeature {
+class DashboardFeature(
+ initialState: State,
+ coroutineScope: CoroutineScope,
+ reducer: Reducer<State, Effect>,
+ actor: Actor<State, Action, Effect>,
+ singleEventProducer: SingleEventProducer<State, Action, Effect, SingleEvent>
+) : BaseFeature<DashboardFeature.State,
+ DashboardFeature.Action,
+ DashboardFeature.Effect,
+ DashboardFeature.SingleEvent>(
+ initialState, actor, reducer, coroutineScope, { message -> Log.d("DashboardFeature", message) },
+ singleEventProducer
+) {
sealed class State {
- object DashboardState : State()
+ object InitialState : State()
+ object LoadingDashboardState : State()
+ data class DashboardState(
+ val trackersCount: Int,
+ val activeTrackersCount: Int,
+ val totalApps: Int,
+ val permissionCount: Int,
+ val appsUsingLocationPerm: Int,
+ val locationMode: LocationMode,
+ val internetPrivacyMode: InternetPrivacyMode
+ ) : State()
+
object QuickProtectionState : State()
}
sealed class SingleEvent {
object NavigateToQuickProtectionSingleEvent : SingleEvent()
object NavigateToTrackersSingleEvent : SingleEvent()
- object NavigateToInternetActivityPolicySingleEvent : SingleEvent()
+ object NavigateToInternetActivityPrivacySingleEvent : SingleEvent()
object NavigateToLocationSingleEvent : SingleEvent()
- object NavigateToPermissionManagementSingleEvent : SingleEvent()
+ object NavigateToPermissionsSingleEvent : SingleEvent()
}
sealed class Action {
object ShowQuickPrivacyProtectionInfoAction : Action()
+ object ObserveDashboardAction : Action()
object ShowDashboardAction : Action()
+ object ShowFakeMyLocationAction : Action()
+ object ShowInternetActivityPrivacyAction : Action()
+ object ShowAppsPermissions : Action()
}
sealed class Effect {
object OpenQuickPrivacyProtectionEffect : Effect()
- object OpenDashboardEffect : Effect()
- }
-}
+ data class OpenDashboardEffect(
+ val trackersCount: Int,
+ val activeTrackersCount: Int,
+ val totalApps: Int,
+ val permissionCount: Int,
+ val appsUsingLocationPerm: Int,
+ val locationMode: LocationMode,
+ val internetPrivacyMode: InternetPrivacyMode
+ ) : Effect()
-private val reducer: Reducer<DashboardFeature.State, DashboardFeature.Effect> = { _, effect ->
- when (effect) {
- DashboardFeature.Effect.OpenQuickPrivacyProtectionEffect -> DashboardFeature.State.QuickProtectionState
- DashboardFeature.Effect.OpenDashboardEffect -> DashboardFeature.State.DashboardState
+ object LoadingDashboardEffect : Effect()
+ data class UpdateActiveTrackersCountEffect(val count: Int) : Effect()
+ data class UpdateLocationModeEffect(val mode: LocationMode) : Effect()
+ data class UpdateInternetActivityModeEffect(val mode: InternetPrivacyMode) : Effect()
+ data class UpdateAppsUsingLocationPermEffect(val apps: Int) : Effect()
+ object OpenFakeMyLocationEffect : Effect()
+ object OpenInternetActivityPrivacyEffect : Effect()
+ object OpenAppsPermissionsEffect : Effect()
}
-}
-private val actor: Actor<DashboardFeature.State, DashboardFeature.Action, DashboardFeature.Effect> =
- { _, action ->
- when (action) {
- DashboardFeature.Action.ShowQuickPrivacyProtectionInfoAction -> flowOf(DashboardFeature.Effect.OpenQuickPrivacyProtectionEffect)
- DashboardFeature.Action.ShowDashboardAction -> flowOf(DashboardFeature.Effect.OpenDashboardEffect)
- }
- }
+ companion object {
+ fun create(initialState: State, coroutineScope: CoroutineScope): DashboardFeature =
+ DashboardFeature(
+ initialState,
+ coroutineScope,
+ reducer = { state, effect ->
+ when (effect) {
+ Effect.OpenQuickPrivacyProtectionEffect -> State.QuickProtectionState
+ is Effect.OpenDashboardEffect -> State.DashboardState(
+ effect.trackersCount,
+ effect.activeTrackersCount,
+ effect.totalApps,
+ effect.permissionCount,
+ effect.appsUsingLocationPerm,
+ effect.locationMode,
+ effect.internetPrivacyMode
+ )
+ Effect.LoadingDashboardEffect -> {
+ if (state is State.InitialState) {
+ State.LoadingDashboardState
+ } else state
+ }
+ is Effect.UpdateActiveTrackersCountEffect -> {
+ if (state is State.DashboardState) {
+ state.copy(activeTrackersCount = effect.count)
+ } else state
+ }
+ is Effect.UpdateInternetActivityModeEffect -> {
+ if (state is State.DashboardState) {
+ state.copy(internetPrivacyMode = effect.mode)
+ } else state
+ }
+ is Effect.UpdateLocationModeEffect -> {
+ if (state is State.DashboardState) {
+ state.copy(locationMode = effect.mode)
+ } else state
+ }
+ is Effect.UpdateAppsUsingLocationPermEffect -> if (state is State.DashboardState) {
+ state.copy(appsUsingLocationPerm = effect.apps)
+ } else state
-fun homeFeature(
- initialState: DashboardFeature.State = DashboardFeature.State.DashboardState,
- coroutineScope: CoroutineScope
-) = BaseFeature<DashboardFeature.State, DashboardFeature.Action, DashboardFeature.Effect, DashboardFeature.SingleEvent>(
- initialState,
- actor,
- reducer,
- coroutineScope
-)
+ Effect.OpenFakeMyLocationEffect -> state
+ Effect.OpenAppsPermissionsEffect -> state
+ Effect.OpenInternetActivityPrivacyEffect -> state
+ }
+ },
+ actor = { _: State, action: Action ->
+ Log.d("Feature", "action: $action")
+ when (action) {
+ Action.ObserveDashboardAction -> merge(
+ DummyDataSource.activeTrackersCount.map {
+ Effect.UpdateActiveTrackersCountEffect(it)
+ },
+ DummyDataSource.appsUsingLocationPerm.map {
+ Effect.UpdateAppsUsingLocationPermEffect(it.size)
+ },
+ DummyDataSource.location.map {
+ Effect.UpdateLocationModeEffect(it.mode)
+ },
+ DummyDataSource.internetActivityMode.map {
+ Effect.UpdateInternetActivityModeEffect(it)
+ }
+ )
+ Action.ShowQuickPrivacyProtectionInfoAction -> flowOf(
+ Effect.OpenQuickPrivacyProtectionEffect
+ )
+ Action.ShowDashboardAction -> flow {
+ emit(Effect.LoadingDashboardEffect)
+ kotlinx.coroutines.delay(2000)
+ emit(
+ Effect.OpenDashboardEffect(
+ DummyDataSource.trackersCount,
+ DummyDataSource.activeTrackersCount.value,
+ DummyDataSource.packages.size,
+ DummyDataSource.permissions.size,
+ DummyDataSource.appsUsingLocationPerm.value.size,
+ DummyDataSource.location.value.mode,
+ DummyDataSource.internetActivityMode.value
+ )
+ )
+ }
+ Action.ShowFakeMyLocationAction -> flowOf(Effect.OpenFakeMyLocationEffect)
+ Action.ShowAppsPermissions -> flowOf(Effect.OpenAppsPermissionsEffect)
+ Action.ShowInternetActivityPrivacyAction -> flowOf(
+ Effect.OpenInternetActivityPrivacyEffect
+ )
+ }
+ },
+ singleEventProducer = { state, _, effect ->
+ Log.d("DashboardFeature", "$state, $effect")
+ if (state is State.DashboardState && effect is Effect.OpenFakeMyLocationEffect)
+ SingleEvent.NavigateToLocationSingleEvent
+ else if (state is State.QuickProtectionState && effect is Effect.OpenQuickPrivacyProtectionEffect)
+ SingleEvent.NavigateToQuickProtectionSingleEvent
+ else if (state is State.DashboardState && effect is Effect.OpenInternetActivityPrivacyEffect)
+ SingleEvent.NavigateToInternetActivityPrivacySingleEvent
+ else if (state is State.DashboardState && effect is Effect.OpenAppsPermissionsEffect)
+ SingleEvent.NavigateToPermissionsSingleEvent
+ else null
+ }
+ )
+ }
+}
diff --git a/app/src/main/java/foundation/e/privacycentralapp/features/dashboard/DashboardFragment.kt b/app/src/main/java/foundation/e/privacycentralapp/features/dashboard/DashboardFragment.kt
index d6a91b8..b9371be 100644
--- a/app/src/main/java/foundation/e/privacycentralapp/features/dashboard/DashboardFragment.kt
+++ b/app/src/main/java/foundation/e/privacycentralapp/features/dashboard/DashboardFragment.kt
@@ -23,8 +23,11 @@ import android.text.Spannable
import android.text.SpannableString
import android.text.style.ForegroundColorSpan
import android.view.View
+import android.widget.ProgressBar
+import android.widget.RelativeLayout
import android.widget.TextView
import android.widget.Toolbar
+import androidx.core.widget.NestedScrollView
import androidx.fragment.app.Fragment
import androidx.fragment.app.activityViewModels
import androidx.fragment.app.add
@@ -32,7 +35,12 @@ import androidx.fragment.app.commit
import androidx.lifecycle.lifecycleScope
import foundation.e.flowmvi.MVIView
import foundation.e.privacycentralapp.R
+import foundation.e.privacycentralapp.dummy.mapToString
+import foundation.e.privacycentralapp.features.internetprivacy.InternetPrivacyFragment
+import foundation.e.privacycentralapp.features.location.FakeLocationFragment
+import foundation.e.privacycentralapp.features.permissions.PermissionsFragment
import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.collect
class DashboardFragment :
Fragment(R.layout.fragment_dashboard),
@@ -43,7 +51,40 @@ class DashboardFragment :
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
lifecycleScope.launchWhenStarted {
- viewModel.homeFeature.takeView(this, this@DashboardFragment)
+ viewModel.dashboardFeature.takeView(this, this@DashboardFragment)
+ }
+ lifecycleScope.launchWhenStarted {
+ viewModel.dashboardFeature.singleEvents.collect { event ->
+ if (event is DashboardFeature.SingleEvent.NavigateToLocationSingleEvent) {
+ requireActivity().supportFragmentManager.commit {
+ add<FakeLocationFragment>(R.id.container)
+ setReorderingAllowed(true)
+ addToBackStack("dashboard")
+ }
+ } else if (event is DashboardFeature.SingleEvent.NavigateToQuickProtectionSingleEvent) {
+ requireActivity().supportFragmentManager.commit {
+ add<QuickProtectionFragment>(R.id.container)
+ setReorderingAllowed(true)
+ addToBackStack("dashboard")
+ }
+ } else if (event is DashboardFeature.SingleEvent.NavigateToInternetActivityPrivacySingleEvent) {
+ requireActivity().supportFragmentManager.commit {
+ add<InternetPrivacyFragment>(R.id.container)
+ setReorderingAllowed(true)
+ addToBackStack("dashboard")
+ }
+ } else if (event is DashboardFeature.SingleEvent.NavigateToPermissionsSingleEvent) {
+ requireActivity().supportFragmentManager.commit {
+ add<PermissionsFragment>(R.id.container)
+ setReorderingAllowed(true)
+ addToBackStack("dashboard")
+ }
+ }
+ }
+ }
+ lifecycleScope.launchWhenStarted {
+ viewModel.submitAction(DashboardFeature.Action.ShowDashboardAction)
+ viewModel.submitAction(DashboardFeature.Action.ObserveDashboardAction)
}
}
@@ -56,6 +97,15 @@ class DashboardFragment :
it.findViewById<TextView>(R.id.tap_to_enable_quick_protection).setOnClickListener {
viewModel.submitAction(DashboardFeature.Action.ShowQuickPrivacyProtectionInfoAction)
}
+ it.findViewById<RelativeLayout>(R.id.my_location).setOnClickListener {
+ viewModel.submitAction(DashboardFeature.Action.ShowFakeMyLocationAction)
+ }
+ it.findViewById<RelativeLayout>(R.id.internet_activity_privacy).setOnClickListener {
+ viewModel.submitAction(DashboardFeature.Action.ShowInternetActivityPrivacyAction)
+ }
+ it.findViewById<RelativeLayout>(R.id.apps_permissions).setOnClickListener {
+ viewModel.submitAction(DashboardFeature.Action.ShowAppsPermissions)
+ }
}
}
@@ -78,15 +128,59 @@ class DashboardFragment :
override fun render(state: DashboardFeature.State) {
when (state) {
- is DashboardFeature.State.QuickProtectionState -> {
- requireActivity().supportFragmentManager.commit {
- add<QuickProtectionFragment>(R.id.container)
- setReorderingAllowed(true)
- addToBackStack("dashboard")
+ is DashboardFeature.State.InitialState, is DashboardFeature.State.LoadingDashboardState -> {
+ view?.let {
+ it.findViewById<ProgressBar>(R.id.loadingSpinner).visibility = View.VISIBLE
+ it.findViewById<NestedScrollView>(R.id.scrollContainer).visibility = View.GONE
}
}
- else -> {
- // TODO: any remaining state must either be handled or needs to be passed down to the UI.
+ is DashboardFeature.State.DashboardState -> {
+ view?.let { view ->
+ view.findViewById<ProgressBar>(R.id.loadingSpinner).visibility = View.GONE
+ view.findViewById<NestedScrollView>(R.id.scrollContainer).visibility =
+ View.VISIBLE
+ view.findViewById<TextView>(R.id.am_i_tracked_subtitle).text = getString(
+ R.string.am_i_tracked_subtitle,
+ state.trackersCount,
+ state.activeTrackersCount
+ )
+ view.findViewById<TextView>(R.id.apps_permissions_subtitle).text = getString(
+ R.string.apps_permissions_subtitle,
+ state.totalApps,
+ state.permissionCount
+ )
+ view.findViewById<TextView>(R.id.my_location_subtitle).let {
+ it.text = getString(
+ R.string.my_location_subtitle,
+ state.appsUsingLocationPerm,
+ )
+ it.append(
+ SpannableString(state.locationMode.mapToString())
+ .also {
+ it.setSpan(
+ ForegroundColorSpan(Color.parseColor("#007fff")),
+ 0,
+ it.length,
+ Spannable.SPAN_EXCLUSIVE_EXCLUSIVE
+ )
+ }
+ )
+ }
+ view.findViewById<TextView>(R.id.internet_activity_privacy_subtitle).let {
+ it.text = getString(R.string.internet_activity_privacy_subtitle)
+ it.append(
+ SpannableString(state.internetPrivacyMode.mapToString())
+ .also {
+ it.setSpan(
+ ForegroundColorSpan(Color.parseColor("#007fff")),
+ 0,
+ it.length,
+ Spannable.SPAN_EXCLUSIVE_EXCLUSIVE
+ )
+ }
+ )
+ }
+ }
}
}
}
diff --git a/app/src/main/java/foundation/e/privacycentralapp/features/dashboard/DashboardViewModel.kt b/app/src/main/java/foundation/e/privacycentralapp/features/dashboard/DashboardViewModel.kt
index 12696d5..9428f41 100644
--- a/app/src/main/java/foundation/e/privacycentralapp/features/dashboard/DashboardViewModel.kt
+++ b/app/src/main/java/foundation/e/privacycentralapp/features/dashboard/DashboardViewModel.kt
@@ -17,9 +17,9 @@
package foundation.e.privacycentralapp.features.dashboard
+import android.util.Log
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
-import foundation.e.flowmvi.feature.BaseFeature
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.asSharedFlow
import kotlinx.coroutines.launch
@@ -29,12 +29,12 @@ class DashboardViewModel : ViewModel() {
private val _actions = MutableSharedFlow<DashboardFeature.Action>()
val actions = _actions.asSharedFlow()
- val homeFeature: BaseFeature<DashboardFeature.State, DashboardFeature.Action,
- DashboardFeature.Effect, DashboardFeature.SingleEvent> by lazy {
- homeFeature(coroutineScope = viewModelScope)
+ val dashboardFeature: DashboardFeature by lazy {
+ DashboardFeature.create(DashboardFeature.State.InitialState, coroutineScope = viewModelScope)
}
fun submitAction(action: DashboardFeature.Action) {
+ Log.d("DashboardViewModel", "submitAction() called with: action = $action")
viewModelScope.launch {
_actions.emit(action)
}
diff --git a/app/src/main/java/foundation/e/privacycentralapp/features/internetprivacy/InternetPrivacyFeature.kt b/app/src/main/java/foundation/e/privacycentralapp/features/internetprivacy/InternetPrivacyFeature.kt
new file mode 100644
index 0000000..66e4add
--- /dev/null
+++ b/app/src/main/java/foundation/e/privacycentralapp/features/internetprivacy/InternetPrivacyFeature.kt
@@ -0,0 +1,110 @@
+/*
+ * 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.privacycentralapp.features.internetprivacy
+
+import android.util.Log
+import foundation.e.flowmvi.Actor
+import foundation.e.flowmvi.Reducer
+import foundation.e.flowmvi.SingleEventProducer
+import foundation.e.flowmvi.feature.BaseFeature
+import foundation.e.privacycentralapp.dummy.DummyDataSource
+import foundation.e.privacycentralapp.dummy.InternetPrivacyMode
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.flow.flow
+import kotlinx.coroutines.flow.flowOf
+
+// Define a state machine for Fake location feature
+class InternetPrivacyFeature(
+ initialState: State,
+ coroutineScope: CoroutineScope,
+ reducer: Reducer<State, Effect>,
+ actor: Actor<State, Action, Effect>,
+ singleEventProducer: SingleEventProducer<State, Action, Effect, SingleEvent>
+) : BaseFeature<InternetPrivacyFeature.State, InternetPrivacyFeature.Action, InternetPrivacyFeature.Effect, InternetPrivacyFeature.SingleEvent>(
+ initialState,
+ actor,
+ reducer,
+ coroutineScope,
+ { message -> Log.d("FakeLocationFeature", message) },
+ singleEventProducer
+) {
+ data class State(val mode: InternetPrivacyMode)
+
+ sealed class SingleEvent {
+ object RealIPSelectedEvent : SingleEvent()
+ object HiddenIPSelectedEvent : SingleEvent()
+ data class ErrorEvent(val error: String) : SingleEvent()
+ }
+
+ sealed class Action {
+ object LoadInternetModeAction : Action()
+ object UseRealIPAction : Action()
+ object UseHiddenIPAction : Action()
+ }
+
+ sealed class Effect {
+ data class ModeUpdatedEffect(val mode: InternetPrivacyMode) : Effect()
+ data class ErrorEffect(val message: String) : Effect()
+ }
+
+ companion object {
+ fun create(
+ initialState: State = State(InternetPrivacyMode.REAL_IP),
+ coroutineScope: CoroutineScope
+ ) = InternetPrivacyFeature(
+ initialState, coroutineScope,
+ reducer = { state, effect ->
+ when (effect) {
+ is Effect.ModeUpdatedEffect -> state.copy(mode = effect.mode)
+ is Effect.ErrorEffect -> state
+ }
+ },
+ actor = { _, action ->
+ when (action) {
+ Action.LoadInternetModeAction -> flowOf(Effect.ModeUpdatedEffect(DummyDataSource.internetActivityMode.value))
+ Action.UseHiddenIPAction, Action.UseRealIPAction -> flow {
+ val success =
+ DummyDataSource.setInternetPrivacyMode(if (action is Action.UseHiddenIPAction) InternetPrivacyMode.HIDE_IP else InternetPrivacyMode.REAL_IP)
+ emit(
+ if (success) Effect.ModeUpdatedEffect(DummyDataSource.internetActivityMode.value) else Effect.ErrorEffect(
+ "Couldn't update internet mode"
+ )
+ )
+ }
+ }
+ },
+ singleEventProducer = { _, action, effect ->
+ when (action) {
+ Action.UseRealIPAction, Action.UseHiddenIPAction -> when (effect) {
+ is Effect.ModeUpdatedEffect -> {
+ if (effect.mode == InternetPrivacyMode.REAL_IP) {
+ SingleEvent.RealIPSelectedEvent
+ } else {
+ SingleEvent.HiddenIPSelectedEvent
+ }
+ }
+ is Effect.ErrorEffect -> {
+ SingleEvent.ErrorEvent(effect.message)
+ }
+ }
+ else -> null
+ }
+ }
+ )
+ }
+}
diff --git a/app/src/main/java/foundation/e/privacycentralapp/features/internetprivacy/InternetPrivacyFragment.kt b/app/src/main/java/foundation/e/privacycentralapp/features/internetprivacy/InternetPrivacyFragment.kt
index ddba807..a8c1671 100644
--- a/app/src/main/java/foundation/e/privacycentralapp/features/internetprivacy/InternetPrivacyFragment.kt
+++ b/app/src/main/java/foundation/e/privacycentralapp/features/internetprivacy/InternetPrivacyFragment.kt
@@ -19,15 +19,53 @@ package foundation.e.privacycentralapp.features.internetprivacy
import android.os.Bundle
import android.view.View
+import android.widget.RadioButton
+import android.widget.Toast
import android.widget.Toolbar
import androidx.fragment.app.Fragment
+import androidx.fragment.app.viewModels
+import androidx.lifecycle.lifecycleScope
+import foundation.e.flowmvi.MVIView
import foundation.e.privacycentralapp.R
+import foundation.e.privacycentralapp.dummy.InternetPrivacyMode
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.collect
+
+class InternetPrivacyFragment :
+ Fragment(R.layout.fragment_internet_activity_policy),
+ MVIView<InternetPrivacyFeature.State, InternetPrivacyFeature.Action> {
+
+ private val viewModel: InternetPrivacyViewModel by viewModels()
+
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+ lifecycleScope.launchWhenStarted {
+ viewModel.internetPrivacyFeature.takeView(this, this@InternetPrivacyFragment)
+ }
+ lifecycleScope.launchWhenStarted {
+ viewModel.internetPrivacyFeature.singleEvents.collect { event ->
+ when (event) {
+ is InternetPrivacyFeature.SingleEvent.ErrorEvent -> displayToast(event.error)
+ InternetPrivacyFeature.SingleEvent.HiddenIPSelectedEvent -> displayToast("Your IP is hidden")
+ InternetPrivacyFeature.SingleEvent.RealIPSelectedEvent -> displayToast("Your IP is visible to internet")
+ }
+ }
+ }
+ lifecycleScope.launchWhenStarted {
+ viewModel.submitAction(InternetPrivacyFeature.Action.LoadInternetModeAction)
+ }
+ }
+
+ private fun displayToast(message: String) {
+ Toast.makeText(requireContext(), message, Toast.LENGTH_SHORT)
+ .show()
+ }
-class InternetPrivacyFragment : Fragment(R.layout.fragment_internet_activity_policy) {
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
val toolbar = view.findViewById<Toolbar>(R.id.toolbar)
setupToolbar(toolbar)
+ bindClickListeners(view)
}
private fun setupToolbar(toolbar: Toolbar) {
@@ -35,4 +73,44 @@ class InternetPrivacyFragment : Fragment(R.layout.fragment_internet_activity_pol
activity.setActionBar(toolbar)
activity.title = "My Internet Activity Privacy"
}
+
+ private fun bindClickListeners(fragmentView: View) {
+ fragmentView.let {
+ it.findViewById<RadioButton>(R.id.radio_use_real_ip)
+ .setOnClickListener { radioButton ->
+ toggleIP(radioButton)
+ }
+ it.findViewById<RadioButton>(R.id.radio_use_hidden_ip)
+ .setOnClickListener { radioButton ->
+ toggleIP(radioButton)
+ }
+ }
+ }
+
+ private fun toggleIP(radioButton: View?) {
+ if (radioButton is RadioButton) {
+ val checked = radioButton.isChecked
+ when (radioButton.id) {
+ R.id.radio_use_real_ip ->
+ if (checked) {
+ viewModel.submitAction(InternetPrivacyFeature.Action.UseRealIPAction)
+ }
+ R.id.radio_use_hidden_ip ->
+ if (checked) {
+ viewModel.submitAction(InternetPrivacyFeature.Action.UseHiddenIPAction)
+ }
+ }
+ }
+ }
+
+ override fun render(state: InternetPrivacyFeature.State) {
+ view?.let {
+ it.findViewById<RadioButton>(R.id.radio_use_hidden_ip).isChecked =
+ state.mode == InternetPrivacyMode.HIDE_IP
+ it.findViewById<RadioButton>(R.id.radio_use_real_ip).isChecked =
+ state.mode == InternetPrivacyMode.REAL_IP
+ }
+ }
+
+ override fun actions(): Flow<InternetPrivacyFeature.Action> = viewModel.actions
}
diff --git a/app/src/main/java/foundation/e/privacycentralapp/features/internetprivacy/InternetPrivacyViewModel.kt b/app/src/main/java/foundation/e/privacycentralapp/features/internetprivacy/InternetPrivacyViewModel.kt
new file mode 100644
index 0000000..b66b611
--- /dev/null
+++ b/app/src/main/java/foundation/e/privacycentralapp/features/internetprivacy/InternetPrivacyViewModel.kt
@@ -0,0 +1,40 @@
+/*
+ * 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.privacycentralapp.features.internetprivacy
+
+import androidx.lifecycle.ViewModel
+import androidx.lifecycle.viewModelScope
+import kotlinx.coroutines.flow.MutableSharedFlow
+import kotlinx.coroutines.flow.asSharedFlow
+import kotlinx.coroutines.launch
+
+class InternetPrivacyViewModel : ViewModel() {
+
+ private val _actions = MutableSharedFlow<InternetPrivacyFeature.Action>()
+ val actions = _actions.asSharedFlow()
+
+ val internetPrivacyFeature: InternetPrivacyFeature by lazy {
+ InternetPrivacyFeature.create(coroutineScope = viewModelScope)
+ }
+
+ fun submitAction(action: InternetPrivacyFeature.Action) {
+ viewModelScope.launch {
+ _actions.emit(action)
+ }
+ }
+}
diff --git a/app/src/main/java/foundation/e/privacycentralapp/features/location/FakeLocationFeature.kt b/app/src/main/java/foundation/e/privacycentralapp/features/location/FakeLocationFeature.kt
new file mode 100644
index 0000000..6b00490
--- /dev/null
+++ b/app/src/main/java/foundation/e/privacycentralapp/features/location/FakeLocationFeature.kt
@@ -0,0 +1,152 @@
+/*
+ * 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.privacycentralapp.features.location
+
+import android.util.Log
+import foundation.e.flowmvi.Actor
+import foundation.e.flowmvi.Reducer
+import foundation.e.flowmvi.SingleEventProducer
+import foundation.e.flowmvi.feature.BaseFeature
+import foundation.e.privacycentralapp.dummy.DummyDataSource
+import foundation.e.privacycentralapp.dummy.Location
+import foundation.e.privacycentralapp.dummy.LocationMode
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.flow.flowOf
+import kotlinx.coroutines.flow.map
+
+// Define a state machine for Fake location feature
+class FakeLocationFeature(
+ initialState: State,
+ coroutineScope: CoroutineScope,
+ reducer: Reducer<State, Effect>,
+ actor: Actor<State, Action, Effect>,
+ singleEventProducer: SingleEventProducer<State, Action, Effect, SingleEvent>
+) : BaseFeature<FakeLocationFeature.State, FakeLocationFeature.Action, FakeLocationFeature.Effect, FakeLocationFeature.SingleEvent>(
+ initialState, actor, reducer, coroutineScope, { message -> Log.d("FakeLocationFeature", message) },
+ singleEventProducer
+) {
+ sealed class State {
+ object InitialState : State()
+ data class LocationState(val location: Location) : State()
+ }
+
+ sealed class SingleEvent {
+ object RandomLocationSelectedEvent : SingleEvent()
+ object RealLocationSelectedEvent : SingleEvent()
+ object SpecificLocationSavedEvent : SingleEvent()
+ data class ErrorEvent(val error: String) : SingleEvent()
+ }
+
+ sealed class Action {
+ object ObserveLocationAction : Action()
+ object UseRealLocationAction : Action()
+ object UseRandomLocationAction : Action()
+ object UseSpecificLocationAction : Action()
+ data class AddSpecificLocationAction(val latitude: Double, val longitude: Double) : Action()
+ }
+
+ sealed class Effect {
+ data class LocationUpdatedEffect(val location: Location) : Effect()
+ object RealLocationSelectedEffect : Effect()
+ object RandomLocationSelectedEffect : Effect()
+ data class SpecificLocationSelectedEffect(val location: Location) : Effect()
+ object SpecificLocationSavedEffect : Effect()
+ data class ErrorEffect(val message: String) : Effect()
+ }
+
+ companion object {
+ fun create(
+ initialState: State = State.InitialState,
+ coroutineScope: CoroutineScope
+ ) = FakeLocationFeature(
+ initialState, coroutineScope,
+ reducer = { state, effect ->
+ when (effect) {
+ Effect.RandomLocationSelectedEffect,
+ Effect.RealLocationSelectedEffect, is Effect.ErrorEffect, Effect.SpecificLocationSavedEffect -> state
+ is Effect.LocationUpdatedEffect -> State.LocationState(effect.location)
+ is Effect.SpecificLocationSelectedEffect -> State.LocationState(effect.location)
+ }
+ },
+ actor = { _, action ->
+ when (action) {
+ is Action.ObserveLocationAction -> DummyDataSource.location.map {
+ Effect.LocationUpdatedEffect(it)
+ }
+ is Action.AddSpecificLocationAction -> {
+ val location = Location(
+ LocationMode.CUSTOM_LOCATION,
+ action.latitude,
+ action.longitude
+ )
+ val success = DummyDataSource.setLocationMode(
+ LocationMode.CUSTOM_LOCATION,
+ location
+ )
+ if (success) {
+ flowOf(
+ Effect.SpecificLocationSavedEffect
+ )
+ } else {
+ flowOf(
+ Effect.ErrorEffect("Couldn't select location")
+ )
+ }
+ }
+ Action.UseRandomLocationAction -> {
+ val success = DummyDataSource.setLocationMode(LocationMode.RANDOM_LOCATION)
+ if (success) {
+ flowOf(
+ Effect.RandomLocationSelectedEffect
+ )
+ } else {
+ flowOf(
+ Effect.ErrorEffect("Couldn't select location")
+ )
+ }
+ }
+ Action.UseRealLocationAction -> {
+ val success = DummyDataSource.setLocationMode(LocationMode.REAL_LOCATION)
+ if (success) {
+ flowOf(
+ Effect.RealLocationSelectedEffect
+ )
+ } else {
+ flowOf(
+ Effect.ErrorEffect("Couldn't select location")
+ )
+ }
+ }
+ Action.UseSpecificLocationAction -> {
+ val location = DummyDataSource.location.value
+ flowOf(Effect.SpecificLocationSelectedEffect(location.copy(mode = LocationMode.CUSTOM_LOCATION)))
+ }
+ }
+ },
+ singleEventProducer = { _, _, effect ->
+ when (effect) {
+ Effect.RandomLocationSelectedEffect -> SingleEvent.RandomLocationSelectedEvent
+ Effect.SpecificLocationSavedEffect -> SingleEvent.SpecificLocationSavedEvent
+ Effect.RealLocationSelectedEffect -> SingleEvent.RealLocationSelectedEvent
+ is Effect.ErrorEffect -> SingleEvent.ErrorEvent(effect.message)
+ else -> null
+ }
+ }
+ )
+ }
+}
diff --git a/app/src/main/java/foundation/e/privacycentralapp/features/location/FakeLocationFragment.kt b/app/src/main/java/foundation/e/privacycentralapp/features/location/FakeLocationFragment.kt
new file mode 100644
index 0000000..6831680
--- /dev/null
+++ b/app/src/main/java/foundation/e/privacycentralapp/features/location/FakeLocationFragment.kt
@@ -0,0 +1,176 @@
+/*
+ * 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.privacycentralapp.features.location
+
+import android.os.Bundle
+import android.text.Editable
+import android.util.Log
+import android.view.View
+import android.widget.Button
+import android.widget.ImageView
+import android.widget.RadioButton
+import android.widget.Toast
+import android.widget.Toolbar
+import androidx.fragment.app.Fragment
+import androidx.fragment.app.viewModels
+import androidx.lifecycle.lifecycleScope
+import com.google.android.material.textfield.TextInputLayout
+import foundation.e.flowmvi.MVIView
+import foundation.e.privacycentralapp.R
+import foundation.e.privacycentralapp.dummy.LocationMode
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.collect
+
+class FakeLocationFragment :
+ Fragment(R.layout.fragment_fake_location),
+ MVIView<FakeLocationFeature.State, FakeLocationFeature.Action> {
+
+ private val viewModel: FakeLocationViewModel by viewModels()
+
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+ lifecycleScope.launchWhenStarted {
+ viewModel.fakeLocationFeature.takeView(this, this@FakeLocationFragment)
+ }
+ lifecycleScope.launchWhenStarted {
+ viewModel.fakeLocationFeature.singleEvents.collect { event ->
+ when (event) {
+ is FakeLocationFeature.SingleEvent.RandomLocationSelectedEvent -> displayToast("Random location selected")
+ is FakeLocationFeature.SingleEvent.SpecificLocationSavedEvent -> displayToast("Specific location selected")
+ is FakeLocationFeature.SingleEvent.ErrorEvent -> displayToast(event.error)
+ FakeLocationFeature.SingleEvent.RealLocationSelectedEvent -> displayToast("Real location selected")
+ }
+ }
+ }
+ lifecycleScope.launchWhenStarted {
+ viewModel.submitAction(FakeLocationFeature.Action.ObserveLocationAction)
+ }
+ }
+
+ private fun displayToast(message: String) {
+ Toast.makeText(requireContext(), message, Toast.LENGTH_SHORT)
+ .show()
+ }
+
+ override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
+ super.onViewCreated(view, savedInstanceState)
+ val toolbar = view.findViewById<Toolbar>(R.id.toolbar)
+ setupToolbar(toolbar)
+ bindClickListeners(view)
+ }
+
+ private fun bindClickListeners(fragmentView: View) {
+ fragmentView.let {
+ it.findViewById<RadioButton>(R.id.radio_use_real_location)
+ .setOnClickListener { radioButton ->
+ toggleLocationType(radioButton)
+ }
+ it.findViewById<RadioButton>(R.id.radio_use_random_location)
+ .setOnClickListener { radioButton ->
+ toggleLocationType(radioButton)
+ }
+ it.findViewById<RadioButton>(R.id.radio_use_specific_location)
+ .setOnClickListener { radioButton ->
+ toggleLocationType(radioButton)
+ }
+ it.findViewById<Button>(R.id.button_add_location)
+ .setOnClickListener {
+ val latitude =
+ fragmentView.findViewById<TextInputLayout>(R.id.edittext_latitude).editText?.text.toString()
+ .toDouble()
+ val longitude =
+ fragmentView.findViewById<TextInputLayout>(R.id.edittext_longitude).editText?.text.toString()
+ .toDouble()
+ saveSpecificLocation(latitude, longitude)
+ }
+ }
+ }
+
+ private fun saveSpecificLocation(latitude: Double, longitude: Double) {
+ viewModel.submitAction(
+ FakeLocationFeature.Action.AddSpecificLocationAction(latitude, longitude)
+ )
+ }
+
+ private fun toggleLocationType(radioButton: View?) {
+ if (radioButton is RadioButton) {
+ val checked = radioButton.isChecked
+ when (radioButton.id) {
+ R.id.radio_use_real_location ->
+ if (checked) {
+ viewModel.submitAction(FakeLocationFeature.Action.UseRealLocationAction)
+ }
+ R.id.radio_use_random_location ->
+ if (checked) {
+ viewModel.submitAction(FakeLocationFeature.Action.UseRandomLocationAction)
+ }
+ R.id.radio_use_specific_location ->
+ if (checked) {
+ viewModel.submitAction(FakeLocationFeature.Action.UseSpecificLocationAction)
+ }
+ }
+ }
+ }
+
+ private fun setupToolbar(toolbar: Toolbar) {
+ val activity = requireActivity()
+ activity.setActionBar(toolbar)
+ activity.title = "Fake My Location"
+ }
+
+ override fun render(state: FakeLocationFeature.State) {
+ when (state) {
+ is FakeLocationFeature.State.LocationState -> {
+ Log.d("FakeMyLocation", "State: $state")
+ when (state.location.mode) {
+ LocationMode.REAL_LOCATION, LocationMode.RANDOM_LOCATION ->
+ view?.let {
+ it.findViewById<RadioButton>(R.id.radio_use_random_location).isChecked =
+ (state.location.mode == LocationMode.RANDOM_LOCATION)
+ it.findViewById<RadioButton>(R.id.radio_use_real_location).isChecked =
+ (state.location.mode == LocationMode.REAL_LOCATION)
+ it.findViewById<ImageView>(R.id.dummy_img_map).visibility = View.GONE
+ it.findViewById<TextInputLayout>(R.id.edittext_latitude).visibility =
+ View.GONE
+ it.findViewById<TextInputLayout>(R.id.edittext_longitude).visibility =
+ View.GONE
+ it.findViewById<Button>(R.id.button_add_location).visibility = View.GONE
+ }
+ LocationMode.CUSTOM_LOCATION -> view?.let {
+ it.findViewById<RadioButton>(R.id.radio_use_specific_location).isChecked =
+ true
+ it.findViewById<ImageView>(R.id.dummy_img_map).visibility = View.VISIBLE
+ it.findViewById<TextInputLayout>(R.id.edittext_latitude).apply {
+ visibility = View.VISIBLE
+ editText?.text = Editable.Factory.getInstance()
+ .newEditable(state.location.latitude.toString())
+ }
+ it.findViewById<TextInputLayout>(R.id.edittext_longitude).apply {
+ visibility = View.VISIBLE
+ editText?.text = Editable.Factory.getInstance()
+ .newEditable(state.location.longitude.toString())
+ }
+ it.findViewById<Button>(R.id.button_add_location).visibility = View.VISIBLE
+ }
+ }
+ }
+ }
+ }
+
+ override fun actions(): Flow<FakeLocationFeature.Action> = viewModel.actions
+}
diff --git a/app/src/main/java/foundation/e/privacycentralapp/features/location/FakeMyLocationFragment.kt b/app/src/main/java/foundation/e/privacycentralapp/features/location/FakeLocationViewModel.kt
index 24d3951..eb55fba 100644
--- a/app/src/main/java/foundation/e/privacycentralapp/features/location/FakeMyLocationFragment.kt
+++ b/app/src/main/java/foundation/e/privacycentralapp/features/location/FakeLocationViewModel.kt
@@ -17,22 +17,24 @@
package foundation.e.privacycentralapp.features.location
-import android.os.Bundle
-import android.view.View
-import android.widget.Toolbar
-import androidx.fragment.app.Fragment
-import foundation.e.privacycentralapp.R
+import androidx.lifecycle.ViewModel
+import androidx.lifecycle.viewModelScope
+import kotlinx.coroutines.flow.MutableSharedFlow
+import kotlinx.coroutines.flow.asSharedFlow
+import kotlinx.coroutines.launch
-class FakeMyLocationFragment : Fragment(R.layout.fragment_fake_location) {
- override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
- super.onViewCreated(view, savedInstanceState)
- val toolbar = view.findViewById<Toolbar>(R.id.toolbar)
- setupToolbar(toolbar)
+class FakeLocationViewModel : ViewModel() {
+
+ private val _actions = MutableSharedFlow<FakeLocationFeature.Action>()
+ val actions = _actions.asSharedFlow()
+
+ val fakeLocationFeature: FakeLocationFeature by lazy {
+ FakeLocationFeature.create(coroutineScope = viewModelScope)
}
- private fun setupToolbar(toolbar: Toolbar) {
- val activity = requireActivity()
- activity.setActionBar(toolbar)
- activity.title = "Fake My Location"
+ fun submitAction(action: FakeLocationFeature.Action) {
+ viewModelScope.launch {
+ _actions.emit(action)
+ }
}
}
diff --git a/app/src/main/java/foundation/e/privacycentralapp/main/MainActivity.kt b/app/src/main/java/foundation/e/privacycentralapp/main/MainActivity.kt
index d5b449f..42f9e24 100644
--- a/app/src/main/java/foundation/e/privacycentralapp/main/MainActivity.kt
+++ b/app/src/main/java/foundation/e/privacycentralapp/main/MainActivity.kt
@@ -25,7 +25,7 @@ import androidx.fragment.app.FragmentActivity
import androidx.fragment.app.add
import androidx.fragment.app.commit
import foundation.e.privacycentralapp.R
-import foundation.e.privacycentralapp.features.permissions.PermissionsFragment
+import foundation.e.privacycentralapp.features.dashboard.DashboardFragment
open class MainActivity : FragmentActivity(R.layout.activity_main) {
@@ -36,7 +36,7 @@ open class MainActivity : FragmentActivity(R.layout.activity_main) {
if (savedInstanceState == null) {
supportFragmentManager.commit {
setReorderingAllowed(true)
- add<PermissionsFragment>(R.id.container)
+ add<DashboardFragment>(R.id.container)
}
}
}
diff --git a/app/src/main/res/drawable/ic_my_location.xml b/app/src/main/res/drawable/ic_my_location.xml
new file mode 100644
index 0000000..3b04dc4
--- /dev/null
+++ b/app/src/main/res/drawable/ic_my_location.xml
@@ -0,0 +1,22 @@
+<!--
+ ~ 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/>.
+ -->
+
+<vector android:height="23.99944dp" android:viewportHeight="42.738"
+ android:viewportWidth="42.739" android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
+ <path android:fillColor="#fc7222" android:pathData="M42.74,5L42.74,37.738A5,5 0,0 1,37.74 42.738L5.001,42.738A5,5 0,0 1,0.001 37.738L0.001,5A5,5 0,0 1,5.001 0L37.74,0A5,5 0,0 1,42.74 5z"/>
+ <path android:fillColor="#fff" android:pathData="M21.369,3.651a13.318,13.318 0,0 0,-13.271 13.27c0,5.322 1.728,6.912 11.888,21.5a1.685,1.685 0,0 0,2.7 0c10.16,-14.584 11.957,-16.173 11.957,-21.5a13.364,13.364 0,0 0,-13.271 -13.27zM21.369,27.98a2.183,2.183 0,0 1,-2.212 -2.212,2.227 2.227,0 0,1 2.212,-2.212 2.274,2.274 0,0 1,2.212 2.212,2.227 2.227,0 0,1 -2.212,2.212zM23.169,20.17v0.138a1.079,1.079 0,0 1,-1.106 1.037h-1.106a1.079,1.079 0,0 1,-1.106 -1.037v-1.175a1.562,1.562 0,0 1,0.968 -1.451c2.074,-1.037 3.456,-1.866 3.456,-2.972a2.722,2.722 0,0 0,-2.773 -2.765,2.737 2.737,0 0,0 -2.626,2 1.038,1.038 0,0 1,-1.037 0.76h-1.175a1.112,1.112 0,0 1,-1.106 -1.313,6.146 6.146,0 0,1 5.944,-4.764 6.038,6.038 0,0 1,6.082 6.082c0,2.834 -2.281,4.354 -4.424,5.46z"/>
+</vector>
diff --git a/app/src/main/res/layout/fragment_dashboard.xml b/app/src/main/res/layout/fragment_dashboard.xml
index ed4de49..663c270 100644
--- a/app/src/main/res/layout/fragment_dashboard.xml
+++ b/app/src/main/res/layout/fragment_dashboard.xml
@@ -15,10 +15,20 @@
tools:layout_height="56dp"
/>
+ <ProgressBar
+ android:id="@+id/loadingSpinner"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_gravity="center"
+ android:indeterminate="true"
+ />
+
<androidx.core.widget.NestedScrollView
+ android:id="@+id/scrollContainer"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_marginTop="?android:attr/actionBarSize"
+ android:visibility="gone"
>
<LinearLayout
@@ -71,8 +81,8 @@
/>
<ImageView
- android:layout_width="wrap_content"
- android:layout_height="wrap_content"
+ android:layout_width="match_parent"
+ android:layout_height="160dp"
android:src="@drawable/dummy_leakage_analytics"
/>
@@ -84,250 +94,256 @@
android:gravity="center"
android:paddingLeft="32dp"
android:paddingRight="32dp"
+ android:paddingBottom="16dp"
android:text="@string/personal_leakage_info"
android:textColor="@color/black"
android:textSize="12sp"
/>
- <RelativeLayout
- android:id="@+id/am_i_tracked"
+ <LinearLayout
android:layout_width="match_parent"
- android:layout_height="wrap_content"
+ android:layout_height="match_parent"
android:background="#f9f9f9"
- android:paddingLeft="32dp"
- android:paddingTop="16dp"
- android:paddingRight="32dp"
- android:paddingBottom="16dp"
+ android:orientation="vertical"
>
- <ImageView
- android:id="@+id/am_i_tracked_icon"
- android:layout_width="36dp"
- android:layout_height="36dp"
- android:layout_alignParentStart="true"
- android:layout_centerVertical="true"
- android:src="@drawable/ic_tracked"
- />
-
- <LinearLayout
+ <RelativeLayout
+ android:id="@+id/am_i_tracked"
android:layout_width="match_parent"
android:layout_height="wrap_content"
- android:layout_toStartOf="@+id/am_i_tracked_chevron"
- android:layout_toEndOf="@+id/am_i_tracked_icon"
- android:orientation="vertical"
- android:paddingStart="16dp"
- android:paddingEnd="32dp"
+ android:paddingLeft="32dp"
+ android:paddingTop="16dp"
+ android:paddingRight="32dp"
+ android:paddingBottom="16dp"
>
- <TextView
- android:id="@+id/am_i_tracked_title"
- android:layout_width="match_parent"
- android:layout_height="wrap_content"
- android:fontFamily="sans-serif-medium"
- android:text="@string/am_i_tracked_title"
- android:textColor="@color/black"
- android:textSize="16sp"
+ <ImageView
+ android:id="@+id/am_i_tracked_icon"
+ android:layout_width="36dp"
+ android:layout_height="36dp"
+ android:layout_alignParentStart="true"
+ android:layout_centerVertical="true"
+ android:src="@drawable/ic_tracked"
/>
- <TextView
- android:id="@+id/am_i_tracked_subtitle"
+ <LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
- android:text="@string/am_i_tracked_subtitle"
- android:textColor="@color/black"
- android:textSize="14sp"
+ android:layout_toStartOf="@+id/am_i_tracked_chevron"
+ android:layout_toEndOf="@+id/am_i_tracked_icon"
+ android:orientation="vertical"
+ android:paddingStart="16dp"
+ android:paddingEnd="32dp"
+ >
+
+ <TextView
+ android:id="@+id/am_i_tracked_title"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:fontFamily="sans-serif-medium"
+ android:text="@string/am_i_tracked_title"
+ android:textColor="@color/black"
+ android:textSize="16sp"
+ />
+
+ <TextView
+ android:id="@+id/am_i_tracked_subtitle"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:text="@string/am_i_tracked_subtitle"
+ android:textColor="@color/black"
+ android:textSize="14sp"
+ />
+ </LinearLayout>
+
+ <ImageView
+ android:id="@+id/am_i_tracked_chevron"
+ android:layout_width="24dp"
+ android:layout_height="24dp"
+ android:layout_alignParentEnd="true"
+ android:layout_centerVertical="true"
+ android:src="@drawable/ic_chevron_right_24dp"
/>
- </LinearLayout>
-
- <ImageView
- android:id="@+id/am_i_tracked_chevron"
- android:layout_width="24dp"
- android:layout_height="24dp"
- android:layout_alignParentEnd="true"
- android:layout_centerVertical="true"
- android:src="@drawable/ic_chevron_right_24dp"
- />
- </RelativeLayout>
-
- <RelativeLayout
- android:id="@+id/apps_permissions"
- android:layout_width="match_parent"
- android:layout_height="wrap_content"
- android:background="#f9f9f9"
- android:paddingLeft="32dp"
- android:paddingTop="16dp"
- android:paddingRight="32dp"
- android:paddingBottom="16dp"
- >
+ </RelativeLayout>
- <ImageView
- android:id="@+id/apps_permissions_icon"
- android:layout_width="36dp"
- android:layout_height="36dp"
- android:layout_alignParentStart="true"
- android:layout_centerVertical="true"
- android:src="@drawable/ic_apps_permissions"
- />
-
- <LinearLayout
+ <RelativeLayout
+ android:id="@+id/apps_permissions"
android:layout_width="match_parent"
android:layout_height="wrap_content"
- android:layout_toStartOf="@+id/apps_permissions_chevron"
- android:layout_toEndOf="@+id/apps_permissions_icon"
- android:orientation="vertical"
- android:paddingStart="16dp"
- android:paddingEnd="32dp"
+ android:paddingLeft="32dp"
+ android:paddingTop="16dp"
+ android:paddingRight="32dp"
+ android:paddingBottom="16dp"
>
- <TextView
- android:id="@+id/apps_permissions_title"
- android:layout_width="match_parent"
- android:layout_height="wrap_content"
- android:fontFamily="sans-serif-medium"
- android:text="@string/apps_permissions_title"
- android:textColor="@color/black"
- android:textSize="16sp"
+ <ImageView
+ android:id="@+id/apps_permissions_icon"
+ android:layout_width="36dp"
+ android:layout_height="36dp"
+ android:layout_alignParentStart="true"
+ android:layout_centerVertical="true"
+ android:src="@drawable/ic_apps_permissions"
/>
- <TextView
- android:id="@+id/apps_permissions_subtitle"
+ <LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
- android:text="@string/apps_permissions_subtitle"
- android:textColor="@color/black"
- android:textSize="14sp"
+ android:layout_toStartOf="@+id/apps_permissions_chevron"
+ android:layout_toEndOf="@+id/apps_permissions_icon"
+ android:orientation="vertical"
+ android:paddingStart="16dp"
+ android:paddingEnd="32dp"
+ >
+
+ <TextView
+ android:id="@+id/apps_permissions_title"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:fontFamily="sans-serif-medium"
+ android:text="@string/apps_permissions_title"
+ android:textColor="@color/black"
+ android:textSize="16sp"
+ />
+
+ <TextView
+ android:id="@+id/apps_permissions_subtitle"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:text="@string/apps_permissions_subtitle"
+ android:textColor="@color/black"
+ android:textSize="14sp"
+ />
+ </LinearLayout>
+
+ <ImageView
+ android:id="@+id/apps_permissions_chevron"
+ android:layout_width="24dp"
+ android:layout_height="24dp"
+ android:layout_alignParentEnd="true"
+ android:layout_centerVertical="true"
+ android:src="@drawable/ic_chevron_right_24dp"
/>
- </LinearLayout>
-
- <ImageView
- android:id="@+id/apps_permissions_chevron"
- android:layout_width="24dp"
- android:layout_height="24dp"
- android:layout_alignParentEnd="true"
- android:layout_centerVertical="true"
- android:src="@drawable/ic_chevron_right_24dp"
- />
- </RelativeLayout>
-
- <RelativeLayout
- android:id="@+id/my_location"
- android:layout_width="match_parent"
- android:layout_height="wrap_content"
- android:background="#f9f9f9"
- android:paddingLeft="32dp"
- android:paddingTop="16dp"
- android:paddingRight="32dp"
- android:paddingBottom="16dp"
- >
-
- <ImageView
- android:id="@+id/my_location_icon"
- android:layout_width="36dp"
- android:layout_height="36dp"
- android:layout_alignParentStart="true"
- android:layout_centerVertical="true"
- android:src="@drawable/ic_location"
- />
+ </RelativeLayout>
- <LinearLayout
+ <RelativeLayout
+ android:id="@+id/my_location"
android:layout_width="match_parent"
android:layout_height="wrap_content"
- android:layout_toStartOf="@+id/my_location_chevron"
- android:layout_toEndOf="@+id/my_location_icon"
- android:orientation="vertical"
- android:paddingStart="16dp"
- android:paddingEnd="32dp"
+ android:paddingLeft="32dp"
+ android:paddingTop="16dp"
+ android:paddingRight="32dp"
+ android:paddingBottom="16dp"
>
- <TextView
- android:id="@+id/my_location_title"
- android:layout_width="match_parent"
- android:layout_height="wrap_content"
- android:fontFamily="sans-serif-medium"
- android:text="@string/my_location_title"
- android:textColor="@color/black"
- android:textSize="16sp"
+ <ImageView
+ android:id="@+id/my_location_icon"
+ android:layout_width="36dp"
+ android:layout_height="36dp"
+ android:layout_alignParentStart="true"
+ android:layout_centerVertical="true"
+ android:src="@drawable/ic_my_location"
/>
- <TextView
- android:id="@+id/my_location_subtitle"
+ <LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
- android:text="@string/my_location_subtitle"
- android:textColor="@color/black"
- android:textSize="14sp"
+ android:layout_toStartOf="@+id/my_location_chevron"
+ android:layout_toEndOf="@+id/my_location_icon"
+ android:orientation="vertical"
+ android:paddingStart="16dp"
+ android:paddingEnd="32dp"
+ >
+
+ <TextView
+ android:id="@+id/my_location_title"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:fontFamily="sans-serif-medium"
+ android:text="@string/my_location_title"
+ android:textColor="@color/black"
+ android:textSize="16sp"
+ />
+
+ <TextView
+ android:id="@+id/my_location_subtitle"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:text="@string/my_location_subtitle"
+ android:textColor="@color/black"
+ android:textSize="14sp"
+ />
+ </LinearLayout>
+
+ <ImageView
+ android:id="@+id/my_location_chevron"
+ android:layout_width="24dp"
+ android:layout_height="24dp"
+ android:layout_alignParentEnd="true"
+ android:layout_centerVertical="true"
+ android:src="@drawable/ic_chevron_right_24dp"
/>
- </LinearLayout>
-
- <ImageView
- android:id="@+id/my_location_chevron"
- android:layout_width="24dp"
- android:layout_height="24dp"
- android:layout_alignParentEnd="true"
- android:layout_centerVertical="true"
- android:src="@drawable/ic_chevron_right_24dp"
- />
- </RelativeLayout>
-
- <RelativeLayout
- android:id="@+id/internet_activity_privacy"
- android:layout_width="match_parent"
- android:layout_height="wrap_content"
- android:background="#f9f9f9"
- android:paddingLeft="32dp"
- android:paddingTop="16dp"
- android:paddingRight="32dp"
- android:paddingBottom="16dp"
- >
-
- <ImageView
- android:id="@+id/internet_activity_privacy_icon"
- android:layout_width="36dp"
- android:layout_height="36dp"
- android:layout_alignParentStart="true"
- android:layout_centerVertical="true"
- android:src="@drawable/ic_internet_activity"
- />
+ </RelativeLayout>
- <LinearLayout
+ <RelativeLayout
+ android:id="@+id/internet_activity_privacy"
android:layout_width="match_parent"
android:layout_height="wrap_content"
- android:layout_toStartOf="@+id/internet_activity_privacy_chevron"
- android:layout_toEndOf="@+id/internet_activity_privacy_icon"
- android:orientation="vertical"
- android:paddingStart="16dp"
- android:paddingEnd="32dp"
+ android:paddingLeft="32dp"
+ android:paddingTop="16dp"
+ android:paddingRight="32dp"
+ android:paddingBottom="16dp"
>
- <TextView
- android:id="@+id/internet_activity_privacy_title"
- android:layout_width="match_parent"
- android:layout_height="wrap_content"
- android:fontFamily="sans-serif-medium"
- android:text="@string/internet_activity_privacy_title"
- android:textColor="@color/black"
- android:textSize="16sp"
+ <ImageView
+ android:id="@+id/internet_activity_privacy_icon"
+ android:layout_width="36dp"
+ android:layout_height="36dp"
+ android:layout_alignParentStart="true"
+ android:layout_centerVertical="true"
+ android:src="@drawable/ic_internet_activity"
/>
- <TextView
- android:id="@+id/internet_activity_privacy_subtitle"
+ <LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
- android:text="@string/internet_activity_privacy_subtitle"
- android:textColor="@color/black"
- android:textSize="14sp"
+ android:layout_toStartOf="@+id/internet_activity_privacy_chevron"
+ android:layout_toEndOf="@+id/internet_activity_privacy_icon"
+ android:orientation="vertical"
+ android:paddingStart="16dp"
+ android:paddingEnd="32dp"
+ >
+
+ <TextView
+ android:id="@+id/internet_activity_privacy_title"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:fontFamily="sans-serif-medium"
+ android:text="@string/internet_activity_privacy_title"
+ android:textColor="@color/black"
+ android:textSize="16sp"
+ />
+
+ <TextView
+ android:id="@+id/internet_activity_privacy_subtitle"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:text="@string/internet_activity_privacy_subtitle"
+ android:textColor="@color/black"
+ android:textSize="14sp"
+ />
+ </LinearLayout>
+
+ <ImageView
+ android:id="@+id/internet_activity_privacy_chevron"
+ android:layout_width="24dp"
+ android:layout_height="24dp"
+ android:layout_alignParentEnd="true"
+ android:layout_centerVertical="true"
+ android:src="@drawable/ic_chevron_right_24dp"
/>
- </LinearLayout>
-
- <ImageView
- android:id="@+id/internet_activity_privacy_chevron"
- android:layout_width="24dp"
- android:layout_height="24dp"
- android:layout_alignParentEnd="true"
- android:layout_centerVertical="true"
- android:src="@drawable/ic_chevron_right_24dp"
- />
- </RelativeLayout>
+ </RelativeLayout>
+
+ </LinearLayout>
</LinearLayout>
</androidx.core.widget.NestedScrollView>
diff --git a/app/src/main/res/layout/fragment_fake_location.xml b/app/src/main/res/layout/fragment_fake_location.xml
index 1b02f86..1ebe9ef 100644
--- a/app/src/main/res/layout/fragment_fake_location.xml
+++ b/app/src/main/res/layout/fragment_fake_location.xml
@@ -99,6 +99,7 @@
</RadioGroup>
<ImageView
+ android:id="@+id/dummy_img_map"
android:layout_width="match_parent"
android:layout_height="254dp"
android:layout_marginTop="32dp"
diff --git a/app/src/main/res/layout/fragment_internet_activity_policy.xml b/app/src/main/res/layout/fragment_internet_activity_policy.xml
index 787ee11..6a53498 100644
--- a/app/src/main/res/layout/fragment_internet_activity_policy.xml
+++ b/app/src/main/res/layout/fragment_internet_activity_policy.xml
@@ -87,10 +87,10 @@
android:textSize="14sp"/>
<foundation.e.privacycentralapp.common.RightRadioButton
- android:id="@+id/radio_hide_ip"
+ android:id="@+id/radio_use_hidden_ip"
android:layout_width="match_parent"
android:layout_height="wrap_content"
- android:text="@string/hide_ip"
+ android:text="@string/hidden_ip"
android:textSize="16sp"
android:layout_marginTop="8dp"
/>
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
index f79852c..989e233 100644
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -4,11 +4,11 @@
<string name="tap_to_enable_quick_protection">Tap to enable quick privacy protection</string>
<string name="personal_leakage_info">Personal data leakage over past 24 hours. </string>
<string name="am_i_tracked_title">Am I tracked?</string>
- <string name="am_i_tracked_subtitle">Currently there are 77 trackers in your apps, 12 trackers are active</string>
+ <string name="am_i_tracked_subtitle">Currently there are %1$d trackers in your apps, %2$d trackers are active</string>
<string name="apps_permissions_title">Apps Permissions</string>
- <string name="apps_permissions_subtitle">120 apps are requesting 72 permissions</string>
+ <string name="apps_permissions_subtitle">%1$d apps are requesting %2$d permissions</string>
<string name="my_location_title">My Location</string>
- <string name="my_location_subtitle">"7 apps are using location permission\n Current location mode: "</string>
+ <string name="my_location_subtitle">"%1$d apps are using location permission\nCurrent location mode: "</string>
<string name="internet_activity_privacy_title">My Internet Activity Privacy</string>
<string name="internet_activity_privacy_subtitle">"Current internet activity mode: "</string>
<string name="quick_protection_info">Quick protection enables these settings when turned on</string>
@@ -24,7 +24,7 @@
<string name="internet_activity_privacy_info">Choose if you want to expose your real IP address or hide when connected to the internet (uses the tor network).</string>
<string name="use_real_ip">Use real IP address</string>
<string name="i_can_be_tracked">I can be tracked by my IP address.</string>
- <string name="hide_ip">Hide IP address</string>
+ <string name="hidden_ip">Hide IP address</string>
<string name="i_am_anonymous">I am anonymous on the internet.</string>
<string name="permission_control_info">Manage and control apps requesting various permissions.</string>
diff --git a/build.gradle b/build.gradle
index f4952ba..57dc767 100644
--- a/build.gradle
+++ b/build.gradle
@@ -80,6 +80,7 @@ subprojects {
// Treat all Kotlin warnings as errors
allWarningsAsErrors = true
+ freeCompilerArgs += "-Xopt-in=kotlinx.coroutines.ExperimentalCoroutinesApi"
// Set JVM target to 1.8
jvmTarget = "1.8"
}
diff --git a/flow-mvi/src/main/java/foundation/e/flowmvi/feature/BaseFeature.kt b/flow-mvi/src/main/java/foundation/e/flowmvi/feature/BaseFeature.kt
index f7236ca..8dec0c4 100644
--- a/flow-mvi/src/main/java/foundation/e/flowmvi/feature/BaseFeature.kt
+++ b/flow-mvi/src/main/java/foundation/e/flowmvi/feature/BaseFeature.kt
@@ -101,8 +101,8 @@ open class BaseFeature<State : Any, in Action : Any, in Effect : Any, SingleEven
logger.invoke("View actions flow started")
emitAll(initialActions.asFlow())
}
- .onCompletion {
- logger.invoke("View actions flow completed")
+ .onCompletion { cause ->
+ logger.invoke("View actions flow completed: $cause")
}
.collectIntoHandler(this, logger)
}
@@ -142,19 +142,3 @@ open class BaseFeature<State : Any, in Action : Any, in Effect : Any, SingleEven
.launchIn(callerCoroutineScope)
}
}
-
-fun <State : Any, Action : Any, Effect : Any, SingleEvent : Any> feature(
- initialState: State,
- actor: Actor<State, Action, Effect>,
- reducer: Reducer<State, Effect>,
- coroutineScope: CoroutineScope,
- defaultLogger: Logger = {},
- singleEventProducer: SingleEventProducer<State, Action, Effect, SingleEvent>? = null
-) = BaseFeature(
- initialState,
- actor,
- reducer,
- coroutineScope,
- defaultLogger,
- singleEventProducer
-)