diff options
15 files changed, 434 insertions, 154 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 3f2dc1e..aef994b 100644 --- a/app/src/main/java/foundation/e/privacycentralapp/dummy/DummyDataSource.kt +++ b/app/src/main/java/foundation/e/privacycentralapp/dummy/DummyDataSource.kt @@ -45,10 +45,11 @@ import kotlin.random.Random * Dummmy permission data class. */ data class Permission( + val id: Int, val name: String, val iconId: Int, - val packagesRequested: List<String> = emptyList(), - val packagesAllowed: List<String> = emptyList() + val packagesRequested: Set<String> = emptySet(), + val packagesAllowed: Set<String> = emptySet() ) enum class LocationMode { @@ -62,8 +63,6 @@ enum class InternetPrivacyMode { 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) @@ -75,8 +74,17 @@ object DummyDataSource { private val _internetActivityMode = MutableStateFlow(InternetPrivacyMode.REAL_IP) val internetActivityMode = _internetActivityMode.asStateFlow() + /** + * Declare dummy permissions with following ids + * + * [0] -> Body sensor + * [1] -> Calendar + * [2] -> Call Logs + * [3] -> Location + */ val permissions = arrayOf("Body Sensor", "Calendar", "Call Logs", "Location") - val icons = arrayOf( + + private val permissionIcons = arrayOf( R.drawable.ic_body_monitor, R.drawable.ic_calendar, R.drawable.ic_call, @@ -98,22 +106,26 @@ object DummyDataSource { "privacycentral" ) - val populatedPermission: List<Permission> by lazy { - fetchPermissions() - } + val _populatedPermissions = MutableStateFlow(fetchPermissions()) + val populatedPermission = _populatedPermissions.asStateFlow() + + private val _appsUsingLocationPerm = + MutableStateFlow(_populatedPermissions.value[3].packagesAllowed) + val appsUsingLocationPerm = _appsUsingLocationPerm.asStateFlow() private fun fetchPermissions(): List<Permission> { val result = mutableListOf<Permission>() permissions.forEachIndexed { index, permission -> when (index) { - 0 -> result.add(Permission(permission, icons[index])) + 0 -> result.add(Permission(index, permission, permissionIcons[index])) 1 -> { val randomPackages = getRandomItems(packages, 8) val grantedPackages = getRandomItems(randomPackages, 3) result.add( Permission( + index, permission, - icons[index], + permissionIcons[index], randomPackages, grantedPackages ) @@ -124,8 +136,9 @@ object DummyDataSource { val grantedPackages = getRandomItems(randomPackages, 9) result.add( Permission( + index, permission, - icons[index], + permissionIcons[index], randomPackages, grantedPackages ) @@ -136,8 +149,9 @@ object DummyDataSource { val grantedPackages = getRandomItems(randomPackages, 3) result.add( Permission( + index, permission, - icons[index], + permissionIcons[index], randomPackages, grantedPackages ) @@ -148,10 +162,10 @@ object DummyDataSource { return result } - private fun <T> getRandomItems(data: Array<T>, limit: Int): List<T> = - getRandomItems(data.asList(), limit) + private fun <T> getRandomItems(data: Array<T>, limit: Int): Set<T> = + getRandomItems(data.toSet(), limit) - private fun <T> getRandomItems(data: List<T>, limit: Int): List<T> { + private fun <T> getRandomItems(data: Set<T>, limit: Int): Set<T> { val randomItems = mutableSetOf<T>() val localData = data.toMutableList() repeat(limit) { @@ -159,12 +173,12 @@ object DummyDataSource { randomItems.add(generated) localData.remove(generated) } - return randomItems.toList() + return randomItems } - fun getPermission(permissionId: Int): Permission { - return populatedPermission.get(permissionId) - } + fun getPermission(permissionId: Int): Permission = populatedPermission.value[permissionId] + + fun getLocationPermissionApps(): Permission = getPermission(3) fun setLocationMode(locationMode: LocationMode, location: Location? = null): Boolean { when (locationMode) { @@ -190,4 +204,24 @@ object DummyDataSource { _internetActivityMode.value = mode return true } + + fun togglePermission(permissionId: Int, packageName: String, grant: Boolean) { + val allPermissions = _populatedPermissions.value.toMutableList() + val permission: Permission = allPermissions[permissionId].let { permission -> + + val packagesAllowed = permission.packagesAllowed.toMutableSet() + + if (grant) packagesAllowed.add(packageName) + else packagesAllowed.remove(packageName) + + permission.copy(packagesAllowed = packagesAllowed) + } + allPermissions[permissionId] = permission + _populatedPermissions.value = allPermissions + + // Update when permission is toggled for Location + if (permissionId == 3) { + _appsUsingLocationPerm.value = _populatedPermissions.value[permissionId].packagesAllowed + } + } } diff --git a/app/src/main/java/foundation/e/privacycentralapp/dummy/Extensions.kt b/app/src/main/java/foundation/e/privacycentralapp/dummy/Extensions.kt index c872012..133ad84 100644 --- a/app/src/main/java/foundation/e/privacycentralapp/dummy/Extensions.kt +++ b/app/src/main/java/foundation/e/privacycentralapp/dummy/Extensions.kt @@ -17,13 +17,16 @@ 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" +import android.content.Context +import foundation.e.privacycentralapp.R + +fun LocationMode.mapToString(context: Context): String = when (this) { + LocationMode.REAL_LOCATION -> context.getString(R.string.real_location_mode) + LocationMode.RANDOM_LOCATION -> context.getString(R.string.random_location_mode) + LocationMode.CUSTOM_LOCATION -> context.getString(R.string.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" +fun InternetPrivacyMode.mapToString(context: Context): String = when (this) { + InternetPrivacyMode.REAL_IP -> context.getString(R.string.i_am_exposing) + InternetPrivacyMode.HIDE_IP -> context.getString(R.string.i_am_anonymous) } 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 b9371be..f0a7397 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 @@ -55,29 +55,36 @@ class 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") + when (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") + 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") + 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") + is DashboardFeature.SingleEvent.NavigateToPermissionsSingleEvent -> { + requireActivity().supportFragmentManager.commit { + add<PermissionsFragment>(R.id.container) + setReorderingAllowed(true) + addToBackStack("dashboard") + } + } + DashboardFeature.SingleEvent.NavigateToTrackersSingleEvent -> { } } } @@ -149,27 +156,13 @@ class DashboardFragment : state.totalApps, state.permissionCount ) - view.findViewById<TextView>(R.id.my_location_subtitle).let { - it.text = getString( + view.findViewById<TextView>(R.id.my_location_subtitle).let { textView -> + textView.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()) + textView.append( + SpannableString(state.locationMode.mapToString(requireContext())) .also { it.setSpan( ForegroundColorSpan(Color.parseColor("#007fff")), @@ -180,8 +173,25 @@ class DashboardFragment : } ) } + view.findViewById<TextView>(R.id.internet_activity_privacy_subtitle) + .let { textView -> + textView.text = getString(R.string.internet_activity_privacy_subtitle) + textView.append( + SpannableString(state.internetPrivacyMode.mapToString(requireContext())) + .also { + it.setSpan( + ForegroundColorSpan(Color.parseColor("#007fff")), + 0, + it.length, + Spannable.SPAN_EXCLUSIVE_EXCLUSIVE + ) + } + ) + } } } + DashboardFeature.State.QuickProtectionState -> { + } } } 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 9428f41..1a3b3df 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,7 +17,6 @@ package foundation.e.privacycentralapp.features.dashboard -import android.util.Log import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import kotlinx.coroutines.flow.MutableSharedFlow @@ -34,7 +33,6 @@ class DashboardViewModel : ViewModel() { } 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 index fc4cf97..b34024e 100644 --- a/app/src/main/java/foundation/e/privacycentralapp/features/internetprivacy/InternetPrivacyFeature.kt +++ b/app/src/main/java/foundation/e/privacycentralapp/features/internetprivacy/InternetPrivacyFeature.kt @@ -40,7 +40,7 @@ class InternetPrivacyFeature( actor, reducer, coroutineScope, - { message -> Log.d("FakeLocationFeature", message) }, + { message -> Log.d("InternetPrivacyFeature", message) }, singleEventProducer ) { data class State(val mode: InternetPrivacyMode) 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 index 6831680..5b58293 100644 --- a/app/src/main/java/foundation/e/privacycentralapp/features/location/FakeLocationFragment.kt +++ b/app/src/main/java/foundation/e/privacycentralapp/features/location/FakeLocationFragment.kt @@ -169,6 +169,8 @@ class FakeLocationFragment : } } } + FakeLocationFeature.State.InitialState -> { + } } } diff --git a/app/src/main/java/foundation/e/privacycentralapp/features/permissions/PermissionAppsAdapter.kt b/app/src/main/java/foundation/e/privacycentralapp/features/permissions/PermissionAppsAdapter.kt index 19460cc..4f9b602 100644 --- a/app/src/main/java/foundation/e/privacycentralapp/features/permissions/PermissionAppsAdapter.kt +++ b/app/src/main/java/foundation/e/privacycentralapp/features/permissions/PermissionAppsAdapter.kt @@ -21,25 +21,33 @@ import android.annotation.SuppressLint import android.view.LayoutInflater import android.view.View import android.view.ViewGroup -import android.widget.ImageView import android.widget.Switch import android.widget.TextView import androidx.recyclerview.widget.RecyclerView import foundation.e.privacycentralapp.R -class PermissionAppsAdapter(private val dataSet: List<Pair<String, Boolean>>) : +class PermissionAppsAdapter( + private val dataSet: List<Pair<String, Boolean>>, + private val listener: (String, Boolean) -> Unit +) : RecyclerView.Adapter<PermissionAppsAdapter.PermissionViewHolder>() { class PermissionViewHolder(view: View) : RecyclerView.ViewHolder(view) { val appName: TextView = view.findViewById(R.id.app_title) - @SuppressLint("UseSwitchCompatOrMaterialCode") val togglePermission: Switch = view.findViewById(R.id.togglePermission) - val appIcon: ImageView = view.findViewById(R.id.app_icon) + + @SuppressLint("UseSwitchCompatOrMaterialCode") + val togglePermission: Switch = view.findViewById(R.id.togglePermission) } override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): PermissionViewHolder { val view = LayoutInflater.from(parent.context) .inflate(R.layout.item_permission_apps, parent, false) - return PermissionViewHolder(view) + val holder = PermissionViewHolder(view) + holder.togglePermission.setOnCheckedChangeListener { _, isChecked -> + listener(dataSet[holder.adapterPosition].first, isChecked) + } + view.findViewById<Switch>(R.id.togglePermission) + return holder } override fun onBindViewHolder(holder: PermissionViewHolder, position: Int) { diff --git a/app/src/main/java/foundation/e/privacycentralapp/features/permissions/PermissionAppsFragment.kt b/app/src/main/java/foundation/e/privacycentralapp/features/permissions/PermissionAppsFragment.kt new file mode 100644 index 0000000..224d1be --- /dev/null +++ b/app/src/main/java/foundation/e/privacycentralapp/features/permissions/PermissionAppsFragment.kt @@ -0,0 +1,105 @@ +/* + * 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.permissions + +import android.os.Bundle +import android.view.View +import android.widget.TextView +import android.widget.Toast +import android.widget.Toolbar +import androidx.fragment.app.Fragment +import androidx.fragment.app.viewModels +import androidx.lifecycle.lifecycleScope +import androidx.recyclerview.widget.LinearLayoutManager +import androidx.recyclerview.widget.RecyclerView +import foundation.e.flowmvi.MVIView +import foundation.e.privacycentralapp.R +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.collect + +class PermissionAppsFragment : + Fragment(R.layout.fragment_permission_apps), + MVIView<PermissionsFeature.State, PermissionsFeature.Action> { + + private val viewModel: PermissionsViewModel by viewModels() + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + lifecycleScope.launchWhenStarted { + viewModel.permissionsFeature.takeView(this, this@PermissionAppsFragment) + } + lifecycleScope.launchWhenStarted { + viewModel.permissionsFeature.singleEvents.collect { event -> + when (event) { + is PermissionsFeature.SingleEvent.ErrorEvent -> displayToast(event.error) + } + } + } + lifecycleScope.launchWhenStarted { + viewModel.submitAction( + PermissionsFeature.Action.LoadPermissionApps( + requireArguments().getInt( + "PERMISSION_ID" + ) + ) + ) + } + } + + 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) + } + + private fun setupToolbar(toolbar: Toolbar) { + val activity = requireActivity() + activity.setActionBar(toolbar) + activity.title = "My Apps Permission" + } + + override fun render(state: PermissionsFeature.State) { + state.currentPermission?.let { permission -> + view?.findViewById<RecyclerView>(R.id.recylcer_view_permission_apps)?.apply { + val listOfPackages = mutableListOf<Pair<String, Boolean>>() + permission.packagesRequested.forEach { + listOfPackages.add(it to permission.packagesAllowed.contains(it)) + } + layoutManager = LinearLayoutManager(requireContext()) + setHasFixedSize(true) + adapter = PermissionAppsAdapter(listOfPackages) { packageName, grant -> + viewModel.submitAction( + PermissionsFeature.Action.TogglePermissionAction( + packageName, + grant + ) + ) + } + } + view?.findViewById<TextView>(R.id.permission_control)?.text = + getString(R.string.apps_access_to_permission, permission.name) + } + } + + override fun actions(): Flow<PermissionsFeature.Action> = viewModel.actions +} diff --git a/app/src/main/java/foundation/e/privacycentralapp/features/permissions/PermissionControlFragment.kt b/app/src/main/java/foundation/e/privacycentralapp/features/permissions/PermissionControlFragment.kt deleted file mode 100644 index 55e3f88..0000000 --- a/app/src/main/java/foundation/e/privacycentralapp/features/permissions/PermissionControlFragment.kt +++ /dev/null @@ -1,59 +0,0 @@ -/* - * 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.permissions - -import android.os.Bundle -import android.view.View -import android.widget.TextView -import android.widget.Toolbar -import androidx.fragment.app.Fragment -import androidx.recyclerview.widget.LinearLayoutManager -import androidx.recyclerview.widget.RecyclerView -import foundation.e.privacycentralapp.R -import foundation.e.privacycentralapp.dummy.DummyDataSource - -class PermissionControlFragment : Fragment(R.layout.fragment_permission_control) { - override fun onViewCreated(view: View, savedInstanceState: Bundle?) { - super.onViewCreated(view, savedInstanceState) - val toolbar = view.findViewById<Toolbar>(R.id.toolbar) - setupToolbar(toolbar) - - val permissionId = requireArguments().getInt("PERMISSION_ID") - loadData(view, permissionId) - } - - private fun loadData(view: View, permissionId: Int) { - val recyclerView = view.findViewById<RecyclerView>(R.id.recylcer_view_permission_apps) - val permission = DummyDataSource.getPermission(permissionId) - val listOfPackages = mutableListOf<Pair<String, Boolean>>() - permission.packagesRequested.forEach { - listOfPackages.add(it to permission.packagesAllowed.contains(it)) - } - recyclerView.layoutManager = LinearLayoutManager(requireContext()) - recyclerView.setHasFixedSize(true) - recyclerView.adapter = PermissionAppsAdapter(listOfPackages) - view.findViewById<TextView>(R.id.permission_control).text = - getString(R.string.apps_access_to_permission, permission.name) - } - - private fun setupToolbar(toolbar: Toolbar) { - val activity = requireActivity() - activity.setActionBar(toolbar) - activity.title = "My Apps Permission" - } -} diff --git a/app/src/main/java/foundation/e/privacycentralapp/features/permissions/PermissionsFeature.kt b/app/src/main/java/foundation/e/privacycentralapp/features/permissions/PermissionsFeature.kt new file mode 100644 index 0000000..d095f00 --- /dev/null +++ b/app/src/main/java/foundation/e/privacycentralapp/features/permissions/PermissionsFeature.kt @@ -0,0 +1,118 @@ +/* + * 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.permissions + +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.Permission +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.flow.flowOf +import kotlinx.coroutines.flow.map + +// Define a state machine for Internet privacy feature +class PermissionsFeature( + initialState: State, + coroutineScope: CoroutineScope, + reducer: Reducer<State, Effect>, + actor: Actor<State, Action, Effect>, + singleEventProducer: SingleEventProducer<State, Action, Effect, SingleEvent> +) : BaseFeature<PermissionsFeature.State, PermissionsFeature.Action, PermissionsFeature.Effect, PermissionsFeature.SingleEvent>( + initialState, + actor, + reducer, + coroutineScope, + { message -> Log.d("PermissionsFeature", message) }, + singleEventProducer +) { + data class State( + val permissions: List<Permission> = emptyList(), + val currentPermission: Permission? = null + ) + + sealed class SingleEvent { + data class ErrorEvent(val error: String) : SingleEvent() + } + + sealed class Action { + object ObservePermissions : Action() + data class LoadPermissionApps(val id: Int) : Action() + data class TogglePermissionAction( + val packageName: String, + val grant: Boolean + ) : Action() + } + + sealed class Effect { + data class PermissionsLoadedEffect(val permissions: List<Permission>) : Effect() + data class PermissionLoadedEffect(val permission: Permission) : Effect() + object PermissionToggledEffect : Effect() + data class ErrorEffect(val message: String) : Effect() + } + + companion object { + fun create( + initialState: State = State(), + coroutineScope: CoroutineScope + ) = PermissionsFeature( + initialState, coroutineScope, + reducer = { state, effect -> + when (effect) { + is Effect.PermissionsLoadedEffect -> State(effect.permissions) + is Effect.PermissionLoadedEffect -> state.copy(currentPermission = effect.permission) + is Effect.ErrorEffect -> state + Effect.PermissionToggledEffect -> state + } + }, + actor = { state, action -> + when (action) { + Action.ObservePermissions -> DummyDataSource.populatedPermission.map { + Effect.PermissionsLoadedEffect(it) + } + is Action.LoadPermissionApps -> flowOf( + Effect.PermissionLoadedEffect( + DummyDataSource.getPermission(action.id) + ) + ) + + is Action.TogglePermissionAction -> { + if (state.currentPermission != null) { + DummyDataSource.togglePermission( + state.currentPermission.id, + action.packageName, + action.grant + ) + flowOf(Effect.PermissionToggledEffect) + } else { + flowOf(Effect.ErrorEffect("Can't update permission")) + } + } + } + }, + singleEventProducer = { _, _, effect -> + when (effect) { + is Effect.ErrorEffect -> SingleEvent.ErrorEvent(effect.message) + else -> null + } + } + ) + } +} diff --git a/app/src/main/java/foundation/e/privacycentralapp/features/permissions/PermissionsFragment.kt b/app/src/main/java/foundation/e/privacycentralapp/features/permissions/PermissionsFragment.kt index 27247ea..864a355 100644 --- a/app/src/main/java/foundation/e/privacycentralapp/features/permissions/PermissionsFragment.kt +++ b/app/src/main/java/foundation/e/privacycentralapp/features/permissions/PermissionsFragment.kt @@ -24,32 +24,34 @@ import androidx.core.os.bundleOf import androidx.fragment.app.Fragment import androidx.fragment.app.add import androidx.fragment.app.commit +import androidx.fragment.app.viewModels +import androidx.lifecycle.lifecycleScope import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.RecyclerView +import foundation.e.flowmvi.MVIView import foundation.e.privacycentralapp.R -import foundation.e.privacycentralapp.dummy.DummyDataSource +import kotlinx.coroutines.flow.Flow + +class PermissionsFragment : + Fragment(R.layout.fragment_permissions), + MVIView<PermissionsFeature.State, PermissionsFeature.Action> { + + private val viewModel: PermissionsViewModel by viewModels() + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + lifecycleScope.launchWhenStarted { + viewModel.permissionsFeature.takeView(this, this@PermissionsFragment) + } + lifecycleScope.launchWhenStarted { + viewModel.submitAction(PermissionsFeature.Action.ObservePermissions) + } + } -class PermissionsFragment : Fragment(R.layout.fragment_permissions) { override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) val toolbar = view.findViewById<Toolbar>(R.id.toolbar) setupToolbar(toolbar) - - loadDataIntoRecyclerView(view.findViewById(R.id.recylcer_view_permissions)) - } - - private fun loadDataIntoRecyclerView(view: RecyclerView) { - val permissions = DummyDataSource.populatedPermission - view.layoutManager = LinearLayoutManager(requireContext()) - view.setHasFixedSize(true) - view.adapter = PermissionsAdapter(requireContext(), permissions) { permissionId -> - requireActivity().supportFragmentManager.commit { - val bundle = bundleOf("PERMISSION_ID" to permissionId) - add<PermissionControlFragment>(R.id.container, args = bundle) - setReorderingAllowed(true) - addToBackStack("permissions") - } - } } private fun setupToolbar(toolbar: Toolbar) { @@ -57,4 +59,21 @@ class PermissionsFragment : Fragment(R.layout.fragment_permissions) { activity.setActionBar(toolbar) activity.title = "My Apps Permission" } + + override fun render(state: PermissionsFeature.State) { + view?.findViewById<RecyclerView>(R.id.recylcer_view_permissions)?.apply { + layoutManager = LinearLayoutManager(requireContext()) + setHasFixedSize(true) + adapter = PermissionsAdapter(requireContext(), state.permissions) { permissionId -> + requireActivity().supportFragmentManager.commit { + val bundle = bundleOf("PERMISSION_ID" to permissionId) + add<PermissionAppsFragment>(R.id.container, args = bundle) + setReorderingAllowed(true) + addToBackStack("permissions") + } + } + } + } + + override fun actions(): Flow<PermissionsFeature.Action> = viewModel.actions } diff --git a/app/src/main/java/foundation/e/privacycentralapp/features/permissions/PermissionsViewModel.kt b/app/src/main/java/foundation/e/privacycentralapp/features/permissions/PermissionsViewModel.kt new file mode 100644 index 0000000..fc50c39 --- /dev/null +++ b/app/src/main/java/foundation/e/privacycentralapp/features/permissions/PermissionsViewModel.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.permissions + +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import kotlinx.coroutines.flow.MutableSharedFlow +import kotlinx.coroutines.flow.asSharedFlow +import kotlinx.coroutines.launch + +class PermissionsViewModel : ViewModel() { + + private val _actions = MutableSharedFlow<PermissionsFeature.Action>() + val actions = _actions.asSharedFlow() + + val permissionsFeature: PermissionsFeature by lazy { + PermissionsFeature.create(coroutineScope = viewModelScope) + } + + fun submitAction(action: PermissionsFeature.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 42f9e24..1b92cb2 100644 --- a/app/src/main/java/foundation/e/privacycentralapp/main/MainActivity.kt +++ b/app/src/main/java/foundation/e/privacycentralapp/main/MainActivity.kt @@ -20,7 +20,6 @@ package foundation.e.privacycentralapp.main import android.app.Activity import android.content.Intent import android.os.Bundle -import androidx.activity.viewModels import androidx.fragment.app.FragmentActivity import androidx.fragment.app.add import androidx.fragment.app.commit @@ -29,8 +28,6 @@ import foundation.e.privacycentralapp.features.dashboard.DashboardFragment open class MainActivity : FragmentActivity(R.layout.activity_main) { - private val viewModel: MainViewModel by viewModels() - override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) if (savedInstanceState == null) { diff --git a/app/src/main/res/layout/fragment_permission_control.xml b/app/src/main/res/layout/fragment_permission_apps.xml index 2888af0..2888af0 100644 --- a/app/src/main/res/layout/fragment_permission_control.xml +++ b/app/src/main/res/layout/fragment_permission_apps.xml diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 989e233..ff0cf0a 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -23,11 +23,16 @@ <string name="add_location">Add location</string> <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="i_can_be_tracked">I can be tracked by my 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="i_am_anonymous">I am anonymous on the internet</string> + <string name="i_am_exposing">I am exposing my real IP address</string> <string name="permission_control_info">Manage and control apps requesting various permissions.</string> <string name="apps_allowed">%1$d of %2$d apps allowed</string> <string name="apps_access_to_permission">Apps which has access to %1$s permission</string> + + <string name="real_location_mode">Real location mode</string> + <string name="random_location_mode">Random location mode</string> + <string name="fake_location_mode">Fake location mode</string> </resources>
\ No newline at end of file |