diff options
author | Amit Kumar <amitkma@e.email> | 2021-05-03 23:38:48 +0530 |
---|---|---|
committer | Amit Kumar <amitkma@e.email> | 2021-05-03 23:38:48 +0530 |
commit | 28f21297e4d700384f0d445fd4a296ad2bcc496a (patch) | |
tree | d85af6dab61c230ab5f0b0f701737fa00580efa5 | |
parent | 0d036a8742f54eb9ea87e64fdce9ab878f58859b (diff) |
Add FakeLocation and InternetActivityPrivacy feature
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 -) |