diff options
author | Guillaume Jacquart <guillaume.jacquart@hoodbrains.com> | 2023-05-02 21:25:17 +0200 |
---|---|---|
committer | Guillaume Jacquart <guillaume.jacquart@hoodbrains.com> | 2023-05-02 22:00:35 +0200 |
commit | a8874167f663885f2d3371801cf03681576ac817 (patch) | |
tree | 5be07b8768142efeade536d4135f2250c1ac9071 /app/src/main/java/foundation/e/privacycentralapp | |
parent | a0ee04ea9dbc0802c828afdf660eb37dc6fa350f (diff) |
1200: rename everything to AdvancedPrivacy
Diffstat (limited to 'app/src/main/java/foundation/e/privacycentralapp')
56 files changed, 0 insertions, 6180 deletions
diff --git a/app/src/main/java/foundation/e/privacycentralapp/DependencyContainer.kt b/app/src/main/java/foundation/e/privacycentralapp/DependencyContainer.kt deleted file mode 100644 index aab81d5..0000000 --- a/app/src/main/java/foundation/e/privacycentralapp/DependencyContainer.kt +++ /dev/null @@ -1,211 +0,0 @@ -/* - * Copyright (C) 2023 MURENA SAS - * 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 - -import android.app.Application -import android.content.Context -import android.os.Process -import androidx.lifecycle.DEFAULT_ARGS_KEY -import androidx.lifecycle.ViewModel -import androidx.lifecycle.ViewModelProvider -import androidx.lifecycle.viewmodel.CreationExtras -import foundation.e.privacycentralapp.common.WarningDialog -import foundation.e.privacycentralapp.data.repositories.AppListsRepository -import foundation.e.privacycentralapp.data.repositories.LocalStateRepository -import foundation.e.privacycentralapp.data.repositories.TrackersRepository -import foundation.e.privacycentralapp.domain.usecases.AppListUseCase -import foundation.e.privacycentralapp.domain.usecases.FakeLocationStateUseCase -import foundation.e.privacycentralapp.domain.usecases.GetQuickPrivacyStateUseCase -import foundation.e.privacycentralapp.domain.usecases.IpScramblingStateUseCase -import foundation.e.privacycentralapp.domain.usecases.ShowFeaturesWarningUseCase -import foundation.e.privacycentralapp.domain.usecases.TrackersStateUseCase -import foundation.e.privacycentralapp.domain.usecases.TrackersStatisticsUseCase -import foundation.e.privacycentralapp.dummy.CityDataSource -import foundation.e.privacycentralapp.features.dashboard.DashboardViewModel -import foundation.e.privacycentralapp.features.internetprivacy.InternetPrivacyViewModel -import foundation.e.privacycentralapp.features.location.FakeLocationViewModel -import foundation.e.privacycentralapp.features.trackers.TrackersViewModel -import foundation.e.privacycentralapp.features.trackers.apptrackers.AppTrackersFragment -import foundation.e.privacycentralapp.features.trackers.apptrackers.AppTrackersViewModel -import foundation.e.privacymodules.fakelocation.FakeLocationModule -import foundation.e.privacymodules.ipscrambler.IpScramblerModule -import foundation.e.privacymodules.ipscramblermodule.IIpScramblerModule -import foundation.e.privacymodules.permissions.PermissionsPrivacyModule -import foundation.e.privacymodules.permissions.data.ApplicationDescription -import foundation.e.privacymodules.permissions.data.ProfileType -import foundation.e.privacymodules.trackers.api.BlockTrackersPrivacyModule -import foundation.e.privacymodules.trackers.api.TrackTrackersPrivacyModule -import kotlinx.coroutines.DelicateCoroutinesApi -import kotlinx.coroutines.GlobalScope - -/** - * Simple container to hold application wide dependencies. - * - */ -@OptIn(DelicateCoroutinesApi::class) -class DependencyContainer(val app: Application) { - val context: Context by lazy { app.applicationContext } - - // Drivers - private val fakeLocationModule: FakeLocationModule by lazy { FakeLocationModule(app.applicationContext) } - private val permissionsModule by lazy { PermissionsPrivacyModule(app.applicationContext) } - private val ipScramblerModule: IIpScramblerModule by lazy { IpScramblerModule(app.applicationContext) } - - private val appDesc by lazy { - ApplicationDescription( - packageName = context.packageName, - uid = Process.myUid(), - label = context.resources.getString(R.string.app_name), - icon = null, - profileId = -1, - profileType = ProfileType.MAIN - ) - } - - private val blockTrackersPrivacyModule by lazy { BlockTrackersPrivacyModule.getInstance(context) } - private val trackTrackersPrivacyModule by lazy { TrackTrackersPrivacyModule.getInstance(context) } - - // Repositories - private val localStateRepository by lazy { LocalStateRepository(context) } - private val trackersRepository by lazy { TrackersRepository(context) } - private val appListsRepository by lazy { AppListsRepository(permissionsModule, context, GlobalScope) } - - // Usecases - val getQuickPrivacyStateUseCase by lazy { - GetQuickPrivacyStateUseCase(localStateRepository) - } - private val ipScramblingStateUseCase by lazy { - IpScramblingStateUseCase( - ipScramblerModule, permissionsModule, appDesc, localStateRepository, - appListsRepository, GlobalScope - ) - } - private val appListUseCase = AppListUseCase(appListsRepository) - - val trackersStatisticsUseCase by lazy { - TrackersStatisticsUseCase(trackTrackersPrivacyModule, blockTrackersPrivacyModule, appListsRepository, context.resources) - } - - val trackersStateUseCase by lazy { - TrackersStateUseCase(blockTrackersPrivacyModule, trackTrackersPrivacyModule, localStateRepository, trackersRepository, appListsRepository, GlobalScope) - } - - private val fakeLocationStateUseCase by lazy { - FakeLocationStateUseCase( - fakeLocationModule, permissionsModule, localStateRepository, CityDataSource, appDesc, context, GlobalScope - ) - } - - val showFeaturesWarningUseCase by lazy { - ShowFeaturesWarningUseCase(localStateRepository = localStateRepository) - } - - val viewModelsFactory by lazy { - ViewModelsFactory( - getQuickPrivacyStateUseCase = getQuickPrivacyStateUseCase, - trackersStatisticsUseCase = trackersStatisticsUseCase, - trackersStateUseCase = trackersStateUseCase, - fakeLocationStateUseCase = fakeLocationStateUseCase, - ipScramblerModule = ipScramblerModule, - ipScramblingStateUseCase = ipScramblingStateUseCase, - appListUseCase = appListUseCase - ) - } - - // Background - fun initBackgroundSingletons() { - trackersStateUseCase - ipScramblingStateUseCase - fakeLocationStateUseCase - - UpdateTrackersWorker.periodicUpdate(context) - - WarningDialog.startListening( - showFeaturesWarningUseCase, - GlobalScope, - context - ) - - Widget.startListening( - context, - getQuickPrivacyStateUseCase, - trackersStatisticsUseCase, - ) - - Notifications.startListening( - context, - getQuickPrivacyStateUseCase, - permissionsModule, - GlobalScope - ) - } -} - -@Suppress("LongParameterList") -class ViewModelsFactory( - private val getQuickPrivacyStateUseCase: GetQuickPrivacyStateUseCase, - private val trackersStatisticsUseCase: TrackersStatisticsUseCase, - private val trackersStateUseCase: TrackersStateUseCase, - private val fakeLocationStateUseCase: FakeLocationStateUseCase, - private val ipScramblerModule: IIpScramblerModule, - private val ipScramblingStateUseCase: IpScramblingStateUseCase, - private val appListUseCase: AppListUseCase -) : ViewModelProvider.Factory { - - @Suppress("UNCHECKED_CAST") - override fun <T : ViewModel> create(modelClass: Class<T>, extras: CreationExtras): T { - return when (modelClass) { - AppTrackersViewModel::class.java -> { - val app = extras[DEFAULT_ARGS_KEY]?.getInt(AppTrackersFragment.PARAM_APP_UID)?.let { - appListUseCase.getApp(it) - } ?: appListUseCase.dummySystemApp - - AppTrackersViewModel( - app = app, - trackersStateUseCase = trackersStateUseCase, - trackersStatisticsUseCase = trackersStatisticsUseCase, - getQuickPrivacyStateUseCase = getQuickPrivacyStateUseCase - ) - } - - TrackersViewModel::class.java -> - TrackersViewModel( - trackersStatisticsUseCase = trackersStatisticsUseCase - ) - FakeLocationViewModel::class.java -> - FakeLocationViewModel( - getQuickPrivacyStateUseCase = getQuickPrivacyStateUseCase, - fakeLocationStateUseCase = fakeLocationStateUseCase - ) - InternetPrivacyViewModel::class.java -> - InternetPrivacyViewModel( - ipScramblerModule = ipScramblerModule, - getQuickPrivacyStateUseCase = getQuickPrivacyStateUseCase, - ipScramblingStateUseCase = ipScramblingStateUseCase, - appListUseCase = appListUseCase - ) - DashboardViewModel::class.java -> - DashboardViewModel( - getPrivacyStateUseCase = getQuickPrivacyStateUseCase, - trackersStatisticsUseCase = trackersStatisticsUseCase - ) - else -> throw IllegalArgumentException("Unknown class $modelClass") - } as T - } -} diff --git a/app/src/main/java/foundation/e/privacycentralapp/Notifications.kt b/app/src/main/java/foundation/e/privacycentralapp/Notifications.kt deleted file mode 100644 index 0df3e18..0000000 --- a/app/src/main/java/foundation/e/privacycentralapp/Notifications.kt +++ /dev/null @@ -1,210 +0,0 @@ -/* - * Copyright (C) 2022 MURENA SAS - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see <https://www.gnu.org/licenses/>. - */ - -package foundation.e.privacycentralapp - -import android.app.NotificationChannel -import android.app.NotificationManager -import android.app.PendingIntent -import android.content.Context -import android.content.Intent -import androidx.annotation.StringRes -import androidx.core.app.NotificationCompat -import androidx.core.app.NotificationManagerCompat -import foundation.e.privacycentralapp.domain.entities.InternetPrivacyMode -import foundation.e.privacycentralapp.domain.entities.MainFeatures -import foundation.e.privacycentralapp.domain.usecases.GetQuickPrivacyStateUseCase -import foundation.e.privacycentralapp.main.MainActivity -import foundation.e.privacymodules.permissions.PermissionsPrivacyModule -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.flow.distinctUntilChanged -import kotlinx.coroutines.flow.launchIn -import kotlinx.coroutines.flow.map -import kotlinx.coroutines.flow.onEach - -object Notifications { - const val CHANNEL_FIRST_BOOT = "first_boot_notification" - const val CHANNEL_FAKE_LOCATION_FLAG = "fake_location_flag" - const val CHANNEL_IPSCRAMBLING_FLAG = "ipscrambling_flag" - - const val NOTIFICATION_FIRST_BOOT = 1000 - const val NOTIFICATION_FAKE_LOCATION_FLAG = NOTIFICATION_FIRST_BOOT + 1 - const val NOTIFICATION_IPSCRAMBLING_FLAG = NOTIFICATION_FAKE_LOCATION_FLAG + 1 - - fun showFirstBootNotification(context: Context) { - createNotificationFirstBootChannel(context) - val notificationBuilder: NotificationCompat.Builder = notificationBuilder( - context, - NotificationContent( - channelId = CHANNEL_FIRST_BOOT, - icon = R.drawable.ic_notification_logo, - title = R.string.first_notification_title, - description = R.string.first_notification_summary, - destinationIntent = - context.packageManager.getLaunchIntentForPackage(context.packageName) - ) - ) - .setAutoCancel(true) - - NotificationManagerCompat.from(context).notify( - NOTIFICATION_FIRST_BOOT, notificationBuilder.build() - ) - } - - fun startListening( - appContext: Context, - getQuickPrivacyStateUseCase: GetQuickPrivacyStateUseCase, - permissionsPrivacyModule: PermissionsPrivacyModule, - appScope: CoroutineScope - ) { - createNotificationFlagChannel( - context = appContext, - permissionsPrivacyModule = permissionsPrivacyModule, - channelId = CHANNEL_FAKE_LOCATION_FLAG, - channelName = R.string.notifications_fake_location_channel_name, - channelDescription = R.string.notifications_fake_location_channel_description - ) - - createNotificationFlagChannel( - context = appContext, - permissionsPrivacyModule = permissionsPrivacyModule, - channelId = CHANNEL_IPSCRAMBLING_FLAG, - channelName = R.string.notifications_ipscrambling_channel_name, - channelDescription = R.string.notifications_ipscrambling_channel_description - ) - - getQuickPrivacyStateUseCase.isLocationHidden.onEach { - if (it) { - showFlagNotification(appContext, MainFeatures.FAKE_LOCATION) - } else { - hideFlagNotification(appContext, MainFeatures.FAKE_LOCATION) - } - }.launchIn(appScope) - - getQuickPrivacyStateUseCase.ipScramblingMode.map { - it != InternetPrivacyMode.REAL_IP - }.distinctUntilChanged().onEach { - if (it) { - showFlagNotification(appContext, MainFeatures.IP_SCRAMBLING) - } else { - hideFlagNotification(appContext, MainFeatures.IP_SCRAMBLING) - } - }.launchIn(appScope) - } - - private fun createNotificationFirstBootChannel(context: Context) { - val channel = NotificationChannel( - CHANNEL_FIRST_BOOT, - context.getString(R.string.notifications_first_boot_channel_name), - NotificationManager.IMPORTANCE_HIGH - ) - NotificationManagerCompat.from(context).createNotificationChannel(channel) - } - - private fun createNotificationFlagChannel( - context: Context, - permissionsPrivacyModule: PermissionsPrivacyModule, - channelId: String, - @StringRes channelName: Int, - @StringRes channelDescription: Int, - ) { - val channel = NotificationChannel( - channelId, context.getString(channelName), NotificationManager.IMPORTANCE_LOW - ) - channel.description = context.getString(channelDescription) - permissionsPrivacyModule.setBlockable(channel) - NotificationManagerCompat.from(context).createNotificationChannel(channel) - } - - private fun showFlagNotification(context: Context, feature: MainFeatures) { - when (feature) { - MainFeatures.FAKE_LOCATION -> showFlagNotification( - context = context, - id = NOTIFICATION_FAKE_LOCATION_FLAG, - content = NotificationContent( - channelId = CHANNEL_FAKE_LOCATION_FLAG, - icon = R.drawable.ic_fmd_bad, - title = R.string.notifications_fake_location_title, - description = R.string.notifications_fake_location_content, - destinationIntent = MainActivity.createFakeLocationIntent(context), - ) - ) - MainFeatures.IP_SCRAMBLING -> showFlagNotification( - context = context, - id = NOTIFICATION_IPSCRAMBLING_FLAG, - content = NotificationContent( - channelId = CHANNEL_IPSCRAMBLING_FLAG, - icon = R.drawable.ic_language, - title = R.string.notifications_ipscrambling_title, - description = R.string.notifications_ipscrambling_content, - destinationIntent = MainActivity.createIpScramblingIntent(context), - ) - ) - else -> {} - } - } - - private fun showFlagNotification( - context: Context, - id: Int, - content: NotificationContent, - ) { - val builder = notificationBuilder(context, content) - .setPriority(NotificationCompat.PRIORITY_LOW) - .setOngoing(true) - - NotificationManagerCompat.from(context).notify(id, builder.build()) - } - - private fun hideFlagNotification(context: Context, feature: MainFeatures) { - val id = when (feature) { - MainFeatures.FAKE_LOCATION -> NOTIFICATION_FAKE_LOCATION_FLAG - MainFeatures.IP_SCRAMBLING -> NOTIFICATION_IPSCRAMBLING_FLAG - else -> return - } - NotificationManagerCompat.from(context).cancel(id) - } - - private data class NotificationContent( - val channelId: String, - val icon: Int, - val title: Int, - val description: Int, - val destinationIntent: Intent? - ) - - private fun notificationBuilder( - context: Context, - content: NotificationContent - ): NotificationCompat.Builder { - val builder = NotificationCompat.Builder(context, content.channelId) - .setSmallIcon(content.icon) - .setPriority(NotificationCompat.PRIORITY_LOW) - .setContentTitle(context.getString(content.title)) - .setStyle(NotificationCompat.BigTextStyle().bigText(context.getString(content.description))) - - content.destinationIntent?.let { - it.flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK - val pendingIntent: PendingIntent = PendingIntent.getActivity( - context, 0, it, PendingIntent.FLAG_IMMUTABLE - ) - builder.setContentIntent(pendingIntent) - } - - return builder - } -} diff --git a/app/src/main/java/foundation/e/privacycentralapp/PrivacyCentralApplication.kt b/app/src/main/java/foundation/e/privacycentralapp/PrivacyCentralApplication.kt deleted file mode 100644 index 2f718b5..0000000 --- a/app/src/main/java/foundation/e/privacycentralapp/PrivacyCentralApplication.kt +++ /dev/null @@ -1,36 +0,0 @@ -/* - * Copyright (C) 2021 E FOUNDATION, 2022 - 2023 MURENA SAS - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see <https://www.gnu.org/licenses/>. - */ - -package foundation.e.privacycentralapp - -import android.app.Application -import com.mapbox.mapboxsdk.Mapbox -import foundation.e.lib.telemetry.Telemetry - -class PrivacyCentralApplication : Application() { - - // Initialize the dependency container. - val dependencyContainer: DependencyContainer by lazy { DependencyContainer(this) } - - override fun onCreate() { - super.onCreate() - Telemetry.init(BuildConfig.SENTRY_DSN, this, true) - Mapbox.getTelemetry()?.setUserTelemetryRequestState(false) - - dependencyContainer.initBackgroundSingletons() - } -} diff --git a/app/src/main/java/foundation/e/privacycentralapp/UpdateTrackersWorker.kt b/app/src/main/java/foundation/e/privacycentralapp/UpdateTrackersWorker.kt deleted file mode 100644 index 13511da..0000000 --- a/app/src/main/java/foundation/e/privacycentralapp/UpdateTrackersWorker.kt +++ /dev/null @@ -1,59 +0,0 @@ -/* - * Copyright (C) 2022 E FOUNDATION - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see <https://www.gnu.org/licenses/>. - */ - -package foundation.e.privacycentralapp - -import android.content.Context -import androidx.work.Constraints -import androidx.work.CoroutineWorker -import androidx.work.ExistingPeriodicWorkPolicy -import androidx.work.NetworkType -import androidx.work.PeriodicWorkRequestBuilder -import androidx.work.WorkManager -import androidx.work.WorkerParameters -import java.util.concurrent.TimeUnit - -class UpdateTrackersWorker(appContext: Context, workerParams: WorkerParameters) : - CoroutineWorker(appContext, workerParams) { - - override suspend fun doWork(): Result { - val trackersStateUseCase = (applicationContext as PrivacyCentralApplication) - .dependencyContainer.trackersStateUseCase - - trackersStateUseCase.updateTrackers() - return Result.success() - } - - companion object { - private val constraints = Constraints.Builder() - .setRequiredNetworkType(NetworkType.CONNECTED) - .build() - - fun periodicUpdate(context: Context) { - val request = PeriodicWorkRequestBuilder<UpdateTrackersWorker>( - 7, TimeUnit.DAYS - ) - .setConstraints(constraints).build() - - WorkManager.getInstance(context).enqueueUniquePeriodicWork( - UpdateTrackersWorker::class.qualifiedName ?: "", - ExistingPeriodicWorkPolicy.KEEP, - request - ) - } - } -} diff --git a/app/src/main/java/foundation/e/privacycentralapp/common/AppsAdapter.kt b/app/src/main/java/foundation/e/privacycentralapp/common/AppsAdapter.kt deleted file mode 100644 index 2fbbc34..0000000 --- a/app/src/main/java/foundation/e/privacycentralapp/common/AppsAdapter.kt +++ /dev/null @@ -1,71 +0,0 @@ -/* - * Copyright (C) 2021 E FOUNDATION, 2022 - 2023 MURENA SAS - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see <https://www.gnu.org/licenses/>. - */ - -package foundation.e.privacycentralapp.common - -import android.view.LayoutInflater -import android.view.View -import android.view.ViewGroup -import android.widget.ImageView -import android.widget.TextView -import androidx.recyclerview.widget.RecyclerView -import foundation.e.privacycentralapp.R -import foundation.e.privacycentralapp.domain.entities.AppWithCounts - -class AppsAdapter( - private val itemsLayout: Int, - private val listener: (Int) -> Unit -) : - RecyclerView.Adapter<AppsAdapter.ViewHolder>() { - - class ViewHolder(view: View, private val listener: (Int) -> Unit) : RecyclerView.ViewHolder(view) { - val appName: TextView = view.findViewById(R.id.title) - val counts: TextView = view.findViewById(R.id.counts) - val icon: ImageView = view.findViewById(R.id.icon) - fun bind(item: AppWithCounts) { - appName.text = item.label - counts.text = if (item.trackersCount > 0) itemView.context.getString( - R.string.trackers_app_trackers_counts, - item.blockedTrackersCount, - item.trackersCount, - item.leaks - ) else "" - icon.setImageDrawable(item.icon) - - itemView.setOnClickListener { listener(item.uid) } - } - } - - var dataSet: List<AppWithCounts> = emptyList() - set(value) { - field = value - notifyDataSetChanged() - } - - override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder { - val view = LayoutInflater.from(parent.context) - .inflate(itemsLayout, parent, false) - return ViewHolder(view, listener) - } - - override fun onBindViewHolder(holder: ViewHolder, position: Int) { - val app = dataSet[position] - holder.bind(app) - } - - override fun getItemCount(): Int = dataSet.size -} diff --git a/app/src/main/java/foundation/e/privacycentralapp/common/BootCompletedReceiver.kt b/app/src/main/java/foundation/e/privacycentralapp/common/BootCompletedReceiver.kt deleted file mode 100644 index d7902ee..0000000 --- a/app/src/main/java/foundation/e/privacycentralapp/common/BootCompletedReceiver.kt +++ /dev/null @@ -1,36 +0,0 @@ -/* - * Copyright (C) 2022 E FOUNDATION - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see <https://www.gnu.org/licenses/>. - */ - -package foundation.e.privacycentralapp.common - -import android.content.BroadcastReceiver -import android.content.Context -import android.content.Intent -import foundation.e.privacycentralapp.Notifications -import foundation.e.privacycentralapp.data.repositories.LocalStateRepository - -class BootCompletedReceiver : BroadcastReceiver() { - override fun onReceive(context: Context, intent: Intent?) { - if (intent?.action == Intent.ACTION_BOOT_COMPLETED) { - val localStateRepository = LocalStateRepository(context) - if (localStateRepository.firstBoot) { - Notifications.showFirstBootNotification(context) - localStateRepository.firstBoot = false - } - } - } -} diff --git a/app/src/main/java/foundation/e/privacycentralapp/common/Factory.kt b/app/src/main/java/foundation/e/privacycentralapp/common/Factory.kt deleted file mode 100644 index 4c7f436..0000000 --- a/app/src/main/java/foundation/e/privacycentralapp/common/Factory.kt +++ /dev/null @@ -1,23 +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.common - -// Definition of a Factory interface with a function to create objects of a type -interface Factory<T> { - fun create(): T -} diff --git a/app/src/main/java/foundation/e/privacycentralapp/common/GraphHolder.kt b/app/src/main/java/foundation/e/privacycentralapp/common/GraphHolder.kt deleted file mode 100644 index a25b68e..0000000 --- a/app/src/main/java/foundation/e/privacycentralapp/common/GraphHolder.kt +++ /dev/null @@ -1,333 +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.common - -import android.content.Context -import android.graphics.Canvas -import android.text.Spannable -import android.text.SpannableStringBuilder -import android.text.style.DynamicDrawableSpan -import android.text.style.ImageSpan -import android.view.View -import android.widget.TextView -import androidx.core.content.ContextCompat -import androidx.core.text.toSpannable -import androidx.core.view.isVisible -import com.github.mikephil.charting.charts.BarChart -import com.github.mikephil.charting.components.AxisBase -import com.github.mikephil.charting.components.MarkerView -import com.github.mikephil.charting.components.XAxis -import com.github.mikephil.charting.components.YAxis -import com.github.mikephil.charting.components.YAxis.AxisDependency -import com.github.mikephil.charting.data.BarData -import com.github.mikephil.charting.data.BarDataSet -import com.github.mikephil.charting.data.BarEntry -import com.github.mikephil.charting.data.Entry -import com.github.mikephil.charting.formatter.ValueFormatter -import com.github.mikephil.charting.highlight.Highlight -import com.github.mikephil.charting.listener.OnChartValueSelectedListener -import com.github.mikephil.charting.renderer.XAxisRenderer -import com.github.mikephil.charting.utils.MPPointF -import foundation.e.privacycentralapp.R -import foundation.e.privacycentralapp.common.extensions.dpToPxF -import kotlin.math.floor - -class GraphHolder(val barChart: BarChart, val context: Context, val isMarkerAbove: Boolean = true) { - var data = emptyList<Pair<Int, Int>>() - set(value) { - field = value - refreshDataSet() - } - var labels = emptyList<String>() - - var graduations: List<String?>? = null - - private var isHighlighted = false - - init { - barChart.description = null - barChart.setTouchEnabled(true) - barChart.setScaleEnabled(false) - - barChart.setDrawGridBackground(false) - barChart.setDrawBorders(false) - barChart.axisLeft.isEnabled = false - barChart.axisRight.isEnabled = false - - barChart.legend.isEnabled = false - - if (isMarkerAbove) prepareXAxisDashboardDay() else prepareXAxisMarkersBelow() - - val periodMarker = PeriodMarkerView(context, isMarkerAbove) - periodMarker.chartView = barChart - barChart.marker = periodMarker - - barChart.setOnChartValueSelectedListener(object : OnChartValueSelectedListener { - override fun onValueSelected(e: Entry?, h: Highlight?) { - h?.let { - val index = it.x.toInt() - if (index >= 0 && - index < labels.size && - index < this@GraphHolder.data.size - ) { - val period = labels[index] - val (blocked, leaked) = this@GraphHolder.data[index] - periodMarker.setLabel(period, blocked, leaked) - } - } - isHighlighted = true - } - - override fun onNothingSelected() { - isHighlighted = false - } - }) - } - - private fun prepareXAxisDashboardDay() { - barChart.extraTopOffset = 44f - - barChart.offsetTopAndBottom(0) - - barChart.setXAxisRenderer(object : XAxisRenderer(barChart.viewPortHandler, barChart.xAxis, barChart.getTransformer(AxisDependency.LEFT)) { - override fun renderAxisLine(c: Canvas) { - mAxisLinePaint.color = mXAxis.axisLineColor - mAxisLinePaint.strokeWidth = mXAxis.axisLineWidth - mAxisLinePaint.pathEffect = mXAxis.axisLineDashPathEffect - - // Top line - c.drawLine( - mViewPortHandler.contentLeft(), - mViewPortHandler.contentTop(), mViewPortHandler.contentRight(), - mViewPortHandler.contentTop(), mAxisLinePaint - ) - - // Bottom line - c.drawLine( - mViewPortHandler.contentLeft(), - mViewPortHandler.contentBottom() - 7.dpToPxF(context), - mViewPortHandler.contentRight(), - mViewPortHandler.contentBottom() - 7.dpToPxF(context), - mAxisLinePaint - ) - } - - override fun renderGridLines(c: Canvas) { - if (!mXAxis.isDrawGridLinesEnabled || !mXAxis.isEnabled) return - val clipRestoreCount = c.save() - c.clipRect(gridClippingRect) - if (mRenderGridLinesBuffer.size != mAxis.mEntryCount * 2) { - mRenderGridLinesBuffer = FloatArray(mXAxis.mEntryCount * 2) - } - val positions = mRenderGridLinesBuffer - run { - var i = 0 - while (i < positions.size) { - positions[i] = mXAxis.mEntries[i / 2] - positions[i + 1] = mXAxis.mEntries[i / 2] - i += 2 - } - } - - mTrans.pointValuesToPixel(positions) - setupGridPaint() - val gridLinePath = mRenderGridLinesPath - gridLinePath.reset() - var i = 0 - while (i < positions.size) { - val bottomY = if (graduations?.getOrNull(i / 2) != null) 0 else 3 - val x = positions[i] - gridLinePath.moveTo(x, mViewPortHandler.contentBottom() - 7.dpToPxF(context)) - gridLinePath.lineTo(x, mViewPortHandler.contentBottom() - bottomY.dpToPxF(context)) - - c.drawPath(gridLinePath, mGridPaint) - - gridLinePath.reset() - - i += 2 - } - c.restoreToCount(clipRestoreCount) - } - }) - - barChart.setDrawValueAboveBar(false) - barChart.xAxis.apply { - isEnabled = true - position = XAxis.XAxisPosition.BOTTOM - - setDrawGridLines(true) - setDrawLabels(true) - setCenterAxisLabels(false) - setLabelCount(25, true) - textColor = context.getColor(R.color.primary_text) - valueFormatter = object : ValueFormatter() { - override fun getAxisLabel(value: Float, axis: AxisBase?): String { - return graduations?.getOrNull(floor(value).toInt() + 1) ?: "" - } - } - } - } - - private fun prepareXAxisMarkersBelow() { - barChart.extraBottomOffset = 44f - - barChart.offsetTopAndBottom(0) - barChart.setDrawValueAboveBar(false) - - barChart.xAxis.apply { - isEnabled = true - position = XAxis.XAxisPosition.BOTH_SIDED - setDrawGridLines(false) - setDrawLabels(false) - } - } - - fun highlightIndex(index: Int) { - if (index >= 0 && index < data.size) { - val xPx = barChart.getTransformer(YAxis.AxisDependency.LEFT) - .getPixelForValues(index.toFloat(), 0f) - .x - val highlight = Highlight( - index.toFloat(), 0f, - xPx.toFloat(), 0f, - 0, YAxis.AxisDependency.LEFT - ) - - barChart.highlightValue(highlight, true) - } - } - - private fun refreshDataSet() { - val trackersDataSet = BarDataSet( - data.mapIndexed { index, value -> - BarEntry( - index.toFloat(), - floatArrayOf(value.first.toFloat(), value.second.toFloat()) - ) - }, - "" - ).apply { - - val blockedColor = ContextCompat.getColor(context, R.color.accent) - val leakedColor = ContextCompat.getColor(context, R.color.red_off) - - colors = listOf( - blockedColor, - leakedColor - ) - - setDrawValues(false) - } - - barChart.data = BarData(trackersDataSet) - barChart.invalidate() - } -} - -class PeriodMarkerView(context: Context, private val isMarkerAbove: Boolean = true) : MarkerView(context, R.layout.chart_tooltip) { - enum class ArrowPosition { LEFT, CENTER, RIGHT } - - private val arrowMargins = 10.dpToPxF(context) - private val mOffset2 = MPPointF(0f, 0f) - - private fun getArrowPosition(posX: Float): ArrowPosition { - val halfWidth = width / 2 - - return chartView?.let { chart -> - if (posX < halfWidth) { - ArrowPosition.LEFT - } else if (chart.width - posX < halfWidth) { - ArrowPosition.RIGHT - } else { - ArrowPosition.CENTER - } - } ?: ArrowPosition.CENTER - } - - private fun showArrow(position: ArrowPosition?) { - val ids = listOf( - R.id.arrow_top_left, R.id.arrow_top_center, R.id.arrow_top_right, - R.id.arrow_bottom_left, R.id.arrow_bottom_center, R.id.arrow_bottom_right - ) - - val toShow = if (isMarkerAbove) when (position) { - ArrowPosition.LEFT -> R.id.arrow_bottom_left - ArrowPosition.CENTER -> R.id.arrow_bottom_center - ArrowPosition.RIGHT -> R.id.arrow_bottom_right - else -> null - } else when (position) { - ArrowPosition.LEFT -> R.id.arrow_top_left - ArrowPosition.CENTER -> R.id.arrow_top_center - ArrowPosition.RIGHT -> R.id.arrow_top_right - else -> null - } - - ids.forEach { id -> - val showIt = id == toShow - findViewById<View>(id)?.let { - if (it.isVisible != showIt) { - it.isVisible = showIt - } - } - } - } - - fun setLabel(period: String, blocked: Int, leaked: Int) { - val span = SpannableStringBuilder(period) - span.append(": $blocked ") - span.setSpan( - ImageSpan(context, R.drawable.ic_legend_blocked, DynamicDrawableSpan.ALIGN_BASELINE), - span.length - 1, - span.length, - Spannable.SPAN_EXCLUSIVE_EXCLUSIVE - ) - span.append(" $leaked ") - span.setSpan( - ImageSpan(context, R.drawable.ic_legend_leaked, DynamicDrawableSpan.ALIGN_BASELINE), - span.length - 1, - span.length, - Spannable.SPAN_EXCLUSIVE_EXCLUSIVE - ) - findViewById<TextView>(R.id.label).text = span.toSpannable() - } - - override fun refreshContent(e: Entry?, highlight: Highlight?) { - highlight?.let { - showArrow(getArrowPosition(highlight.xPx)) - } - super.refreshContent(e, highlight) - } - - override fun getOffsetForDrawingAtPoint(posX: Float, posY: Float): MPPointF { - val x = when (getArrowPosition(posX)) { - ArrowPosition.LEFT -> -arrowMargins - ArrowPosition.RIGHT -> -width + arrowMargins - ArrowPosition.CENTER -> -width.toFloat() / 2 - } - - mOffset2.x = x - mOffset2.y = if (isMarkerAbove) -posY - else -posY + (chartView?.height?.toFloat() ?: 0f) - height - - return mOffset2 - } - - override fun draw(canvas: Canvas?, posX: Float, posY: Float) { - super.draw(canvas, posX, posY) - } -} diff --git a/app/src/main/java/foundation/e/privacycentralapp/common/NavToolbarFragment.kt b/app/src/main/java/foundation/e/privacycentralapp/common/NavToolbarFragment.kt deleted file mode 100644 index 6955405..0000000 --- a/app/src/main/java/foundation/e/privacycentralapp/common/NavToolbarFragment.kt +++ /dev/null @@ -1,33 +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.common - -import androidx.annotation.LayoutRes -import com.google.android.material.appbar.MaterialToolbar - -abstract class NavToolbarFragment(@LayoutRes contentLayoutId: Int) : ToolbarFragment(contentLayoutId) { - - override fun setupToolbar(toolbar: MaterialToolbar) { - super.setupToolbar(toolbar) - toolbar.apply { - setNavigationOnClickListener { - requireActivity().onBackPressed() - } - } - } -} diff --git a/app/src/main/java/foundation/e/privacycentralapp/common/RightRadioButton.kt b/app/src/main/java/foundation/e/privacycentralapp/common/RightRadioButton.kt deleted file mode 100644 index bbc108b..0000000 --- a/app/src/main/java/foundation/e/privacycentralapp/common/RightRadioButton.kt +++ /dev/null @@ -1,43 +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.common - -import android.annotation.SuppressLint -import android.content.Context -import android.util.AttributeSet -import android.widget.RadioButton - -/** - * A custom [RadioButton] which displays the radio drawable on the right side. - */ -@SuppressLint("AppCompatCustomView") -class RightRadioButton : RadioButton { - - constructor(context: Context) : super(context) - constructor(context: Context, attrs: AttributeSet?) : super(context, attrs) - constructor(context: Context, attrs: AttributeSet?, defStyleAttr: Int) : super( - context, - attrs, - defStyleAttr - ) - - // Returns layout direction as right-to-left to draw the compound button on right side. - override fun getLayoutDirection(): Int { - return LAYOUT_DIRECTION_RTL - } -} diff --git a/app/src/main/java/foundation/e/privacycentralapp/common/TextViewHelpers.kt b/app/src/main/java/foundation/e/privacycentralapp/common/TextViewHelpers.kt deleted file mode 100644 index d85f4a7..0000000 --- a/app/src/main/java/foundation/e/privacycentralapp/common/TextViewHelpers.kt +++ /dev/null @@ -1,63 +0,0 @@ -/* - * Copyright (C) 2022 E FOUNDATION - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see <https://www.gnu.org/licenses/>. - */ - -package foundation.e.privacycentralapp.common - -import android.content.Context -import android.content.res.ColorStateList -import android.text.Spannable -import android.text.SpannableString -import android.text.style.DynamicDrawableSpan -import android.text.style.ImageSpan -import android.widget.TextView -import androidx.annotation.StringRes -import androidx.appcompat.content.res.AppCompatResources -import androidx.appcompat.widget.TooltipCompat -import foundation.e.privacycentralapp.R - -fun setToolTipForAsterisk( - textView: TextView, - @StringRes textId: Int, - @StringRes tooltipTextId: Int -) { - textView.text = asteriskAsInfoIconSpannable(textView.context, textId, textView.textColors) - TooltipCompat.setTooltipText(textView, textView.context.getString(tooltipTextId)) - - textView.setOnClickListener { it.performLongClick() } -} - -private fun asteriskAsInfoIconSpannable( - context: Context, - @StringRes textId: Int, - tint: ColorStateList -): Spannable { - val spannable = SpannableString(context.getString(textId)) - val index = spannable.lastIndexOf("*") - if (index != -1) { - AppCompatResources.getDrawable(context, R.drawable.ic_info_16dp)?.let { - it.setTintList(tint) - it.setBounds(0, 0, it.intrinsicWidth, it.intrinsicHeight) - spannable.setSpan( - ImageSpan(it, DynamicDrawableSpan.ALIGN_CENTER), - index, - index + 1, - Spannable.SPAN_INCLUSIVE_INCLUSIVE - ) - } - } - return spannable -} diff --git a/app/src/main/java/foundation/e/privacycentralapp/common/ThrottleFlow.kt b/app/src/main/java/foundation/e/privacycentralapp/common/ThrottleFlow.kt deleted file mode 100644 index 21e1542..0000000 --- a/app/src/main/java/foundation/e/privacycentralapp/common/ThrottleFlow.kt +++ /dev/null @@ -1,36 +0,0 @@ -/* - * Copyright (C) 2022 E FOUNDATION - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see <https://www.gnu.org/licenses/>. - */ - -package foundation.e.privacycentralapp.common - -import kotlinx.coroutines.FlowPreview -import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.flow -import kotlin.time.Duration - -@FlowPreview -fun <T> Flow<T>.throttleFirst(windowDuration: Duration): Flow<T> = flow { - var lastEmissionTime = 0L - collect { upstream -> - val currentTime = System.currentTimeMillis() - val mayEmit = currentTime - lastEmissionTime > windowDuration.inWholeMilliseconds - if (mayEmit) { - lastEmissionTime = currentTime - emit(upstream) - } - } -} diff --git a/app/src/main/java/foundation/e/privacycentralapp/common/ToggleAppsAdapter.kt b/app/src/main/java/foundation/e/privacycentralapp/common/ToggleAppsAdapter.kt deleted file mode 100644 index c41c0cf..0000000 --- a/app/src/main/java/foundation/e/privacycentralapp/common/ToggleAppsAdapter.kt +++ /dev/null @@ -1,76 +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.common - -import android.view.LayoutInflater -import android.view.View -import android.view.ViewGroup -import android.widget.CheckBox -import android.widget.ImageView -import android.widget.TextView -import androidx.recyclerview.widget.RecyclerView -import foundation.e.privacycentralapp.R -import foundation.e.privacymodules.permissions.data.ApplicationDescription - -class ToggleAppsAdapter( - private val itemsLayout: Int, - private val listener: (String) -> Unit -) : - RecyclerView.Adapter<ToggleAppsAdapter.ViewHolder>() { - - class ViewHolder(view: View, private val listener: (String) -> Unit) : RecyclerView.ViewHolder(view) { - val appName: TextView = view.findViewById(R.id.title) - - val togglePermission: CheckBox = view.findViewById(R.id.toggle) - - fun bind(item: Pair<ApplicationDescription, Boolean>, isEnabled: Boolean) { - appName.text = item.first.label - togglePermission.isChecked = item.second - togglePermission.isEnabled = isEnabled - - itemView.findViewById<ImageView>(R.id.icon).setImageDrawable(item.first.icon) - togglePermission.setOnClickListener { listener(item.first.packageName) } - } - } - - var dataSet: List<Pair<ApplicationDescription, Boolean>> = emptyList() - set(value) { - field = value - notifyDataSetChanged() - } - - var isEnabled: Boolean = true - - fun setData(list: List<Pair<ApplicationDescription, Boolean>>, isEnabled: Boolean = true) { - this.isEnabled = isEnabled - dataSet = list - } - - override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder { - val view = LayoutInflater.from(parent.context) - .inflate(itemsLayout, parent, false) - return ViewHolder(view, listener) - } - - override fun onBindViewHolder(holder: ViewHolder, position: Int) { - val permission = dataSet[position] - holder.bind(permission, isEnabled) - } - - override fun getItemCount(): Int = dataSet.size -} diff --git a/app/src/main/java/foundation/e/privacycentralapp/common/ToolbarFragment.kt b/app/src/main/java/foundation/e/privacycentralapp/common/ToolbarFragment.kt deleted file mode 100644 index 5c18548..0000000 --- a/app/src/main/java/foundation/e/privacycentralapp/common/ToolbarFragment.kt +++ /dev/null @@ -1,45 +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.common - -import android.os.Bundle -import android.view.View -import androidx.annotation.LayoutRes -import androidx.fragment.app.Fragment -import com.google.android.material.appbar.MaterialToolbar -import foundation.e.privacycentralapp.R - -abstract class ToolbarFragment(@LayoutRes contentLayoutId: Int) : Fragment(contentLayoutId) { - - /** - * @return title to be used in toolbar - */ - abstract fun getTitle(): String - - override fun onViewCreated(view: View, savedInstanceState: Bundle?) { - super.onViewCreated(view, savedInstanceState) - - setupToolbar(view.findViewById(R.id.toolbar)) - } - - open fun setupToolbar(toolbar: MaterialToolbar) { - toolbar.title = getTitle() - } - - fun getToolbar(): MaterialToolbar? = view?.findViewById(R.id.toolbar) -} diff --git a/app/src/main/java/foundation/e/privacycentralapp/common/WarningDialog.kt b/app/src/main/java/foundation/e/privacycentralapp/common/WarningDialog.kt deleted file mode 100644 index cbbeffa..0000000 --- a/app/src/main/java/foundation/e/privacycentralapp/common/WarningDialog.kt +++ /dev/null @@ -1,130 +0,0 @@ -/* - * Copyright (C) 2022 E FOUNDATION - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see <https://www.gnu.org/licenses/>. - */ - -package foundation.e.privacycentralapp.common - -import android.app.Activity -import android.content.Context -import android.content.Intent -import android.graphics.drawable.ColorDrawable -import android.os.Bundle -import android.util.Log -import android.view.View -import android.widget.CheckBox -import androidx.appcompat.app.AlertDialog -import foundation.e.privacycentralapp.PrivacyCentralApplication -import foundation.e.privacycentralapp.R -import foundation.e.privacycentralapp.domain.entities.MainFeatures -import foundation.e.privacycentralapp.domain.entities.MainFeatures.FAKE_LOCATION -import foundation.e.privacycentralapp.domain.entities.MainFeatures.IP_SCRAMBLING -import foundation.e.privacycentralapp.domain.entities.MainFeatures.TRACKERS_CONTROL -import foundation.e.privacycentralapp.domain.usecases.ShowFeaturesWarningUseCase -import foundation.e.privacycentralapp.main.MainActivity -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.flow.launchIn -import kotlinx.coroutines.flow.map - -class WarningDialog : Activity() { - companion object { - private const val PARAM_FEATURE = "feature" - - fun startListening( - showFeaturesWarningUseCase: ShowFeaturesWarningUseCase, - appScope: CoroutineScope, - appContext: Context - ) { - showFeaturesWarningUseCase.showWarning().map { feature -> - appContext.startActivity( - createIntent(context = appContext, feature = feature) - ) - }.launchIn(appScope) - } - - private fun createIntent( - context: Context, - feature: MainFeatures, - ): Intent { - val intent = Intent(context, WarningDialog::class.java) - intent.putExtra(PARAM_FEATURE, feature.name) - intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK - return intent - } - } - - override fun onCreate(savedInstanceState: Bundle?) { - super.onCreate(savedInstanceState) - getWindow().setBackgroundDrawable(ColorDrawable(0)) - - val feature = try { - MainFeatures.valueOf(intent.getStringExtra(PARAM_FEATURE) ?: "") - } catch (e: Exception) { - Log.e("WarningDialog", "Missing mandatory activity parameter", e) - finish() - return - } - - showWarningDialog(feature) - } - - private fun showWarningDialog(feature: MainFeatures) { - val builder = AlertDialog.Builder(this) - builder.setOnDismissListener { finish() } - - val content: View = layoutInflater.inflate(R.layout.alertdialog_do_not_show_again, null) - val checkbox = content.findViewById<CheckBox>(R.id.checkbox) - builder.setView(content) - - builder.setMessage( - when (feature) { - TRACKERS_CONTROL -> R.string.warningdialog_trackers_message - FAKE_LOCATION -> R.string.warningdialog_location_message - IP_SCRAMBLING -> R.string.warningdialog_ipscrambling_message - } - ) - - builder.setTitle( - when (feature) { - TRACKERS_CONTROL -> R.string.warningdialog_trackers_title - FAKE_LOCATION -> R.string.warningdialog_location_title - IP_SCRAMBLING -> R.string.warningdialog_ipscrambling_title - } - ) - - builder.setPositiveButton( - when (feature) { - IP_SCRAMBLING -> R.string.warningdialog_ipscrambling_cta - else -> R.string.ok - } - ) { _, _ -> - if (checkbox.isChecked()) { - (application as PrivacyCentralApplication) - .dependencyContainer.showFeaturesWarningUseCase - .doNotShowAgain(feature) - } - finish() - } - - if (feature == TRACKERS_CONTROL) { - builder.setNeutralButton(R.string.warningdialog_trackers_secondary_cta) { _, _ -> - startActivity(MainActivity.createTrackersIntent(this)) - finish() - } - } - - builder.show() - } -} diff --git a/app/src/main/java/foundation/e/privacycentralapp/common/extensions/AnyExtension.kt b/app/src/main/java/foundation/e/privacycentralapp/common/extensions/AnyExtension.kt deleted file mode 100644 index 71de99a..0000000 --- a/app/src/main/java/foundation/e/privacycentralapp/common/extensions/AnyExtension.kt +++ /dev/null @@ -1,22 +0,0 @@ -/* - * Copyright (C) 2022 E FOUNDATION - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see <https://www.gnu.org/licenses/>. - */ - -package foundation.e.privacycentralapp.common.extensions - -import android.content.Context - -fun Int.dpToPxF(context: Context): Float = this.toFloat() * context.resources.displayMetrics.density diff --git a/app/src/main/java/foundation/e/privacycentralapp/data/repositories/AppListsRepository.kt b/app/src/main/java/foundation/e/privacycentralapp/data/repositories/AppListsRepository.kt deleted file mode 100644 index a4f7487..0000000 --- a/app/src/main/java/foundation/e/privacycentralapp/data/repositories/AppListsRepository.kt +++ /dev/null @@ -1,281 +0,0 @@ -/* - * Copyright (C) 2022 E FOUNDATION, 2022 - 2023 MURENA SAS - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see <https://www.gnu.org/licenses/>. - */ - -package foundation.e.privacycentralapp.data.repositories - -import android.Manifest -import android.content.Context -import android.content.Intent -import android.content.pm.ApplicationInfo -import android.content.pm.PackageInfo -import foundation.e.privacycentralapp.R -import foundation.e.privacymodules.permissions.PermissionsPrivacyModule -import foundation.e.privacymodules.permissions.data.ApplicationDescription -import foundation.e.privacymodules.permissions.data.ProfileType -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.Job -import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.map -import kotlinx.coroutines.launch -import kotlinx.coroutines.runBlocking - -class AppListsRepository( - private val permissionsModule: PermissionsPrivacyModule, - private val context: Context, - private val coroutineScope: CoroutineScope -) { - companion object { - private const val PNAME_SETTINGS = "com.android.settings" - private const val PNAME_PWAPLAYER = "foundation.e.pwaplayer" - private const val PNAME_INTENT_VERIFICATION = "com.android.statementservice" - private const val PNAME_MICROG_SERVICES_CORE = "com.google.android.gms" - - val compatibiltyPNames = setOf( - PNAME_PWAPLAYER, PNAME_INTENT_VERIFICATION, PNAME_MICROG_SERVICES_CORE - ) - } - - val dummySystemApp = ApplicationDescription( - packageName = "foundation.e.dummysystemapp", - uid = -1, - label = context.getString(R.string.dummy_system_app_label), - icon = context.getDrawable(R.drawable.ic_e_app_logo), - profileId = -1, - profileType = ProfileType.MAIN - ) - - val dummyCompatibilityApp = ApplicationDescription( - packageName = "foundation.e.dummyappscompatibilityapp", - uid = -2, - label = context.getString(R.string.dummy_apps_compatibility_app_label), - icon = context.getDrawable(R.drawable.ic_apps_compatibility_components), - profileId = -1, - profileType = ProfileType.MAIN - ) - - private suspend fun fetchAppDescriptions(fetchMissingIcons: Boolean = false) { - val launcherPackageNames = context.packageManager.queryIntentActivities( - Intent(Intent.ACTION_MAIN, null).apply { addCategory(Intent.CATEGORY_LAUNCHER) }, - 0 - ).mapNotNull { it.activityInfo?.packageName } - - val visibleAppsFilter = { packageInfo: PackageInfo -> - hasInternetPermission(packageInfo) && - isStandardApp(packageInfo.applicationInfo, launcherPackageNames) - } - - val hiddenAppsFilter = { packageInfo: PackageInfo -> - hasInternetPermission(packageInfo) && - isHiddenSystemApp(packageInfo.applicationInfo, launcherPackageNames) - } - - val compatibilityAppsFilter = { packageInfo: PackageInfo -> - packageInfo.packageName in compatibiltyPNames - } - - val visibleApps = recycleIcons( - newApps = permissionsModule.getApplications(visibleAppsFilter), - fetchMissingIcons = fetchMissingIcons - ) - val hiddenApps = permissionsModule.getApplications(hiddenAppsFilter) - val compatibilityApps = permissionsModule.getApplications(compatibilityAppsFilter) - - updateMaps(visibleApps + hiddenApps + compatibilityApps) - - allProfilesAppDescriptions.emit( - Triple( - visibleApps + dummySystemApp + dummyCompatibilityApp, - hiddenApps, - compatibilityApps - ) - ) - } - - private fun recycleIcons( - newApps: List<ApplicationDescription>, - fetchMissingIcons: Boolean - ): List<ApplicationDescription> { - val oldVisibleApps = allProfilesAppDescriptions.value.first - return newApps.map { app -> - app.copy( - icon = oldVisibleApps.find { app.apId == it.apId }?.icon - ?: if (fetchMissingIcons) permissionsModule.getApplicationIcon(app) else null - ) - } - } - - private fun updateMaps(apps: List<ApplicationDescription>) { - val byUid = mutableMapOf<Int, ApplicationDescription>() - val byApId = mutableMapOf<String, ApplicationDescription>() - apps.forEach { app -> - byUid[app.uid]?.run { packageName > app.packageName } == true - if (byUid[app.uid].let { it == null || it.packageName > app.packageName }) { - byUid[app.uid] = app - } - - byApId[app.apId] = app - } - appsByUid = byUid - appsByAPId = byApId - } - - private var lastFetchApps = 0 - private var refreshAppJob: Job? = null - private fun refreshAppDescriptions(fetchMissingIcons: Boolean = true, force: Boolean = false): Job? { - if (refreshAppJob == null) { - refreshAppJob = coroutineScope.launch(Dispatchers.IO) { - if (force || context.packageManager.getChangedPackages(lastFetchApps) != null) { - fetchAppDescriptions(fetchMissingIcons = fetchMissingIcons) - if (fetchMissingIcons) { - lastFetchApps = context.packageManager.getChangedPackages(lastFetchApps) - ?.sequenceNumber ?: lastFetchApps - } - - refreshAppJob = null - } - } - } - - return refreshAppJob - } - - fun mainProfileApps(): Flow<List<ApplicationDescription>> { - refreshAppDescriptions() - return allProfilesAppDescriptions.map { - it.first.filter { app -> app.profileType == ProfileType.MAIN } - .sortedBy { app -> app.label.toString().lowercase() } - } - } - - fun getMainProfileHiddenSystemApps(): List<ApplicationDescription> { - return allProfilesAppDescriptions.value.second.filter { it.profileType == ProfileType.MAIN } - } - - fun apps(): Flow<List<ApplicationDescription>> { - refreshAppDescriptions() - return allProfilesAppDescriptions.map { - it.first.sortedBy { app -> app.label.toString().lowercase() } - } - } - - fun allApps(): Flow<List<ApplicationDescription>> { - return allProfilesAppDescriptions.map { - it.first + it.second + it.third - } - } - - private fun getHiddenSystemApps(): List<ApplicationDescription> { - return allProfilesAppDescriptions.value.second - } - - private fun getCompatibilityApps(): List<ApplicationDescription> { - return allProfilesAppDescriptions.value.third - } - - fun anyForHiddenApps(app: ApplicationDescription, test: (ApplicationDescription) -> Boolean): Boolean { - return if (app == dummySystemApp) { - getHiddenSystemApps().any { test(it) } - } else if (app == dummyCompatibilityApp) { - getCompatibilityApps().any { test(it) } - } else test(app) - } - - fun applyForHiddenApps(app: ApplicationDescription, action: (ApplicationDescription) -> Unit) { - mapReduceForHiddenApps(app = app, map = action, reduce = {}) - } - - fun <T, R> mapReduceForHiddenApps( - app: ApplicationDescription, - map: (ApplicationDescription) -> T, - reduce: (List<T>) -> R - ): R { - return if (app == dummySystemApp) { - reduce(getHiddenSystemApps().map(map)) - } else if (app == dummyCompatibilityApp) { - reduce(getCompatibilityApps().map(map)) - } else reduce(listOf(map(app))) - } - - private var appsByUid = mapOf<Int, ApplicationDescription>() - private var appsByAPId = mapOf<String, ApplicationDescription>() - - fun getApp(appUid: Int): ApplicationDescription? { - return appsByUid[appUid] ?: run { - runBlocking { refreshAppDescriptions(fetchMissingIcons = false, force = true)?.join() } - appsByUid[appUid] - } - } - - fun getApp(apId: String): ApplicationDescription? { - if (apId.isBlank()) return null - - return appsByAPId[apId] ?: run { - runBlocking { refreshAppDescriptions(fetchMissingIcons = false, force = true)?.join() } - appsByAPId[apId] - } - } - - private val allProfilesAppDescriptions = MutableStateFlow( - Triple( - emptyList<ApplicationDescription>(), - emptyList<ApplicationDescription>(), - emptyList<ApplicationDescription>() - ) - ) - - private fun hasInternetPermission(packageInfo: PackageInfo): Boolean { - return packageInfo.requestedPermissions?.contains(Manifest.permission.INTERNET) == true - } - - @Suppress("ReturnCount") - private fun isNotHiddenSystemApp(app: ApplicationInfo, launcherApps: List<String>): Boolean { - if (app.packageName == PNAME_SETTINGS) { - return false - } else if (app.packageName == PNAME_PWAPLAYER) { - return true - } else if (app.hasFlag(ApplicationInfo.FLAG_UPDATED_SYSTEM_APP)) { - return true - } else if (!app.hasFlag(ApplicationInfo.FLAG_SYSTEM)) { - return true - } else if (launcherApps.contains(app.packageName)) { - return true - } - return false - } - - private fun isStandardApp(app: ApplicationInfo, launcherApps: List<String>): Boolean { - return when { - app.packageName == PNAME_SETTINGS -> false - app.packageName in compatibiltyPNames -> false - app.hasFlag(ApplicationInfo.FLAG_UPDATED_SYSTEM_APP) -> true - !app.hasFlag(ApplicationInfo.FLAG_SYSTEM) -> true - launcherApps.contains(app.packageName) -> true - else -> false - } - } - - private fun isHiddenSystemApp(app: ApplicationInfo, launcherApps: List<String>): Boolean { - return when { - app.packageName in compatibiltyPNames -> false - else -> !isNotHiddenSystemApp(app, launcherApps) - } - } - - private fun ApplicationInfo.hasFlag(flag: Int) = (flags and flag) == 1 -} diff --git a/app/src/main/java/foundation/e/privacycentralapp/data/repositories/CityDataSource.kt b/app/src/main/java/foundation/e/privacycentralapp/data/repositories/CityDataSource.kt deleted file mode 100644 index d6a6a19..0000000 --- a/app/src/main/java/foundation/e/privacycentralapp/data/repositories/CityDataSource.kt +++ /dev/null @@ -1,46 +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.dummy - -object CityDataSource { - private val BARCELONA = Pair(41.3851f, 2.1734f) - private val BUDAPEST = Pair(47.4979f, 19.0402f) - private val ABU_DHABI = Pair(24.4539f, 54.3773f) - private val HYDERABAD = Pair(17.3850f, 78.4867f) - private val QUEZON_CITY = Pair(14.6760f, 121.0437f) - private val PARIS = Pair(48.8566f, 2.3522f) - private val LONDON = Pair(51.5074f, 0.1278f) - private val SHANGHAI = Pair(31.2304f, 121.4737f) - private val MADRID = Pair(40.4168f, -3.7038f) - private val LAHORE = Pair(31.5204f, 74.3587f) - private val CHICAGO = Pair(41.8781f, -87.6298f) - - val citiesLocationsList = listOf( - BARCELONA, - BUDAPEST, - ABU_DHABI, - HYDERABAD, - QUEZON_CITY, - PARIS, - LONDON, - SHANGHAI, - MADRID, - LAHORE, - CHICAGO - ) -} diff --git a/app/src/main/java/foundation/e/privacycentralapp/data/repositories/LocalStateRepository.kt b/app/src/main/java/foundation/e/privacycentralapp/data/repositories/LocalStateRepository.kt deleted file mode 100644 index ed97c94..0000000 --- a/app/src/main/java/foundation/e/privacycentralapp/data/repositories/LocalStateRepository.kt +++ /dev/null @@ -1,116 +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.data.repositories - -import android.content.Context -import foundation.e.privacycentralapp.domain.entities.InternetPrivacyMode -import foundation.e.privacycentralapp.domain.entities.LocationMode -import foundation.e.privacymodules.permissions.data.ApplicationDescription -import kotlinx.coroutines.flow.MutableSharedFlow -import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.SharedFlow -import kotlinx.coroutines.flow.asStateFlow -import kotlinx.coroutines.flow.update - -class LocalStateRepository(context: Context) { - companion object { - private const val SHARED_PREFS_FILE = "localState" - private const val KEY_BLOCK_TRACKERS = "blockTrackers" - private const val KEY_IP_SCRAMBLING = "ipScrambling" - private const val KEY_FAKE_LOCATION = "fakeLocation" - private const val KEY_FAKE_LATITUDE = "fakeLatitude" - private const val KEY_FAKE_LONGITUDE = "fakeLongitude" - private const val KEY_FIRST_BOOT = "firstBoot" - private const val KEY_HIDE_WARNING_TRACKERS = "hide_warning_trackers" - private const val KEY_HIDE_WARNING_LOCATION = "hide_warning_location" - private const val KEY_HIDE_WARNING_IPSCRAMBLING = "hide_warning_ipscrambling" - } - - private val sharedPref = context.getSharedPreferences(SHARED_PREFS_FILE, Context.MODE_PRIVATE) - - private val _blockTrackers = MutableStateFlow(sharedPref.getBoolean(KEY_BLOCK_TRACKERS, true)) - val blockTrackers = _blockTrackers.asStateFlow() - - fun setBlockTrackers(enabled: Boolean) { - set(KEY_BLOCK_TRACKERS, enabled) - _blockTrackers.update { enabled } - } - - val areAllTrackersBlocked: MutableStateFlow<Boolean> = MutableStateFlow(false) - - private val _fakeLocationEnabled = MutableStateFlow(sharedPref.getBoolean(KEY_FAKE_LOCATION, false)) - - val fakeLocationEnabled = _fakeLocationEnabled.asStateFlow() - - fun setFakeLocationEnabled(enabled: Boolean) { - set(KEY_FAKE_LOCATION, enabled) - _fakeLocationEnabled.update { enabled } - } - - var fakeLocation: Pair<Float, Float> - get() = Pair( - // Initial default value is Quezon City - sharedPref.getFloat(KEY_FAKE_LATITUDE, 14.6760f), - sharedPref.getFloat(KEY_FAKE_LONGITUDE, 121.0437f) - ) - - set(value) { - sharedPref.edit() - .putFloat(KEY_FAKE_LATITUDE, value.first) - .putFloat(KEY_FAKE_LONGITUDE, value.second) - .apply() - } - - val locationMode: MutableStateFlow<LocationMode> = MutableStateFlow(LocationMode.REAL_LOCATION) - - private val _ipScramblingSetting = MutableStateFlow(sharedPref.getBoolean(KEY_IP_SCRAMBLING, false)) - val ipScramblingSetting = _ipScramblingSetting.asStateFlow() - - fun setIpScramblingSetting(enabled: Boolean) { - set(KEY_IP_SCRAMBLING, enabled) - _ipScramblingSetting.update { enabled } - } - - val internetPrivacyMode: MutableStateFlow<InternetPrivacyMode> = MutableStateFlow(InternetPrivacyMode.REAL_IP) - - private val _otherVpnRunning = MutableSharedFlow<ApplicationDescription>() - suspend fun emitOtherVpnRunning(appDesc: ApplicationDescription) { - _otherVpnRunning.emit(appDesc) - } - val otherVpnRunning: SharedFlow<ApplicationDescription> = _otherVpnRunning - - var firstBoot: Boolean - get() = sharedPref.getBoolean(KEY_FIRST_BOOT, true) - set(value) = set(KEY_FIRST_BOOT, value) - - var hideWarningTrackers: Boolean - get() = sharedPref.getBoolean(KEY_HIDE_WARNING_TRACKERS, false) - set(value) = set(KEY_HIDE_WARNING_TRACKERS, value) - - var hideWarningLocation: Boolean - get() = sharedPref.getBoolean(KEY_HIDE_WARNING_LOCATION, false) - set(value) = set(KEY_HIDE_WARNING_LOCATION, value) - - var hideWarningIpScrambling: Boolean - get() = sharedPref.getBoolean(KEY_HIDE_WARNING_IPSCRAMBLING, false) - set(value) = set(KEY_HIDE_WARNING_IPSCRAMBLING, value) - - private fun set(key: String, value: Boolean) { - sharedPref.edit().putBoolean(key, value).apply() - } -} diff --git a/app/src/main/java/foundation/e/privacycentralapp/data/repositories/TrackersRepository.kt b/app/src/main/java/foundation/e/privacycentralapp/data/repositories/TrackersRepository.kt deleted file mode 100644 index b5310e1..0000000 --- a/app/src/main/java/foundation/e/privacycentralapp/data/repositories/TrackersRepository.kt +++ /dev/null @@ -1,129 +0,0 @@ -/* - * Copyright (C) 2022 E FOUNDATION - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see <https://www.gnu.org/licenses/>. - */ - -package foundation.e.privacycentralapp.data.repositories - -import android.content.Context -import android.util.Log -import com.google.gson.Gson -import foundation.e.privacymodules.trackers.api.Tracker -import retrofit2.Retrofit -import retrofit2.converter.scalars.ScalarsConverterFactory -import retrofit2.http.GET -import java.io.File -import java.io.FileInputStream -import java.io.FileWriter -import java.io.IOException -import java.io.InputStreamReader -import java.io.PrintWriter - -class TrackersRepository(private val context: Context) { - - private val eTrackerFileName = "e_trackers.json" - private val eTrackerFile = File(context.filesDir.absolutePath, eTrackerFileName) - - var trackers: List<Tracker> = emptyList() - private set - - init { - initTrackersFile() - } - - suspend fun update() { - val api = ETrackersApi.build() - saveData(eTrackerFile, api.trackers()) - initTrackersFile() - } - - private fun initTrackersFile() { - try { - var inputStream = context.assets.open(eTrackerFileName) - if (eTrackerFile.exists()) { - inputStream = FileInputStream(eTrackerFile) - } - val reader = InputStreamReader(inputStream, "UTF-8") - val trackerResponse = - Gson().fromJson(reader, ETrackersApi.ETrackersResponse::class.java) - - trackers = mapper(trackerResponse) - - reader.close() - inputStream.close() - } catch (e: Exception) { - Log.e("TrackersRepository", "While parsing trackers in assets", e) - } - } - - private fun mapper(response: ETrackersApi.ETrackersResponse): List<Tracker> { - return response.trackers.mapNotNull { - try { - it.toTracker() - } catch (e: Exception) { - null - } - } - } - - private fun ETrackersApi.ETrackersResponse.ETracker.toTracker(): Tracker { - return Tracker( - id = id!!, - hostnames = hostnames!!.toSet(), - label = name!!, - exodusId = exodusId - ) - } - - private fun saveData(file: File, data: String): Boolean { - try { - val fos = FileWriter(file, false) - val ps = PrintWriter(fos) - ps.apply { - print(data) - flush() - close() - } - return true - } catch (e: IOException) { - e.printStackTrace() - } - return false - } -} - -interface ETrackersApi { - companion object { - fun build(): ETrackersApi { - val retrofit = Retrofit.Builder() - .baseUrl("https://gitlab.e.foundation/e/os/tracker-list/-/raw/main/") - .addConverterFactory(ScalarsConverterFactory.create()) - .build() - return retrofit.create(ETrackersApi::class.java) - } - } - - @GET("list/e_trackers.json") - suspend fun trackers(): String - - data class ETrackersResponse(val trackers: List<ETracker>) { - data class ETracker( - val id: String?, - val hostnames: List<String>?, - val name: String?, - val exodusId: String? - ) - } -} diff --git a/app/src/main/java/foundation/e/privacycentralapp/domain/entities/AppWithCounts.kt b/app/src/main/java/foundation/e/privacycentralapp/domain/entities/AppWithCounts.kt deleted file mode 100644 index afdd2d5..0000000 --- a/app/src/main/java/foundation/e/privacycentralapp/domain/entities/AppWithCounts.kt +++ /dev/null @@ -1,59 +0,0 @@ -/* - * Copyright (C) 2023 MURENA SAS - * Copyright (C) 2022 E FOUNDATION - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see <https://www.gnu.org/licenses/>. - */ - -package foundation.e.privacycentralapp.domain.entities - -import android.graphics.drawable.Drawable -import foundation.e.privacymodules.permissions.data.ApplicationDescription - -data class AppWithCounts( - val appDesc: ApplicationDescription, - val packageName: String, - val uid: Int, - var label: CharSequence?, - var icon: Drawable?, - val isWhitelisted: Boolean = false, - val trackersCount: Int = 0, - val whiteListedTrackersCount: Int = 0, - val blockedLeaks: Int = 0, - val leaks: Int = 0, -) { - constructor( - app: ApplicationDescription, - isWhitelisted: Boolean, - trackersCount: Int, - whiteListedTrackersCount: Int, - blockedLeaks: Int, - leaks: Int, - ) : - this( - appDesc = app, - packageName = app.packageName, - uid = app.uid, - label = app.label, - icon = app.icon, - isWhitelisted = isWhitelisted, - trackersCount = trackersCount, - whiteListedTrackersCount = whiteListedTrackersCount, - blockedLeaks = blockedLeaks, - leaks = leaks - ) - - val blockedTrackersCount get() = if (isWhitelisted) 0 - else Math.max(trackersCount - whiteListedTrackersCount, 0) -} diff --git a/app/src/main/java/foundation/e/privacycentralapp/domain/entities/InternetPrivacyMode.kt b/app/src/main/java/foundation/e/privacycentralapp/domain/entities/InternetPrivacyMode.kt deleted file mode 100644 index f849d57..0000000 --- a/app/src/main/java/foundation/e/privacycentralapp/domain/entities/InternetPrivacyMode.kt +++ /dev/null @@ -1,29 +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.domain.entities - -enum class InternetPrivacyMode { - REAL_IP, - HIDE_IP, - HIDE_IP_LOADING, - REAL_IP_LOADING; - - val isChecked get() = this == HIDE_IP || this == HIDE_IP_LOADING - - val isLoading get() = this == HIDE_IP_LOADING || this == REAL_IP_LOADING -} diff --git a/app/src/main/java/foundation/e/privacycentralapp/domain/entities/LocationMode.kt b/app/src/main/java/foundation/e/privacycentralapp/domain/entities/LocationMode.kt deleted file mode 100644 index 35a77b3..0000000 --- a/app/src/main/java/foundation/e/privacycentralapp/domain/entities/LocationMode.kt +++ /dev/null @@ -1,22 +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.domain.entities - -enum class LocationMode { - REAL_LOCATION, RANDOM_LOCATION, SPECIFIC_LOCATION -} diff --git a/app/src/main/java/foundation/e/privacycentralapp/domain/entities/MainFeatures.kt b/app/src/main/java/foundation/e/privacycentralapp/domain/entities/MainFeatures.kt deleted file mode 100644 index 0e7f99c..0000000 --- a/app/src/main/java/foundation/e/privacycentralapp/domain/entities/MainFeatures.kt +++ /dev/null @@ -1,22 +0,0 @@ -/* - * Copyright (C) 2022 E FOUNDATION - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see <https://www.gnu.org/licenses/>. - */ - -package foundation.e.privacycentralapp.domain.entities - -enum class MainFeatures { - TRACKERS_CONTROL, FAKE_LOCATION, IP_SCRAMBLING -} diff --git a/app/src/main/java/foundation/e/privacycentralapp/domain/entities/QuickPrivacyState.kt b/app/src/main/java/foundation/e/privacycentralapp/domain/entities/QuickPrivacyState.kt deleted file mode 100644 index 3257402..0000000 --- a/app/src/main/java/foundation/e/privacycentralapp/domain/entities/QuickPrivacyState.kt +++ /dev/null @@ -1,24 +0,0 @@ -/* - * Copyright (C) 2022 E FOUNDATION - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see <https://www.gnu.org/licenses/>. - */ - -package foundation.e.privacycentralapp.domain.entities - -enum class QuickPrivacyState { - DISABLED, ENABLED, FULL_ENABLED; - - fun isEnabled(): Boolean = this != DISABLED -} diff --git a/app/src/main/java/foundation/e/privacycentralapp/domain/entities/TrackerMode.kt b/app/src/main/java/foundation/e/privacycentralapp/domain/entities/TrackerMode.kt deleted file mode 100644 index 9f057be..0000000 --- a/app/src/main/java/foundation/e/privacycentralapp/domain/entities/TrackerMode.kt +++ /dev/null @@ -1,22 +0,0 @@ -/* - * Copyright (C) 2022 E FOUNDATION - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see <https://www.gnu.org/licenses/>. - */ - -package foundation.e.privacycentralapp.domain.entities - -enum class TrackerMode { - DENIED, CUSTOM, VULNERABLE -} diff --git a/app/src/main/java/foundation/e/privacycentralapp/domain/entities/TrackersPeriodicStatistics.kt b/app/src/main/java/foundation/e/privacycentralapp/domain/entities/TrackersPeriodicStatistics.kt deleted file mode 100644 index 8ce55dd..0000000 --- a/app/src/main/java/foundation/e/privacycentralapp/domain/entities/TrackersPeriodicStatistics.kt +++ /dev/null @@ -1,25 +0,0 @@ -/* - * Copyright (C) 2022 E FOUNDATION - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see <https://www.gnu.org/licenses/>. - */ - -package foundation.e.privacycentralapp.domain.entities - -data class TrackersPeriodicStatistics( - val callsBlockedNLeaked: List<Pair<Int, Int>>, - val periods: List<String>, - val trackersCount: Int, - val graduations: List<String?>? = null -) diff --git a/app/src/main/java/foundation/e/privacycentralapp/domain/usecases/AppListUseCase.kt b/app/src/main/java/foundation/e/privacycentralapp/domain/usecases/AppListUseCase.kt deleted file mode 100644 index dd62839..0000000 --- a/app/src/main/java/foundation/e/privacycentralapp/domain/usecases/AppListUseCase.kt +++ /dev/null @@ -1,39 +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.domain.usecases - -import foundation.e.privacycentralapp.data.repositories.AppListsRepository -import foundation.e.privacymodules.permissions.data.ApplicationDescription -import kotlinx.coroutines.flow.Flow - -class AppListUseCase( - private val appListsRepository: AppListsRepository -) { - val dummySystemApp = appListsRepository.dummySystemApp - fun getApp(uid: Int): ApplicationDescription { - return when (uid) { - dummySystemApp.uid -> dummySystemApp - appListsRepository.dummyCompatibilityApp.uid -> - appListsRepository.dummyCompatibilityApp - else -> appListsRepository.getApp(uid) ?: dummySystemApp - } - } - fun getAppsUsingInternet(): Flow<List<ApplicationDescription>> { - return appListsRepository.mainProfileApps() - } -} diff --git a/app/src/main/java/foundation/e/privacycentralapp/domain/usecases/FakeLocationStateUseCase.kt b/app/src/main/java/foundation/e/privacycentralapp/domain/usecases/FakeLocationStateUseCase.kt deleted file mode 100644 index 0ff2edb..0000000 --- a/app/src/main/java/foundation/e/privacycentralapp/domain/usecases/FakeLocationStateUseCase.kt +++ /dev/null @@ -1,209 +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.domain.usecases - -import android.app.AppOpsManager -import android.content.Context -import android.content.pm.PackageManager -import android.location.Location -import android.location.LocationListener -import android.location.LocationManager -import android.os.Bundle -import android.util.Log -import foundation.e.privacycentralapp.data.repositories.LocalStateRepository -import foundation.e.privacycentralapp.domain.entities.LocationMode -import foundation.e.privacycentralapp.dummy.CityDataSource -import foundation.e.privacymodules.fakelocation.IFakeLocationModule -import foundation.e.privacymodules.permissions.PermissionsPrivacyModule -import foundation.e.privacymodules.permissions.data.AppOpModes -import foundation.e.privacymodules.permissions.data.ApplicationDescription -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.StateFlow -import kotlinx.coroutines.flow.update -import kotlinx.coroutines.launch -import kotlin.random.Random - -class FakeLocationStateUseCase( - private val fakeLocationModule: IFakeLocationModule, - private val permissionsModule: PermissionsPrivacyModule, - private val localStateRepository: LocalStateRepository, - private val citiesRepository: CityDataSource, - private val appDesc: ApplicationDescription, - private val appContext: Context, - coroutineScope: CoroutineScope -) { - companion object { - private const val TAG = "FakeLocationStateUseCase" - } - - private val _configuredLocationMode = MutableStateFlow<Triple<LocationMode, Float?, Float?>>(Triple(LocationMode.REAL_LOCATION, null, null)) - val configuredLocationMode: StateFlow<Triple<LocationMode, Float?, Float?>> = _configuredLocationMode - - init { - coroutineScope.launch { - localStateRepository.fakeLocationEnabled.collect { - applySettings(it, localStateRepository.fakeLocation) - } - } - } - - private val locationManager: LocationManager - get() = appContext.getSystemService(Context.LOCATION_SERVICE) as LocationManager - - private fun hasAcquireLocationPermission(): Boolean { - return (appContext.checkSelfPermission(android.Manifest.permission.ACCESS_FINE_LOCATION) == PackageManager.PERMISSION_GRANTED) || - permissionsModule.toggleDangerousPermission(appDesc, android.Manifest.permission.ACCESS_FINE_LOCATION, true) - } - - private fun applySettings(isEnabled: Boolean, fakeLocation: Pair<Float, Float>, isSpecificLocation: Boolean = false) { - _configuredLocationMode.value = computeLocationMode(isEnabled, fakeLocation, isSpecificLocation) - - if (isEnabled && hasAcquireMockLocationPermission()) { - fakeLocationModule.startFakeLocation() - fakeLocationModule.setFakeLocation(fakeLocation.first.toDouble(), fakeLocation.second.toDouble()) - localStateRepository.locationMode.value = configuredLocationMode.value.first - } else { - fakeLocationModule.stopFakeLocation() - localStateRepository.locationMode.value = LocationMode.REAL_LOCATION - } - } - - private fun hasAcquireMockLocationPermission(): Boolean { - return (permissionsModule.getAppOpMode(appDesc, AppOpsManager.OPSTR_MOCK_LOCATION) == AppOpModes.ALLOWED) || - permissionsModule.setAppOpMode(appDesc, AppOpsManager.OPSTR_MOCK_LOCATION, AppOpModes.ALLOWED) - } - - fun setSpecificLocation(latitude: Float, longitude: Float) { - setFakeLocation(latitude to longitude, true) - } - - fun setRandomLocation() { - val randomIndex = Random.nextInt(citiesRepository.citiesLocationsList.size) - val location = citiesRepository.citiesLocationsList[randomIndex] - - setFakeLocation(location) - } - - private fun setFakeLocation(location: Pair<Float, Float>, isSpecificLocation: Boolean = false) { - localStateRepository.fakeLocation = location - localStateRepository.setFakeLocationEnabled(true) - applySettings(true, location, isSpecificLocation) - } - - fun stopFakeLocation() { - localStateRepository.setFakeLocationEnabled(false) - applySettings(false, localStateRepository.fakeLocation) - } - - private fun computeLocationMode( - isFakeLocationEnabled: Boolean, - fakeLocation: Pair<Float, Float>, - isSpecificLocation: Boolean = false, - ): Triple<LocationMode, Float?, Float?> { - return Triple( - when { - !isFakeLocationEnabled -> LocationMode.REAL_LOCATION - (fakeLocation in citiesRepository.citiesLocationsList && !isSpecificLocation) -> - LocationMode.RANDOM_LOCATION - else -> LocationMode.SPECIFIC_LOCATION - }, - fakeLocation.first, - fakeLocation.second - ) - } - - val currentLocation = MutableStateFlow<Location?>(null) - - private var localListener = object : LocationListener { - - override fun onLocationChanged(location: Location) { - currentLocation.update { previous -> - if ((previous?.time ?: 0) + 1800 < location.time || - (previous?.accuracy ?: Float.MAX_VALUE) > location.accuracy - ) { - location - } else { - previous - } - } - } - - // Deprecated since API 29, never called. - override fun onStatusChanged(provider: String?, status: Int, extras: Bundle?) {} - - override fun onProviderEnabled(provider: String) { - reset() - } - - override fun onProviderDisabled(provider: String) { - reset() - } - - private fun reset() { - stopListeningLocation() - currentLocation.value = null - startListeningLocation() - } - } - - fun startListeningLocation(): Boolean { - return if (hasAcquireLocationPermission()) { - requestLocationUpdates() - true - } else false - } - - fun stopListeningLocation() { - locationManager.removeUpdates(localListener) - } - - private fun requestLocationUpdates() { - val networkProvider = LocationManager.NETWORK_PROVIDER - .takeIf { it in locationManager.allProviders } - val gpsProvider = LocationManager.GPS_PROVIDER - .takeIf { it in locationManager.allProviders } - - try { - networkProvider?.let { - locationManager.requestLocationUpdates( - it, - 1000L, - 0f, - localListener - ) - } - gpsProvider?.let { - locationManager.requestLocationUpdates( - it, - 1000L, - 0f, - localListener - ) - } - - networkProvider?.let { locationManager.getLastKnownLocation(it) } - ?: gpsProvider?.let { locationManager.getLastKnownLocation(it) } - ?.let { - localListener.onLocationChanged(it) - } - } catch (se: SecurityException) { - Log.e(TAG, "Missing permission", se) - } - } -} diff --git a/app/src/main/java/foundation/e/privacycentralapp/domain/usecases/GetQuickPrivacyStateUseCase.kt b/app/src/main/java/foundation/e/privacycentralapp/domain/usecases/GetQuickPrivacyStateUseCase.kt deleted file mode 100644 index e2c0e7f..0000000 --- a/app/src/main/java/foundation/e/privacycentralapp/domain/usecases/GetQuickPrivacyStateUseCase.kt +++ /dev/null @@ -1,89 +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.domain.usecases - -import foundation.e.privacycentralapp.data.repositories.LocalStateRepository -import foundation.e.privacycentralapp.domain.entities.InternetPrivacyMode -import foundation.e.privacycentralapp.domain.entities.LocationMode -import foundation.e.privacycentralapp.domain.entities.QuickPrivacyState -import foundation.e.privacycentralapp.domain.entities.TrackerMode -import foundation.e.privacymodules.permissions.data.ApplicationDescription -import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.SharedFlow -import kotlinx.coroutines.flow.StateFlow -import kotlinx.coroutines.flow.combine -import kotlinx.coroutines.flow.map - -class GetQuickPrivacyStateUseCase( - private val localStateRepository: LocalStateRepository -) { - val quickPrivacyState: Flow<QuickPrivacyState> = combine( - localStateRepository.blockTrackers, - localStateRepository.areAllTrackersBlocked, - localStateRepository.locationMode, - localStateRepository.internetPrivacyMode - ) { isBlockTrackers, isAllTrackersBlocked, locationMode, internetPrivacyMode -> - when { - !isBlockTrackers && - locationMode == LocationMode.REAL_LOCATION && - internetPrivacyMode == InternetPrivacyMode.REAL_IP -> QuickPrivacyState.DISABLED - - isAllTrackersBlocked && - locationMode != LocationMode.REAL_LOCATION && - internetPrivacyMode in listOf( - InternetPrivacyMode.HIDE_IP, - InternetPrivacyMode.HIDE_IP_LOADING - ) -> QuickPrivacyState.FULL_ENABLED - - else -> QuickPrivacyState.ENABLED - } - } - - val trackerMode: Flow<TrackerMode> = combine( - localStateRepository.blockTrackers, - localStateRepository.areAllTrackersBlocked - ) { isBlockTrackers, isAllTrackersBlocked -> - when { - isBlockTrackers && isAllTrackersBlocked -> TrackerMode.DENIED - isBlockTrackers && !isAllTrackersBlocked -> TrackerMode.CUSTOM - else -> TrackerMode.VULNERABLE - } - } - - val isLocationHidden: Flow<Boolean> = localStateRepository.locationMode.map { locationMode -> - locationMode != LocationMode.REAL_LOCATION - } - - val locationMode: StateFlow<LocationMode> = localStateRepository.locationMode - - val ipScramblingMode: Flow<InternetPrivacyMode> = localStateRepository.internetPrivacyMode - - fun toggleTrackers() { - localStateRepository.setBlockTrackers(!localStateRepository.blockTrackers.value) - } - - fun toggleLocation() { - localStateRepository.setFakeLocationEnabled(!localStateRepository.fakeLocationEnabled.value) - } - - fun toggleIpScrambling() { - localStateRepository.setIpScramblingSetting(!localStateRepository.ipScramblingSetting.value) - } - - val otherVpnRunning: SharedFlow<ApplicationDescription> = localStateRepository.otherVpnRunning -} diff --git a/app/src/main/java/foundation/e/privacycentralapp/domain/usecases/IpScramblingStateUseCase.kt b/app/src/main/java/foundation/e/privacycentralapp/domain/usecases/IpScramblingStateUseCase.kt deleted file mode 100644 index dcb417b..0000000 --- a/app/src/main/java/foundation/e/privacycentralapp/domain/usecases/IpScramblingStateUseCase.kt +++ /dev/null @@ -1,170 +0,0 @@ -/* - * Copyright (C) 2021 E FOUNDATION, 2023 MURENA SAS - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see <https://www.gnu.org/licenses/>. - */ - -package foundation.e.privacycentralapp.domain.usecases - -import foundation.e.privacycentralapp.data.repositories.AppListsRepository -import foundation.e.privacycentralapp.data.repositories.LocalStateRepository -import foundation.e.privacycentralapp.domain.entities.InternetPrivacyMode -import foundation.e.privacycentralapp.domain.entities.InternetPrivacyMode.HIDE_IP -import foundation.e.privacycentralapp.domain.entities.InternetPrivacyMode.HIDE_IP_LOADING -import foundation.e.privacycentralapp.domain.entities.InternetPrivacyMode.REAL_IP -import foundation.e.privacycentralapp.domain.entities.InternetPrivacyMode.REAL_IP_LOADING -import foundation.e.privacymodules.ipscramblermodule.IIpScramblerModule -import foundation.e.privacymodules.permissions.IPermissionsPrivacyModule -import foundation.e.privacymodules.permissions.data.ApplicationDescription -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.channels.awaitClose -import kotlinx.coroutines.flow.SharingStarted -import kotlinx.coroutines.flow.StateFlow -import kotlinx.coroutines.flow.callbackFlow -import kotlinx.coroutines.flow.stateIn -import kotlinx.coroutines.launch - -class IpScramblingStateUseCase( - private val ipScramblerModule: IIpScramblerModule, - private val permissionsPrivacyModule: IPermissionsPrivacyModule, - private val appDesc: ApplicationDescription, - private val localStateRepository: LocalStateRepository, - private val appListsRepository: AppListsRepository, - private val coroutineScope: CoroutineScope -) { - val internetPrivacyMode: StateFlow<InternetPrivacyMode> = callbackFlow { - val listener = object : IIpScramblerModule.Listener { - override fun onStatusChanged(newStatus: IIpScramblerModule.Status) { - trySend(map(newStatus)) - } - - override fun log(message: String) {} - override fun onTrafficUpdate( - upload: Long, - download: Long, - read: Long, - write: Long - ) { - } - } - ipScramblerModule.addListener(listener) - ipScramblerModule.requestStatus() - awaitClose { ipScramblerModule.removeListener(listener) } - }.stateIn( - scope = coroutineScope, - started = SharingStarted.Eagerly, - initialValue = REAL_IP - ) - - init { - coroutineScope.launch(Dispatchers.Default) { - localStateRepository.ipScramblingSetting.collect { - applySettings(it) - } - } - - coroutineScope.launch { - internetPrivacyMode.collect { localStateRepository.internetPrivacyMode.value = it } - } - } - - fun toggle(hideIp: Boolean) { - localStateRepository.setIpScramblingSetting(enabled = hideIp) - } - - private fun getHiddenPackageNames(): List<String> { - return appListsRepository.getMainProfileHiddenSystemApps().map { it.packageName } - } - - val bypassTorApps: Set<String> get() { - var whitelist = ipScramblerModule.appList - if (getHiddenPackageNames().any { it in whitelist }) { - val mutable = whitelist.toMutableSet() - mutable.removeAll(getHiddenPackageNames()) - mutable.add(appListsRepository.dummySystemApp.packageName) - whitelist = mutable - } - if (AppListsRepository.compatibiltyPNames.any { it in whitelist }) { - val mutable = whitelist.toMutableSet() - mutable.removeAll(AppListsRepository.compatibiltyPNames) - mutable.add(appListsRepository.dummyCompatibilityApp.packageName) - whitelist = mutable - } - return whitelist - } - - fun toggleBypassTor(packageName: String) { - val visibleList = bypassTorApps.toMutableSet() - val rawList = ipScramblerModule.appList.toMutableSet() - - if (visibleList.contains(packageName)) { - if (packageName == appListsRepository.dummySystemApp.packageName) { - rawList.removeAll(getHiddenPackageNames()) - } else if (packageName == appListsRepository.dummyCompatibilityApp.packageName) { - rawList.removeAll(AppListsRepository.compatibiltyPNames) - } else { - rawList.remove(packageName) - } - } else { - if (packageName == appListsRepository.dummySystemApp.packageName) { - rawList.addAll(getHiddenPackageNames()) - } else if (packageName == appListsRepository.dummyCompatibilityApp.packageName) { - rawList.addAll(AppListsRepository.compatibiltyPNames) - } else { - rawList.add(packageName) - } - } - ipScramblerModule.appList = rawList - } - - private fun applySettings(isIpScramblingEnabled: Boolean) { - val currentMode = localStateRepository.internetPrivacyMode.value - when { - isIpScramblingEnabled && currentMode in setOf(REAL_IP, REAL_IP_LOADING) -> - applyStartIpScrambling() - - !isIpScramblingEnabled && currentMode in setOf(HIDE_IP, HIDE_IP_LOADING) -> - ipScramblerModule.stop() - - else -> {} - } - } - - private fun applyStartIpScrambling() { - ipScramblerModule.prepareAndroidVpn()?.let { - permissionsPrivacyModule.setVpnPackageAuthorization(appDesc.packageName) - permissionsPrivacyModule.getAlwaysOnVpnPackage() - }?.let { - coroutineScope.launch { - localStateRepository.emitOtherVpnRunning( - permissionsPrivacyModule.getApplicationDescription(packageName = it, withIcon = false) - ) - } - localStateRepository.setIpScramblingSetting(enabled = false) - } ?: run { - ipScramblerModule.start(enableNotification = false) - } - } - - private fun map(status: IIpScramblerModule.Status): InternetPrivacyMode { - return when (status) { - IIpScramblerModule.Status.OFF -> REAL_IP - IIpScramblerModule.Status.ON -> HIDE_IP - IIpScramblerModule.Status.STARTING -> HIDE_IP_LOADING - IIpScramblerModule.Status.STOPPING, - IIpScramblerModule.Status.START_DISABLED -> REAL_IP_LOADING - } - } -} diff --git a/app/src/main/java/foundation/e/privacycentralapp/domain/usecases/ShowFeaturesWarningUseCase.kt b/app/src/main/java/foundation/e/privacycentralapp/domain/usecases/ShowFeaturesWarningUseCase.kt deleted file mode 100644 index e347b34..0000000 --- a/app/src/main/java/foundation/e/privacycentralapp/domain/usecases/ShowFeaturesWarningUseCase.kt +++ /dev/null @@ -1,54 +0,0 @@ -/* - * Copyright (C) 2022 E FOUNDATION - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see <https://www.gnu.org/licenses/>. - */ - -package foundation.e.privacycentralapp.domain.usecases - -import foundation.e.privacycentralapp.data.repositories.LocalStateRepository -import foundation.e.privacycentralapp.domain.entities.MainFeatures -import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.drop -import kotlinx.coroutines.flow.dropWhile -import kotlinx.coroutines.flow.filter -import kotlinx.coroutines.flow.map -import kotlinx.coroutines.flow.merge - -class ShowFeaturesWarningUseCase( - private val localStateRepository: LocalStateRepository -) { - - fun showWarning(): Flow<MainFeatures> { - return merge( - localStateRepository.blockTrackers.drop(1).dropWhile { !it } - .filter { it && !localStateRepository.hideWarningTrackers } - .map { MainFeatures.TRACKERS_CONTROL }, - localStateRepository.fakeLocationEnabled.drop(1).dropWhile { !it } - .filter { it && !localStateRepository.hideWarningLocation } - .map { MainFeatures.FAKE_LOCATION }, - localStateRepository.ipScramblingSetting.drop(1).dropWhile { !it } - .filter { it && !localStateRepository.hideWarningIpScrambling } - .map { MainFeatures.IP_SCRAMBLING } - ) - } - - fun doNotShowAgain(feature: MainFeatures) { - when (feature) { - MainFeatures.TRACKERS_CONTROL -> localStateRepository.hideWarningTrackers = true - MainFeatures.FAKE_LOCATION -> localStateRepository.hideWarningLocation = true - MainFeatures.IP_SCRAMBLING -> localStateRepository.hideWarningIpScrambling = true - } - } -} diff --git a/app/src/main/java/foundation/e/privacycentralapp/domain/usecases/TrackersStateUseCase.kt b/app/src/main/java/foundation/e/privacycentralapp/domain/usecases/TrackersStateUseCase.kt deleted file mode 100644 index afb6d1e..0000000 --- a/app/src/main/java/foundation/e/privacycentralapp/domain/usecases/TrackersStateUseCase.kt +++ /dev/null @@ -1,105 +0,0 @@ -/* - * Copyright (C) 2021 E FOUNDATION, 2022 - 2023 MURENA SAS - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see <https://www.gnu.org/licenses/>. - */ - -package foundation.e.privacycentralapp.domain.usecases - -import foundation.e.privacycentralapp.data.repositories.AppListsRepository -import foundation.e.privacycentralapp.data.repositories.LocalStateRepository -import foundation.e.privacycentralapp.data.repositories.TrackersRepository -import foundation.e.privacymodules.permissions.data.ApplicationDescription -import foundation.e.privacymodules.trackers.api.IBlockTrackersPrivacyModule -import foundation.e.privacymodules.trackers.api.ITrackTrackersPrivacyModule -import foundation.e.privacymodules.trackers.api.Tracker -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.launch - -class TrackersStateUseCase( - private val blockTrackersPrivacyModule: IBlockTrackersPrivacyModule, - private val trackersPrivacyModule: ITrackTrackersPrivacyModule, - private val localStateRepository: LocalStateRepository, - private val trackersRepository: TrackersRepository, - private val appListsRepository: AppListsRepository, - private val coroutineScope: CoroutineScope -) { - init { - trackersPrivacyModule.start( - trackers = trackersRepository.trackers, - getAppByAPId = appListsRepository::getApp, - getAppByUid = appListsRepository::getApp, - enableNotification = false - ) - coroutineScope.launch { - localStateRepository.blockTrackers.collect { enabled -> - if (enabled) { - blockTrackersPrivacyModule.enableBlocking() - } else { - blockTrackersPrivacyModule.disableBlocking() - } - updateAllTrackersBlockedState() - } - } - } - - private fun updateAllTrackersBlockedState() { - localStateRepository.areAllTrackersBlocked.value = blockTrackersPrivacyModule.isBlockingEnabled() && - blockTrackersPrivacyModule.isWhiteListEmpty() - } - - fun isWhitelisted(app: ApplicationDescription): Boolean { - return isWhitelisted(app, appListsRepository, blockTrackersPrivacyModule) - } - - fun toggleAppWhitelist(app: ApplicationDescription, isWhitelisted: Boolean) { - appListsRepository.applyForHiddenApps(app) { - blockTrackersPrivacyModule.setWhiteListed(it, isWhitelisted) - } - updateAllTrackersBlockedState() - } - - fun blockTracker(app: ApplicationDescription, tracker: Tracker, isBlocked: Boolean) { - appListsRepository.applyForHiddenApps(app) { - blockTrackersPrivacyModule.setWhiteListed(tracker, it, !isBlocked) - } - updateAllTrackersBlockedState() - } - - fun clearWhitelist(app: ApplicationDescription) { - appListsRepository.applyForHiddenApps( - app, - blockTrackersPrivacyModule::clearWhiteList - ) - updateAllTrackersBlockedState() - } - - fun updateTrackers() = coroutineScope.launch { - trackersRepository.update() - trackersPrivacyModule.start( - trackers = trackersRepository.trackers, - getAppByAPId = appListsRepository::getApp, - getAppByUid = appListsRepository::getApp, - enableNotification = false - ) - } -} - -fun isWhitelisted( - app: ApplicationDescription, - appListsRepository: AppListsRepository, - blockTrackersPrivacyModule: IBlockTrackersPrivacyModule -): Boolean { - return appListsRepository.anyForHiddenApps(app, blockTrackersPrivacyModule::isWhitelisted) -} diff --git a/app/src/main/java/foundation/e/privacycentralapp/domain/usecases/TrackersStatisticsUseCase.kt b/app/src/main/java/foundation/e/privacycentralapp/domain/usecases/TrackersStatisticsUseCase.kt deleted file mode 100644 index 5ca7039..0000000 --- a/app/src/main/java/foundation/e/privacycentralapp/domain/usecases/TrackersStatisticsUseCase.kt +++ /dev/null @@ -1,278 +0,0 @@ -/* - * Copyright (C) 2021 E FOUNDATION, 2022 - 2023 MURENA SAS - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see <https://www.gnu.org/licenses/>. - */ - -package foundation.e.privacycentralapp.domain.usecases - -import android.content.res.Resources -import foundation.e.privacycentralapp.R -import foundation.e.privacycentralapp.common.throttleFirst -import foundation.e.privacycentralapp.data.repositories.AppListsRepository -import foundation.e.privacycentralapp.domain.entities.AppWithCounts -import foundation.e.privacycentralapp.domain.entities.TrackersPeriodicStatistics -import foundation.e.privacymodules.permissions.data.ApplicationDescription -import foundation.e.privacymodules.trackers.api.IBlockTrackersPrivacyModule -import foundation.e.privacymodules.trackers.api.ITrackTrackersPrivacyModule -import foundation.e.privacymodules.trackers.api.Tracker -import kotlinx.coroutines.FlowPreview -import kotlinx.coroutines.channels.awaitClose -import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.callbackFlow -import kotlinx.coroutines.flow.flowOf -import kotlinx.coroutines.flow.map -import kotlinx.coroutines.flow.onStart -import java.time.ZonedDateTime -import java.time.format.DateTimeFormatter -import java.time.temporal.ChronoUnit -import kotlin.time.Duration -import kotlin.time.Duration.Companion.seconds - -class TrackersStatisticsUseCase( - private val trackTrackersPrivacyModule: ITrackTrackersPrivacyModule, - private val blockTrackersPrivacyModule: IBlockTrackersPrivacyModule, - private val appListsRepository: AppListsRepository, - private val resources: Resources -) { - fun initAppList() { - appListsRepository.apps() - } - - private fun rawUpdates(): Flow<Unit> = callbackFlow { - val listener = object : ITrackTrackersPrivacyModule.Listener { - override fun onNewData() { - trySend(Unit) - } - } - trackTrackersPrivacyModule.addListener(listener) - awaitClose { trackTrackersPrivacyModule.removeListener(listener) } - } - - @OptIn(FlowPreview::class) - fun listenUpdates(debounce: Duration = 1.seconds) = rawUpdates() - .throttleFirst(windowDuration = debounce) - .onStart { emit(Unit) } - - fun getDayStatistics(): Pair<TrackersPeriodicStatistics, Int> { - return TrackersPeriodicStatistics( - callsBlockedNLeaked = trackTrackersPrivacyModule.getPastDayTrackersCalls(), - periods = buildDayLabels(), - trackersCount = trackTrackersPrivacyModule.getPastDayTrackersCount(), - graduations = buildDayGraduations(), - ) to trackTrackersPrivacyModule.getTrackersCount() - } - - fun getNonBlockedTrackersCount(): Flow<Int> { - return if (blockTrackersPrivacyModule.isBlockingEnabled()) - appListsRepository.allApps().map { apps -> - val whiteListedTrackers = mutableSetOf<Tracker>() - val whiteListedApps = blockTrackersPrivacyModule.getWhiteListedApp() - apps.forEach { app -> - if (app in whiteListedApps) { - whiteListedTrackers.addAll(trackTrackersPrivacyModule.getTrackersForApp(app)) - } else { - whiteListedTrackers.addAll(blockTrackersPrivacyModule.getWhiteList(app)) - } - } - whiteListedTrackers.size - } - else flowOf(trackTrackersPrivacyModule.getTrackersCount()) - } - - fun getMostLeakedApp(): ApplicationDescription? { - return trackTrackersPrivacyModule.getPastDayMostLeakedApp() - } - - fun getDayTrackersCalls() = trackTrackersPrivacyModule.getPastDayTrackersCalls() - - fun getDayTrackersCount() = trackTrackersPrivacyModule.getPastDayTrackersCount() - - private fun buildDayGraduations(): List<String?> { - val formatter = DateTimeFormatter.ofPattern( - resources.getString(R.string.trackers_graph_hours_period_format) - ) - - val periods = mutableListOf<String?>() - var end = ZonedDateTime.now() - for (i in 1..24) { - val start = end.truncatedTo(ChronoUnit.HOURS) - periods.add(if (start.hour % 6 == 0) formatter.format(start) else null) - end = start.minus(1, ChronoUnit.MINUTES) - } - return periods.reversed() - } - - private fun buildDayLabels(): List<String> { - val formatter = DateTimeFormatter.ofPattern( - resources.getString(R.string.trackers_graph_hours_period_format) - ) - val periods = mutableListOf<String>() - var end = ZonedDateTime.now() - for (i in 1..24) { - val start = end.truncatedTo(ChronoUnit.HOURS) - periods.add("${formatter.format(start)} - ${formatter.format(end)}") - end = start.minus(1, ChronoUnit.MINUTES) - } - return periods.reversed() - } - - private fun buildMonthLabels(): List<String> { - val formater = DateTimeFormatter.ofPattern( - resources.getString(R.string.trackers_graph_days_period_format) - ) - val periods = mutableListOf<String>() - var day = ZonedDateTime.now().truncatedTo(ChronoUnit.DAYS) - for (i in 1..30) { - periods.add(formater.format(day)) - day = day.minus(1, ChronoUnit.DAYS) - } - return periods.reversed() - } - - private fun buildYearLabels(): List<String> { - val formater = DateTimeFormatter.ofPattern( - resources.getString(R.string.trackers_graph_months_period_format) - ) - val periods = mutableListOf<String>() - var month = ZonedDateTime.now().truncatedTo(ChronoUnit.DAYS).withDayOfMonth(1) - for (i in 1..12) { - periods.add(formater.format(month)) - month = month.minus(1, ChronoUnit.MONTHS) - } - return periods.reversed() - } - - fun getDayMonthYearStatistics(): Triple<TrackersPeriodicStatistics, TrackersPeriodicStatistics, TrackersPeriodicStatistics> { - return with(trackTrackersPrivacyModule) { - Triple( - TrackersPeriodicStatistics( - callsBlockedNLeaked = getPastDayTrackersCalls(), - periods = buildDayLabels(), - trackersCount = getPastDayTrackersCount() - ), - TrackersPeriodicStatistics( - callsBlockedNLeaked = getPastMonthTrackersCalls(), - periods = buildMonthLabels(), - trackersCount = getPastMonthTrackersCount() - ), - TrackersPeriodicStatistics( - callsBlockedNLeaked = getPastYearTrackersCalls(), - periods = buildYearLabels(), - trackersCount = getPastYearTrackersCount() - ) - ) - } - } - - fun getTrackersWithWhiteList(app: ApplicationDescription): List<Pair<Tracker, Boolean>> { - return appListsRepository.mapReduceForHiddenApps( - app = app, - map = { appDesc: ApplicationDescription -> - ( - trackTrackersPrivacyModule.getTrackersForApp(appDesc) to - blockTrackersPrivacyModule.getWhiteList(appDesc) - ) - }, - reduce = { lists -> - lists.unzip().let { (trackerLists, whiteListedIdLists) -> - val whiteListedIds = whiteListedIdLists.flatten().map { it.id }.toSet() - - trackerLists.flatten().distinctBy { it.id }.sortedBy { it.label.lowercase() } - .map { tracker -> tracker to (tracker.id in whiteListedIds) } - } - } - ) - } - - fun isWhiteListEmpty(app: ApplicationDescription): Boolean { - return appListsRepository.mapReduceForHiddenApps( - app = app, - map = { appDesc: ApplicationDescription -> - blockTrackersPrivacyModule.getWhiteList(appDesc).isEmpty() - }, - reduce = { areEmpty -> areEmpty.all { it } } - ) - } - - fun getCalls(app: ApplicationDescription): Pair<Int, Int> { - return appListsRepository.mapReduceForHiddenApps( - app = app, - map = trackTrackersPrivacyModule::getPastDayTrackersCallsForApp, - reduce = { zip -> - zip.unzip().let { (blocked, leaked) -> - blocked.sum() to leaked.sum() - } - } - ) - } - - fun getAppsWithCounts(): Flow<List<AppWithCounts>> { - val trackersCounts = trackTrackersPrivacyModule.getTrackersCountByApp() - val hiddenAppsTrackersWithWhiteList = - getTrackersWithWhiteList(appListsRepository.dummySystemApp) - val acAppsTrackersWithWhiteList = - getTrackersWithWhiteList(appListsRepository.dummyCompatibilityApp) - - return appListsRepository.apps() - .map { apps -> - val callsByApp = trackTrackersPrivacyModule.getPastDayTrackersCallsByApps() - apps.map { app -> - val calls = appListsRepository.mapReduceForHiddenApps( - app = app, - map = { callsByApp.getOrDefault(app, 0 to 0) }, - reduce = { - it.unzip().let { (blocked, leaked) -> - blocked.sum() to leaked.sum() - } - } - ) - - AppWithCounts( - app = app, - isWhitelisted = !blockTrackersPrivacyModule.isBlockingEnabled() || - isWhitelisted(app, appListsRepository, blockTrackersPrivacyModule), - trackersCount = when (app) { - appListsRepository.dummySystemApp -> - hiddenAppsTrackersWithWhiteList.size - appListsRepository.dummyCompatibilityApp -> - acAppsTrackersWithWhiteList.size - else -> trackersCounts.getOrDefault(app, 0) - }, - whiteListedTrackersCount = when (app) { - appListsRepository.dummySystemApp -> - hiddenAppsTrackersWithWhiteList.count { it.second } - appListsRepository.dummyCompatibilityApp -> - acAppsTrackersWithWhiteList.count { it.second } - else -> - blockTrackersPrivacyModule.getWhiteList(app).size - }, - blockedLeaks = calls.first, - leaks = calls.second - ) - } - .sortedWith(mostLeakedAppsComparator) - } - } - - private val mostLeakedAppsComparator: Comparator<AppWithCounts> = Comparator { o1, o2 -> - val leaks = o2.leaks - o1.leaks - if (leaks != 0) leaks else { - val whitelisted = o2.whiteListedTrackersCount - o1.whiteListedTrackersCount - if (whitelisted != 0) whitelisted else { - o2.trackersCount - o1.trackersCount - } - } - } -} diff --git a/app/src/main/java/foundation/e/privacycentralapp/domain/usecases/UpdateWidgetUseCase.kt b/app/src/main/java/foundation/e/privacycentralapp/domain/usecases/UpdateWidgetUseCase.kt deleted file mode 100644 index f70065c..0000000 --- a/app/src/main/java/foundation/e/privacycentralapp/domain/usecases/UpdateWidgetUseCase.kt +++ /dev/null @@ -1,33 +0,0 @@ -/* - * Copyright (C) 2022 E FOUNDATION - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see <https://www.gnu.org/licenses/>. - */ - -package foundation.e.privacycentralapp.domain.usecases - -import foundation.e.privacycentralapp.data.repositories.LocalStateRepository -import foundation.e.privacymodules.trackers.api.ITrackTrackersPrivacyModule - -class UpdateWidgetUseCase( - private val localStateRepository: LocalStateRepository, - private val trackTrackersPrivacyModule: ITrackTrackersPrivacyModule, -) { - init { - trackTrackersPrivacyModule.addListener(object : ITrackTrackersPrivacyModule.Listener { - override fun onNewData() { - } - }) - } -} 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 deleted file mode 100644 index 0dc24e8..0000000 --- a/app/src/main/java/foundation/e/privacycentralapp/features/dashboard/DashboardFragment.kt +++ /dev/null @@ -1,307 +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.dashboard - -import android.content.Intent -import android.os.Bundle -import android.text.Html -import android.text.Html.FROM_HTML_MODE_LEGACY -import android.view.View -import android.widget.Toast -import androidx.core.content.ContextCompat.getColor -import androidx.core.os.bundleOf -import androidx.core.view.isVisible -import androidx.fragment.app.commit -import androidx.fragment.app.replace -import androidx.fragment.app.viewModels -import androidx.lifecycle.Lifecycle -import androidx.lifecycle.lifecycleScope -import androidx.lifecycle.repeatOnLifecycle -import foundation.e.privacycentralapp.DependencyContainer -import foundation.e.privacycentralapp.PrivacyCentralApplication -import foundation.e.privacycentralapp.R -import foundation.e.privacycentralapp.common.GraphHolder -import foundation.e.privacycentralapp.common.NavToolbarFragment -import foundation.e.privacycentralapp.databinding.FragmentDashboardBinding -import foundation.e.privacycentralapp.domain.entities.InternetPrivacyMode -import foundation.e.privacycentralapp.domain.entities.LocationMode -import foundation.e.privacycentralapp.domain.entities.QuickPrivacyState -import foundation.e.privacycentralapp.domain.entities.TrackerMode -import foundation.e.privacycentralapp.features.dashboard.DashboardViewModel.Action -import foundation.e.privacycentralapp.features.dashboard.DashboardViewModel.SingleEvent -import foundation.e.privacycentralapp.features.internetprivacy.InternetPrivacyFragment -import foundation.e.privacycentralapp.features.location.FakeLocationFragment -import foundation.e.privacycentralapp.features.trackers.TrackersFragment -import foundation.e.privacycentralapp.features.trackers.apptrackers.AppTrackersFragment -import kotlinx.coroutines.launch - -class DashboardFragment : NavToolbarFragment(R.layout.fragment_dashboard) { - companion object { - private const val PARAM_HIGHLIGHT_INDEX = "PARAM_HIGHLIGHT_INDEX" - fun buildArgs(highlightIndex: Int): Bundle = bundleOf( - PARAM_HIGHLIGHT_INDEX to highlightIndex - ) - } - - private val dependencyContainer: DependencyContainer by lazy { - (this.requireActivity().application as PrivacyCentralApplication).dependencyContainer - } - - private val viewModel: DashboardViewModel by viewModels { - dependencyContainer.viewModelsFactory - } - - private var graphHolder: GraphHolder? = null - - private var _binding: FragmentDashboardBinding? = null - private val binding get() = _binding!! - - private var highlightIndexOnStart: Int? = null - - override fun onCreate(savedInstanceState: Bundle?) { - super.onCreate(savedInstanceState) - - highlightIndexOnStart = arguments?.getInt(PARAM_HIGHLIGHT_INDEX, -1) - } - - override fun onViewCreated(view: View, savedInstanceState: Bundle?) { - super.onViewCreated(view, savedInstanceState) - _binding = FragmentDashboardBinding.bind(view) - - graphHolder = GraphHolder(binding.graph, requireContext()) - - binding.leakingAppButton.setOnClickListener { - viewModel.submitAction(Action.ShowMostLeakedApp) - } - binding.toggleTrackers.setOnClickListener { - viewModel.submitAction(Action.ToggleTrackers) - } - binding.toggleLocation.setOnClickListener { - viewModel.submitAction(Action.ToggleLocation) - } - binding.toggleIpscrambling.setOnClickListener { - viewModel.submitAction(Action.ToggleIpScrambling) - } - binding.myLocation.container.setOnClickListener { - viewModel.submitAction(Action.ShowFakeMyLocationAction) - } - binding.internetActivityPrivacy.container.setOnClickListener { - viewModel.submitAction(Action.ShowInternetActivityPrivacyAction) - } - binding.appsPermissions.container.setOnClickListener { - viewModel.submitAction(Action.ShowAppsPermissions) - } - - binding.amITracked.container.setOnClickListener { - viewModel.submitAction(Action.ShowTrackers) - } - - viewLifecycleOwner.lifecycleScope.launch { - viewLifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) { - render(viewModel.state.value) - viewModel.state.collect(::render) - } - } - - viewLifecycleOwner.lifecycleScope.launch { - viewLifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) { - viewModel.singleEvents.collect { event -> - when (event) { - is SingleEvent.NavigateToLocationSingleEvent -> { - requireActivity().supportFragmentManager.commit { - replace<FakeLocationFragment>(R.id.container) - setReorderingAllowed(true) - addToBackStack("dashboard") - } - } - is SingleEvent.NavigateToInternetActivityPrivacySingleEvent -> { - requireActivity().supportFragmentManager.commit { - replace<InternetPrivacyFragment>(R.id.container) - setReorderingAllowed(true) - addToBackStack("dashboard") - } - } - is SingleEvent.NavigateToPermissionsSingleEvent -> { - val intent = Intent("android.intent.action.MANAGE_PERMISSIONS") - requireActivity().startActivity(intent) - } - SingleEvent.NavigateToTrackersSingleEvent -> { - requireActivity().supportFragmentManager.commit { - replace<TrackersFragment>(R.id.container) - setReorderingAllowed(true) - addToBackStack("dashboard") - } - } - is SingleEvent.NavigateToAppDetailsEvent -> { - requireActivity().supportFragmentManager.commit { - replace<AppTrackersFragment>( - R.id.container, - args = AppTrackersFragment.buildArgs( - event.appDesc.label.toString(), - event.appDesc.packageName, - event.appDesc.uid - ) - ) - setReorderingAllowed(true) - addToBackStack("dashboard") - } - } - is SingleEvent.ToastMessageSingleEvent -> - Toast.makeText( - requireContext(), - getString(event.message, *event.args.toTypedArray()), - Toast.LENGTH_LONG - ).show() - } - } - } - } - - viewLifecycleOwner.lifecycleScope.launch { - viewLifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) { - viewModel.doOnStartedState() - } - } - } - - override fun getTitle(): String { - return getString(R.string.dashboard_title) - } - - private fun render(state: DashboardState) { - binding.stateLabel.text = getString( - when (state.quickPrivacyState) { - QuickPrivacyState.DISABLED -> R.string.dashboard_state_title_off - QuickPrivacyState.FULL_ENABLED -> R.string.dashboard_state_title_on - QuickPrivacyState.ENABLED -> R.string.dashboard_state_title_custom - } - ) - - binding.stateIcon.setImageResource( - if (state.quickPrivacyState.isEnabled()) R.drawable.ic_shield_on - else R.drawable.ic_shield_off - ) - - binding.toggleTrackers.isChecked = state.trackerMode != TrackerMode.VULNERABLE - - binding.stateTrackers.text = getString( - when (state.trackerMode) { - TrackerMode.DENIED -> R.string.dashboard_state_trackers_on - TrackerMode.VULNERABLE -> R.string.dashboard_state_trackers_off - TrackerMode.CUSTOM -> R.string.dashboard_state_trackers_custom - } - ) - binding.stateTrackers.setTextColor( - getColor( - requireContext(), - if (state.trackerMode == TrackerMode.VULNERABLE) R.color.red_off - else R.color.green_valid - ) - ) - - binding.toggleLocation.isChecked = state.isLocationHidden - - binding.stateGeolocation.text = getString( - if (state.isLocationHidden) R.string.dashboard_state_geolocation_on - else R.string.dashboard_state_geolocation_off - ) - binding.stateGeolocation.setTextColor( - getColor( - requireContext(), - if (state.isLocationHidden) R.color.green_valid - else R.color.red_off - ) - ) - - binding.toggleIpscrambling.isChecked = state.ipScramblingMode.isChecked - val isLoading = state.ipScramblingMode.isLoading - - binding.stateIpAddress.text = getString( - if (state.ipScramblingMode == InternetPrivacyMode.HIDE_IP) R.string.dashboard_state_ipaddress_on - else R.string.dashboard_state_ipaddress_off - ) - - binding.stateIpAddressLoader.visibility = if (isLoading) View.VISIBLE else View.GONE - binding.stateIpAddress.visibility = if (!isLoading) View.VISIBLE else View.GONE - - binding.stateIpAddress.setTextColor( - getColor( - requireContext(), - if (state.ipScramblingMode == InternetPrivacyMode.HIDE_IP) R.color.green_valid - else R.color.red_off - ) - ) - - if (state.dayStatistics?.all { it.first == 0 && it.second == 0 } == true) { - binding.graph.visibility = View.INVISIBLE - binding.graphLegend.isVisible = false - binding.leakingAppButton.isVisible = false - binding.graphEmpty.isVisible = true - } else { - binding.graph.isVisible = true - binding.graphLegend.isVisible = true - binding.leakingAppButton.isVisible = true - binding.graphEmpty.isVisible = false - state.dayStatistics?.let { graphHolder?.data = it } - state.dayLabels?.let { graphHolder?.labels = it } - state.dayGraduations?.let { graphHolder?.graduations = it } - - binding.graphLegend.text = Html.fromHtml( - getString( - R.string.dashboard_graph_trackers_legend, - state.leakedTrackersCount?.toString() ?: "No" - ), - FROM_HTML_MODE_LEGACY - ) - - highlightIndexOnStart?.let { - binding.graph.post { - graphHolder?.highlightIndex(it) - } - highlightIndexOnStart = null - } - } - - if (state.allowedTrackersCount != null && state.trackersCount != null) { - binding.amITracked.subTitle = getString(R.string.dashboard_am_i_tracked_subtitle, state.trackersCount, state.allowedTrackersCount) - } else { - binding.amITracked.subTitle = "" - } - - binding.myLocation.subTitle = getString( - when (state.locationMode) { - LocationMode.REAL_LOCATION -> R.string.dashboard_location_subtitle_off - LocationMode.SPECIFIC_LOCATION -> R.string.dashboard_location_subtitle_specific - LocationMode.RANDOM_LOCATION -> R.string.dashboard_location_subtitle_random - } - ) - - binding.internetActivityPrivacy.subTitle = getString( - if (state.ipScramblingMode == InternetPrivacyMode.HIDE_IP) R.string.dashboard_internet_activity_privacy_subtitle_on - else R.string.dashboard_internet_activity_privacy_subtitle_off - ) - - binding.executePendingBindings() - } - - override fun onDestroyView() { - super.onDestroyView() - graphHolder = null - _binding = null - } -} diff --git a/app/src/main/java/foundation/e/privacycentralapp/features/dashboard/DashboardState.kt b/app/src/main/java/foundation/e/privacycentralapp/features/dashboard/DashboardState.kt deleted file mode 100644 index 0e3521d..0000000 --- a/app/src/main/java/foundation/e/privacycentralapp/features/dashboard/DashboardState.kt +++ /dev/null @@ -1,37 +0,0 @@ -/* - * Copyright (C) 2022 E FOUNDATION - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see <https://www.gnu.org/licenses/>. - */ - -package foundation.e.privacycentralapp.features.dashboard - -import foundation.e.privacycentralapp.domain.entities.InternetPrivacyMode -import foundation.e.privacycentralapp.domain.entities.LocationMode -import foundation.e.privacycentralapp.domain.entities.QuickPrivacyState -import foundation.e.privacycentralapp.domain.entities.TrackerMode - -data class DashboardState( - val quickPrivacyState: QuickPrivacyState = QuickPrivacyState.DISABLED, - val trackerMode: TrackerMode = TrackerMode.VULNERABLE, - val isLocationHidden: Boolean = false, - val ipScramblingMode: InternetPrivacyMode = InternetPrivacyMode.REAL_IP_LOADING, - val locationMode: LocationMode = LocationMode.REAL_LOCATION, - val leakedTrackersCount: Int? = null, - val trackersCount: Int? = null, - val allowedTrackersCount: Int? = null, - val dayStatistics: List<Pair<Int, Int>>? = null, - val dayLabels: List<String>? = null, - val dayGraduations: List<String?>? = null, -) 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 deleted file mode 100644 index f3a9774..0000000 --- a/app/src/main/java/foundation/e/privacycentralapp/features/dashboard/DashboardViewModel.kt +++ /dev/null @@ -1,158 +0,0 @@ -/* -* Copyright (C) 2023 MURENA SAS - * 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.dashboard - -import androidx.annotation.StringRes -import androidx.lifecycle.ViewModel -import androidx.lifecycle.viewModelScope -import foundation.e.privacycentralapp.R -import foundation.e.privacycentralapp.domain.usecases.GetQuickPrivacyStateUseCase -import foundation.e.privacycentralapp.domain.usecases.TrackersStatisticsUseCase -import foundation.e.privacymodules.permissions.data.ApplicationDescription -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.delay -import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.MutableSharedFlow -import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.asSharedFlow -import kotlinx.coroutines.flow.asStateFlow -import kotlinx.coroutines.flow.first -import kotlinx.coroutines.flow.flatMapLatest -import kotlinx.coroutines.flow.map -import kotlinx.coroutines.flow.merge -import kotlinx.coroutines.flow.update -import kotlinx.coroutines.launch -import kotlinx.coroutines.withContext - -class DashboardViewModel( - private val getPrivacyStateUseCase: GetQuickPrivacyStateUseCase, - private val trackersStatisticsUseCase: TrackersStatisticsUseCase, -) : ViewModel() { - - private val _state = MutableStateFlow(DashboardState()) - val state = _state.asStateFlow() - - private val _singleEvents = MutableSharedFlow<SingleEvent>() - val singleEvents = _singleEvents.asSharedFlow() - - init { - viewModelScope.launch(Dispatchers.IO) { trackersStatisticsUseCase.initAppList() } - } - - suspend fun doOnStartedState() = withContext(Dispatchers.IO) { - merge( - getPrivacyStateUseCase.quickPrivacyState.map { - _state.update { s -> s.copy(quickPrivacyState = it) } - }, - getPrivacyStateUseCase.ipScramblingMode.map { - _state.update { s -> s.copy(ipScramblingMode = it) } - }, - trackersStatisticsUseCase.listenUpdates().flatMapLatest { - fetchStatistics() - }, - getPrivacyStateUseCase.trackerMode.map { - _state.update { s -> s.copy(trackerMode = it) } - }, - getPrivacyStateUseCase.isLocationHidden.map { - _state.update { s -> s.copy(isLocationHidden = it) } - }, - getPrivacyStateUseCase.locationMode.map { - _state.update { s -> s.copy(locationMode = it) } - }, - getPrivacyStateUseCase.otherVpnRunning.map { - _singleEvents.emit( - SingleEvent.ToastMessageSingleEvent( - R.string.ipscrambling_error_always_on_vpn_already_running, - listOf(it.label ?: "") - ) - ) - } - ).collect {} - } - - fun submitAction(action: Action) = viewModelScope.launch { - when (action) { - is Action.ToggleTrackers -> { - getPrivacyStateUseCase.toggleTrackers() - // Add delay here to prevent race condition with trackers state. - delay(200) - fetchStatistics().first() - } - is Action.ToggleLocation -> getPrivacyStateUseCase.toggleLocation() - is Action.ToggleIpScrambling -> getPrivacyStateUseCase.toggleIpScrambling() - is Action.ShowFakeMyLocationAction -> - _singleEvents.emit(SingleEvent.NavigateToLocationSingleEvent) - is Action.ShowAppsPermissions -> - _singleEvents.emit(SingleEvent.NavigateToPermissionsSingleEvent) - is Action.ShowInternetActivityPrivacyAction -> - _singleEvents.emit(SingleEvent.NavigateToInternetActivityPrivacySingleEvent) - is Action.ShowTrackers -> - _singleEvents.emit(SingleEvent.NavigateToTrackersSingleEvent) - is Action.ShowMostLeakedApp -> actionShowMostLeakedApp() - } - } - - private suspend fun fetchStatistics(): Flow<Unit> = withContext(Dispatchers.IO) { - trackersStatisticsUseCase.getNonBlockedTrackersCount().map { nonBlockedTrackersCount -> - trackersStatisticsUseCase.getDayStatistics().let { (dayStatistics, trackersCount) -> - _state.update { s -> - s.copy( - dayStatistics = dayStatistics.callsBlockedNLeaked, - dayLabels = dayStatistics.periods, - dayGraduations = dayStatistics.graduations, - leakedTrackersCount = dayStatistics.trackersCount, - trackersCount = trackersCount, - allowedTrackersCount = nonBlockedTrackersCount - ) - } - } - } - } - - private suspend fun actionShowMostLeakedApp() = withContext(Dispatchers.IO) { - _singleEvents.emit( - trackersStatisticsUseCase.getMostLeakedApp()?.let { - SingleEvent.NavigateToAppDetailsEvent(appDesc = it) - } ?: SingleEvent.NavigateToTrackersSingleEvent - ) - } - - sealed class SingleEvent { - object NavigateToTrackersSingleEvent : SingleEvent() - object NavigateToInternetActivityPrivacySingleEvent : SingleEvent() - object NavigateToLocationSingleEvent : SingleEvent() - object NavigateToPermissionsSingleEvent : SingleEvent() - data class NavigateToAppDetailsEvent(val appDesc: ApplicationDescription) : SingleEvent() - data class ToastMessageSingleEvent( - @StringRes val message: Int, - val args: List<Any> = emptyList() - ) : SingleEvent() - } - - sealed class Action { - object ToggleTrackers : Action() - object ToggleLocation : Action() - object ToggleIpScrambling : Action() - object ShowFakeMyLocationAction : Action() - object ShowInternetActivityPrivacyAction : Action() - object ShowAppsPermissions : Action() - object ShowTrackers : Action() - object ShowMostLeakedApp : Action() - } -} 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 deleted file mode 100644 index afef986..0000000 --- a/app/src/main/java/foundation/e/privacycentralapp/features/internetprivacy/InternetPrivacyFragment.kt +++ /dev/null @@ -1,201 +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.internetprivacy - -import android.os.Bundle -import android.view.View -import android.widget.AdapterView -import android.widget.ArrayAdapter -import android.widget.Toast -import androidx.fragment.app.viewModels -import androidx.lifecycle.Lifecycle -import androidx.lifecycle.lifecycleScope -import androidx.lifecycle.repeatOnLifecycle -import androidx.recyclerview.widget.LinearLayoutManager -import foundation.e.privacycentralapp.DependencyContainer -import foundation.e.privacycentralapp.PrivacyCentralApplication -import foundation.e.privacycentralapp.R -import foundation.e.privacycentralapp.common.NavToolbarFragment -import foundation.e.privacycentralapp.common.ToggleAppsAdapter -import foundation.e.privacycentralapp.common.setToolTipForAsterisk -import foundation.e.privacycentralapp.databinding.FragmentInternetActivityPolicyBinding -import foundation.e.privacycentralapp.domain.entities.InternetPrivacyMode -import kotlinx.coroutines.launch -import java.util.Locale - -class InternetPrivacyFragment : NavToolbarFragment(R.layout.fragment_internet_activity_policy) { - - private val dependencyContainer: DependencyContainer by lazy { - (this.requireActivity().application as PrivacyCentralApplication).dependencyContainer - } - - private val viewModel: InternetPrivacyViewModel by viewModels { - dependencyContainer.viewModelsFactory - } - - private var _binding: FragmentInternetActivityPolicyBinding? = null - private val binding get() = _binding!! - - private fun displayToast(message: String) { - Toast.makeText(requireContext(), message, Toast.LENGTH_SHORT) - .show() - } - - override fun onViewCreated(view: View, savedInstanceState: Bundle?) { - super.onViewCreated(view, savedInstanceState) - _binding = FragmentInternetActivityPolicyBinding.bind(view) - - binding.apps.apply { - layoutManager = LinearLayoutManager(requireContext()) - setHasFixedSize(true) - adapter = ToggleAppsAdapter(R.layout.ipscrambling_item_app_toggle) { packageName -> - viewModel.submitAction( - InternetPrivacyViewModel.Action.ToggleAppIpScrambled(packageName) - ) - } - } - - binding.radioUseRealIp.radiobutton.setOnClickListener { - viewModel.submitAction(InternetPrivacyViewModel.Action.UseRealIPAction) - } - - binding.radioUseHiddenIp.radiobutton.setOnClickListener { - viewModel.submitAction(InternetPrivacyViewModel.Action.UseHiddenIPAction) - } - - setToolTipForAsterisk( - textView = binding.ipscramblingSelectApps, - textId = R.string.ipscrambling_select_app, - tooltipTextId = R.string.ipscrambling_app_list_infos - ) - - binding.ipscramblingSelectLocation.apply { - adapter = ArrayAdapter( - requireContext(), android.R.layout.simple_spinner_item, - viewModel.availablesLocationsIds.map { - if (it == "") { - getString(R.string.ipscrambling_any_location) - } else { - Locale("", it).displayCountry - } - } - ).apply { - setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item) - } - - onItemSelectedListener = object : AdapterView.OnItemSelectedListener { - override fun onItemSelected( - parentView: AdapterView<*>, - selectedItemView: View?, - position: Int, - id: Long - ) { - viewModel.submitAction( - InternetPrivacyViewModel.Action.SelectLocationAction( - position - ) - ) - } - - override fun onNothingSelected(parentView: AdapterView<*>?) {} - } - } - - viewLifecycleOwner.lifecycleScope.launch { - viewLifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) { - render(viewModel.state.value) - viewModel.state.collect(::render) - } - } - - viewLifecycleOwner.lifecycleScope.launch { - viewLifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) { - viewModel.singleEvents.collect { event -> - when (event) { - is InternetPrivacyViewModel.SingleEvent.ErrorEvent -> { - displayToast(getString(event.errorResId, *event.args.toTypedArray())) - } - } - } - } - } - viewLifecycleOwner.lifecycleScope.launch { - viewLifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) { - viewModel.doOnStartedState() - } - } - } - - override fun getTitle(): String = getString(R.string.ipscrambling_title) - - private fun render(state: InternetPrivacyState) { - binding.radioUseHiddenIp.radiobutton.apply { - isChecked = state.mode in listOf( - InternetPrivacyMode.HIDE_IP, - InternetPrivacyMode.HIDE_IP_LOADING - ) - isEnabled = state.mode != InternetPrivacyMode.HIDE_IP_LOADING - } - binding.radioUseRealIp.radiobutton.apply { - isChecked = - state.mode in listOf( - InternetPrivacyMode.REAL_IP, - InternetPrivacyMode.REAL_IP_LOADING - ) - isEnabled = state.mode != InternetPrivacyMode.REAL_IP_LOADING - } - - binding.ipscramblingSelectLocation.setSelection(state.selectedLocationPosition) - - // TODO: this should not be mandatory. - binding.apps.post { - (binding.apps.adapter as ToggleAppsAdapter?)?.setData( - list = state.getApps(), - isEnabled = state.mode == InternetPrivacyMode.HIDE_IP - ) - } - - val viewIdsToHide = listOf( - binding.ipscramblingLocationLabel, - binding.selectLocationContainer, - binding.ipscramblingSelectLocation, - binding.ipscramblingSelectApps, - binding.apps - ) - - when { - state.mode in listOf( - InternetPrivacyMode.HIDE_IP_LOADING, - InternetPrivacyMode.REAL_IP_LOADING - ) - || state.availableApps.isEmpty() -> { - binding.loader.visibility = View.VISIBLE - viewIdsToHide.forEach { it.visibility = View.GONE } - } - else -> { - binding.loader.visibility = View.GONE - viewIdsToHide.forEach { it.visibility = View.VISIBLE } - } - } - } - - override fun onDestroyView() { - super.onDestroyView() - _binding = null - } -} diff --git a/app/src/main/java/foundation/e/privacycentralapp/features/internetprivacy/InternetPrivacyState.kt b/app/src/main/java/foundation/e/privacycentralapp/features/internetprivacy/InternetPrivacyState.kt deleted file mode 100644 index 54b7e01..0000000 --- a/app/src/main/java/foundation/e/privacycentralapp/features/internetprivacy/InternetPrivacyState.kt +++ /dev/null @@ -1,36 +0,0 @@ -/* - * Copyright (C) 2022 E FOUNDATION - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see <https://www.gnu.org/licenses/>. - */ - -package foundation.e.privacycentralapp.features.internetprivacy - -import foundation.e.privacycentralapp.domain.entities.InternetPrivacyMode -import foundation.e.privacymodules.permissions.data.ApplicationDescription - -data class InternetPrivacyState( - val mode: InternetPrivacyMode = InternetPrivacyMode.REAL_IP, - val availableApps: List<ApplicationDescription> = emptyList(), - val bypassTorApps: Collection<String> = emptyList(), - val selectedLocation: String = "", - val availableLocationIds: List<String> = emptyList(), - val forceRedraw: Boolean = false, -) { - fun getApps(): List<Pair<ApplicationDescription, Boolean>> { - return availableApps.map { it to (it.packageName !in bypassTorApps) } - } - - val selectedLocationPosition get() = availableLocationIds.indexOf(selectedLocation) -} 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 deleted file mode 100644 index bbd6239..0000000 --- a/app/src/main/java/foundation/e/privacycentralapp/features/internetprivacy/InternetPrivacyViewModel.kt +++ /dev/null @@ -1,157 +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.internetprivacy - -import androidx.annotation.StringRes -import androidx.lifecycle.ViewModel -import androidx.lifecycle.viewModelScope -import foundation.e.privacycentralapp.R -import foundation.e.privacycentralapp.domain.entities.InternetPrivacyMode -import foundation.e.privacycentralapp.domain.usecases.AppListUseCase -import foundation.e.privacycentralapp.domain.usecases.GetQuickPrivacyStateUseCase -import foundation.e.privacycentralapp.domain.usecases.IpScramblingStateUseCase -import foundation.e.privacymodules.ipscramblermodule.IIpScramblerModule -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.FlowPreview -import kotlinx.coroutines.flow.MutableSharedFlow -import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.asSharedFlow -import kotlinx.coroutines.flow.asStateFlow -import kotlinx.coroutines.flow.debounce -import kotlinx.coroutines.flow.map -import kotlinx.coroutines.flow.merge -import kotlinx.coroutines.flow.update -import kotlinx.coroutines.launch -import kotlinx.coroutines.withContext - -class InternetPrivacyViewModel( - private val ipScramblerModule: IIpScramblerModule, - private val getQuickPrivacyStateUseCase: GetQuickPrivacyStateUseCase, - private val ipScramblingStateUseCase: IpScramblingStateUseCase, - private val appListUseCase: AppListUseCase -) : ViewModel() { - companion object { - private const val WARNING_LOADING_LONG_DELAY = 5 * 1000L - } - - private val _state = MutableStateFlow(InternetPrivacyState()) - val state = _state.asStateFlow() - - private val _singleEvents = MutableSharedFlow<SingleEvent>() - val singleEvents = _singleEvents.asSharedFlow() - - val availablesLocationsIds = listOf("", *ipScramblerModule.getAvailablesLocations().sorted().toTypedArray()) - - init { - viewModelScope.launch(Dispatchers.IO) { - _state.update { - it.copy( - mode = ipScramblingStateUseCase.internetPrivacyMode.value, - availableLocationIds = availablesLocationsIds, - selectedLocation = ipScramblerModule.exitCountry - ) - } - } - } - - @OptIn(FlowPreview::class) - suspend fun doOnStartedState() = withContext(Dispatchers.IO) { - launch { - merge( - appListUseCase.getAppsUsingInternet().map { apps -> - _state.update { s -> - s.copy( - availableApps = apps, - bypassTorApps = ipScramblingStateUseCase.bypassTorApps - ) - } - }, - ipScramblingStateUseCase.internetPrivacyMode.map { - _state.update { s -> s.copy(mode = it) } - } - ).collect {} - } - - launch { - ipScramblingStateUseCase.internetPrivacyMode - .map { it == InternetPrivacyMode.HIDE_IP_LOADING } - .debounce(WARNING_LOADING_LONG_DELAY) - .collect { - if (it) _singleEvents.emit( - SingleEvent.ErrorEvent(R.string.ipscrambling_warning_starting_long) - ) - } - } - - launch { - getQuickPrivacyStateUseCase.otherVpnRunning.collect { - _singleEvents.emit( - SingleEvent.ErrorEvent( - R.string.ipscrambling_error_always_on_vpn_already_running, - listOf(it.label ?: "") - ) - ) - _state.update { it.copy(forceRedraw = !it.forceRedraw) } - } - } - } - - fun submitAction(action: Action) = viewModelScope.launch { - when (action) { - is Action.UseRealIPAction -> actionUseRealIP() - is Action.UseHiddenIPAction -> actionUseHiddenIP() - is Action.ToggleAppIpScrambled -> actionToggleAppIpScrambled(action) - is Action.SelectLocationAction -> actionSelectLocation(action) - } - } - - private fun actionUseRealIP() { - ipScramblingStateUseCase.toggle(hideIp = false) - } - - private fun actionUseHiddenIP() { - ipScramblingStateUseCase.toggle(hideIp = true) - } - - private suspend fun actionToggleAppIpScrambled(action: Action.ToggleAppIpScrambled) = withContext(Dispatchers.IO) { - ipScramblingStateUseCase.toggleBypassTor(action.packageName) - _state.update { it.copy(bypassTorApps = ipScramblingStateUseCase.bypassTorApps) } - } - - private suspend fun actionSelectLocation(action: Action.SelectLocationAction) = withContext(Dispatchers.IO) { - val locationId = _state.value.availableLocationIds[action.position] - if (locationId != ipScramblerModule.exitCountry) { - ipScramblerModule.exitCountry = locationId - _state.update { it.copy(selectedLocation = locationId) } - } - } - - sealed class SingleEvent { - data class ErrorEvent( - @StringRes val errorResId: Int, - val args: List<Any> = emptyList() - ) : SingleEvent() - } - - sealed class Action { - object UseRealIPAction : Action() - object UseHiddenIPAction : Action() - data class ToggleAppIpScrambled(val packageName: String) : Action() - data class SelectLocationAction(val position: Int) : Action() - } -} 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 deleted file mode 100644 index 9e3f854..0000000 --- a/app/src/main/java/foundation/e/privacycentralapp/features/location/FakeLocationFragment.kt +++ /dev/null @@ -1,376 +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.location - -import android.Manifest -import android.annotation.SuppressLint -import android.content.Context -import android.location.Location -import android.os.Bundle -import android.text.Editable -import android.view.View -import android.widget.Toast -import androidx.activity.result.contract.ActivityResultContracts -import androidx.annotation.NonNull -import androidx.core.view.isVisible -import androidx.core.widget.addTextChangedListener -import androidx.fragment.app.viewModels -import androidx.lifecycle.Lifecycle -import androidx.lifecycle.lifecycleScope -import androidx.lifecycle.repeatOnLifecycle -import com.google.android.material.textfield.TextInputEditText -import com.google.android.material.textfield.TextInputLayout -import com.google.android.material.textfield.TextInputLayout.END_ICON_CUSTOM -import com.google.android.material.textfield.TextInputLayout.END_ICON_NONE -import com.mapbox.mapboxsdk.Mapbox -import com.mapbox.mapboxsdk.camera.CameraUpdateFactory -import com.mapbox.mapboxsdk.geometry.LatLng -import com.mapbox.mapboxsdk.location.LocationComponent -import com.mapbox.mapboxsdk.location.LocationComponentActivationOptions -import com.mapbox.mapboxsdk.location.LocationUpdate -import com.mapbox.mapboxsdk.location.modes.CameraMode -import com.mapbox.mapboxsdk.location.modes.RenderMode -import com.mapbox.mapboxsdk.maps.MapboxMap -import com.mapbox.mapboxsdk.maps.Style -import foundation.e.privacycentralapp.DependencyContainer -import foundation.e.privacycentralapp.PrivacyCentralApplication -import foundation.e.privacycentralapp.R -import foundation.e.privacycentralapp.common.NavToolbarFragment -import foundation.e.privacycentralapp.databinding.FragmentFakeLocationBinding -import foundation.e.privacycentralapp.domain.entities.LocationMode -import foundation.e.privacycentralapp.features.location.FakeLocationViewModel.Action -import kotlinx.coroutines.Job -import kotlinx.coroutines.delay -import kotlinx.coroutines.ensureActive -import kotlinx.coroutines.launch - -class FakeLocationFragment : NavToolbarFragment(R.layout.fragment_fake_location) { - - private var isFirstLaunch: Boolean = true - - private val dependencyContainer: DependencyContainer by lazy { - (this.requireActivity().application as PrivacyCentralApplication).dependencyContainer - } - - private val viewModel: FakeLocationViewModel by viewModels { - dependencyContainer.viewModelsFactory - } - - private var _binding: FragmentFakeLocationBinding? = null - private val binding get() = _binding!! - - private var mapboxMap: MapboxMap? = null - private var locationComponent: LocationComponent? = null - - private var inputJob: Job? = null - - private val locationPermissionRequest = registerForActivityResult( - ActivityResultContracts.RequestMultiplePermissions() - ) { permissions -> - if (permissions.getOrDefault(Manifest.permission.ACCESS_FINE_LOCATION, false) || - permissions.getOrDefault(Manifest.permission.ACCESS_COARSE_LOCATION, false) - ) { - viewModel.submitAction(Action.StartListeningLocation) - } // TODO: else. - } - - companion object { - private const val DEBOUNCE_PERIOD = 1000L - } - - override fun onAttach(context: Context) { - super.onAttach(context) - Mapbox.getInstance(requireContext(), getString(R.string.mapbox_key)) - } - - override fun getTitle(): String = getString(R.string.location_title) - - private fun displayToast(message: String) { - Toast.makeText(requireContext(), message, Toast.LENGTH_SHORT) - .show() - } - - override fun onViewCreated(view: View, savedInstanceState: Bundle?) { - super.onViewCreated(view, savedInstanceState) - _binding = FragmentFakeLocationBinding.bind(view) - - binding.mapView.setup(savedInstanceState) { mapboxMap -> - this.mapboxMap = mapboxMap - mapboxMap.uiSettings.isRotateGesturesEnabled = false - mapboxMap.setStyle(Style.MAPBOX_STREETS) { style -> - enableLocationPlugin(style) - - mapboxMap.addOnCameraMoveListener { - if (binding.mapView.isEnabled) { - mapboxMap.cameraPosition.target.let { - viewModel.submitAction( - Action.SetSpecificLocationAction( - it.latitude.toFloat(), - it.longitude.toFloat() - ) - ) - } - } - } - // Bind click listeners once map is ready. - bindClickListeners() - - render(viewModel.state.value) - viewLifecycleOwner.lifecycleScope.launch { - viewLifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) { - viewModel.singleEvents.collect { event -> - if (event is FakeLocationViewModel.SingleEvent.LocationUpdatedEvent) { - updateLocation(event.location, event.mode) - } - } - } - } - } - } - - viewLifecycleOwner.lifecycleScope.launch { - viewLifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) { - render(viewModel.state.value) - viewModel.state.collect(::render) - } - } - - viewLifecycleOwner.lifecycleScope.launch { - viewLifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) { - viewModel.singleEvents.collect { event -> - when (event) { - is FakeLocationViewModel.SingleEvent.ErrorEvent -> { - displayToast(event.error) - } - is FakeLocationViewModel.SingleEvent.RequestLocationPermission -> { - // TODO for standalone: rationale dialog - locationPermissionRequest.launch( - arrayOf( - Manifest.permission.ACCESS_FINE_LOCATION, - Manifest.permission.ACCESS_COARSE_LOCATION - ) - ) - } - is FakeLocationViewModel.SingleEvent.LocationUpdatedEvent -> { - // Nothing here, another collect linked to mapbox view. - } - } - } - } - } - - viewLifecycleOwner.lifecycleScope.launch { - viewLifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) { - viewModel.doOnStartedState() - } - } - } - - private fun getCoordinatesAfterTextChanged( - inputLayout: TextInputLayout, - editText: TextInputEditText, - isLat: Boolean - ) = { editable: Editable? -> - inputJob?.cancel() - if (editable != null && editable.isNotEmpty() && editText.isEnabled) { - inputJob = lifecycleScope.launch { - delay(DEBOUNCE_PERIOD) - ensureActive() - try { - val value = editable.toString().toFloat() - val maxValue = if (isLat) 90f else 180f - - if (value > maxValue || value < -maxValue) { - throw NumberFormatException("value $value is out of bounds") - } - inputLayout.error = null - - inputLayout.setEndIconDrawable(R.drawable.ic_valid) - inputLayout.endIconMode = END_ICON_CUSTOM - - // Here, value is valid, try to send the values - try { - val lat = binding.edittextLatitude.text.toString().toFloat() - val lon = binding.edittextLongitude.text.toString().toFloat() - if (lat <= 90f && lat >= -90f && lon <= 180f && lon >= -180f) { - mapboxMap?.moveCamera( - CameraUpdateFactory.newLatLng( - LatLng(lat.toDouble(), lon.toDouble()) - ) - ) - } - } catch (e: NumberFormatException) { - } - } catch (e: NumberFormatException) { - inputLayout.endIconMode = END_ICON_NONE - inputLayout.error = getString(R.string.location_input_error) - } - } - } - } - - @SuppressLint("ClickableViewAccessibility") - private fun bindClickListeners() { - binding.radioUseRealLocation.setOnClickListener { - viewModel.submitAction(Action.UseRealLocationAction) - } - binding.radioUseRandomLocation.setOnClickListener { - viewModel.submitAction(Action.UseRandomLocationAction) - } - binding.radioUseSpecificLocation.setOnClickListener { - mapboxMap?.cameraPosition?.target?.let { - viewModel.submitAction( - Action.SetSpecificLocationAction(it.latitude.toFloat(), it.longitude.toFloat()) - ) - } - } - binding.edittextLatitude.addTextChangedListener( - afterTextChanged = getCoordinatesAfterTextChanged( - binding.textlayoutLatitude, - binding.edittextLatitude, - true - ) - ) - - binding.edittextLongitude.addTextChangedListener( - afterTextChanged = getCoordinatesAfterTextChanged( - binding.textlayoutLongitude, - binding.edittextLongitude, - false - ) - ) - } - - @SuppressLint("MissingPermission") - private fun render(state: FakeLocationState) { - binding.radioUseRandomLocation.isChecked = state.mode == LocationMode.RANDOM_LOCATION - - binding.radioUseSpecificLocation.isChecked = state.mode == LocationMode.SPECIFIC_LOCATION - - binding.radioUseRealLocation.isChecked = state.mode == LocationMode.REAL_LOCATION - - binding.mapView.isEnabled = (state.mode == LocationMode.SPECIFIC_LOCATION) - - if (state.mode == LocationMode.REAL_LOCATION) { - binding.centeredMarker.isVisible = false - } else { - binding.mapLoader.isVisible = false - binding.mapOverlay.isVisible = state.mode != LocationMode.SPECIFIC_LOCATION - binding.centeredMarker.isVisible = true - - mapboxMap?.moveCamera( - CameraUpdateFactory.newLatLng( - LatLng( - state.specificLatitude?.toDouble() ?: 0.0, - state.specificLongitude?.toDouble() ?: 0.0 - ) - ) - ) - } - - binding.textlayoutLatitude.isVisible = (state.mode == LocationMode.SPECIFIC_LOCATION) - binding.textlayoutLongitude.isVisible = (state.mode == LocationMode.SPECIFIC_LOCATION) - - binding.edittextLatitude.setText(state.specificLatitude?.toString()) - binding.edittextLongitude.setText(state.specificLongitude?.toString()) - } - - @SuppressLint("MissingPermission") - private fun updateLocation(lastLocation: Location?, mode: LocationMode) { - lastLocation?.let { location -> - locationComponent?.isLocationComponentEnabled = true - val locationUpdate = LocationUpdate.Builder() - .location(location) - .animationDuration(100) - .build() - locationComponent?.forceLocationUpdate(locationUpdate) - - if (mode == LocationMode.REAL_LOCATION) { - binding.mapLoader.isVisible = false - binding.mapOverlay.isVisible = false - - val update = CameraUpdateFactory.newLatLng( - LatLng(location.latitude, location.longitude) - ) - - if (isFirstLaunch) { - mapboxMap?.moveCamera(update) - isFirstLaunch = false - } else { - mapboxMap?.animateCamera(update) - } - } - } ?: run { - locationComponent?.isLocationComponentEnabled = false - if (mode == LocationMode.REAL_LOCATION) { - binding.mapLoader.isVisible = true - binding.mapOverlay.isVisible = true - } - } - } - - @SuppressLint("MissingPermission") - private fun enableLocationPlugin(@NonNull loadedMapStyle: Style) { - // Check if permissions are enabled and if not request - locationComponent = mapboxMap?.locationComponent - locationComponent?.activateLocationComponent( - LocationComponentActivationOptions.builder( - requireContext(), loadedMapStyle - ).useDefaultLocationEngine(false).build() - ) - locationComponent?.isLocationComponentEnabled = true - locationComponent?.cameraMode = CameraMode.NONE - locationComponent?.renderMode = RenderMode.NORMAL - } - - override fun onStart() { - super.onStart() - binding.mapView.onStart() - } - - override fun onResume() { - super.onResume() - viewModel.submitAction(Action.StartListeningLocation) - binding.mapView.onResume() - } - - override fun onPause() { - super.onPause() - viewModel.submitAction(Action.StopListeningLocation) - binding.mapView.onPause() - } - - override fun onStop() { - super.onStop() - binding.mapView.onStop() - } - - override fun onLowMemory() { - super.onLowMemory() - binding.mapView.onLowMemory() - } - - override fun onDestroyView() { - super.onDestroyView() - binding.mapView.onDestroy() - mapboxMap = null - locationComponent = null - inputJob = null - _binding = null - } -} diff --git a/app/src/main/java/foundation/e/privacycentralapp/features/location/FakeLocationMapView.kt b/app/src/main/java/foundation/e/privacycentralapp/features/location/FakeLocationMapView.kt deleted file mode 100644 index e71bfcc..0000000 --- a/app/src/main/java/foundation/e/privacycentralapp/features/location/FakeLocationMapView.kt +++ /dev/null @@ -1,53 +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.location - -import android.annotation.SuppressLint -import android.content.Context -import android.os.Bundle -import android.util.AttributeSet -import android.view.MotionEvent -import com.mapbox.mapboxsdk.maps.MapView -import com.mapbox.mapboxsdk.maps.OnMapReadyCallback - -class FakeLocationMapView @JvmOverloads constructor( - context: Context, - attrs: AttributeSet? = null, - defStyleAttr: Int = 0 -) : MapView(context, attrs, defStyleAttr) { - - /** - * Overrides onTouchEvent because this MapView is part of a scroll view - * and we want this map view to consume all touch events originating on this view. - */ - @SuppressLint("ClickableViewAccessibility") - override fun onTouchEvent(event: MotionEvent?): Boolean { - when (event?.action) { - MotionEvent.ACTION_DOWN -> parent.requestDisallowInterceptTouchEvent(true) - MotionEvent.ACTION_UP -> parent.requestDisallowInterceptTouchEvent(false) - } - super.onTouchEvent(event) - return true - } -} - -fun FakeLocationMapView.setup(savedInstanceState: Bundle?, callback: OnMapReadyCallback) = - this.apply { - onCreate(savedInstanceState) - getMapAsync(callback) - } diff --git a/app/src/main/java/foundation/e/privacycentralapp/features/location/FakeLocationState.kt b/app/src/main/java/foundation/e/privacycentralapp/features/location/FakeLocationState.kt deleted file mode 100644 index 50d7a14..0000000 --- a/app/src/main/java/foundation/e/privacycentralapp/features/location/FakeLocationState.kt +++ /dev/null @@ -1,29 +0,0 @@ -/* - * Copyright (C) 2022 E FOUNDATION - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see <https://www.gnu.org/licenses/>. - */ - -package foundation.e.privacycentralapp.features.location - -import android.location.Location -import foundation.e.privacycentralapp.domain.entities.LocationMode - -data class FakeLocationState( - val mode: LocationMode = LocationMode.REAL_LOCATION, - val currentLocation: Location? = null, - val specificLatitude: Float? = null, - val specificLongitude: Float? = null, - val forceRefresh: Boolean = false, -) diff --git a/app/src/main/java/foundation/e/privacycentralapp/features/location/FakeLocationViewModel.kt b/app/src/main/java/foundation/e/privacycentralapp/features/location/FakeLocationViewModel.kt deleted file mode 100644 index 1cdf9f4..0000000 --- a/app/src/main/java/foundation/e/privacycentralapp/features/location/FakeLocationViewModel.kt +++ /dev/null @@ -1,126 +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.location - -import android.location.Location -import androidx.lifecycle.ViewModel -import androidx.lifecycle.viewModelScope -import foundation.e.privacycentralapp.domain.entities.LocationMode -import foundation.e.privacycentralapp.domain.usecases.FakeLocationStateUseCase -import foundation.e.privacycentralapp.domain.usecases.GetQuickPrivacyStateUseCase -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.FlowPreview -import kotlinx.coroutines.flow.MutableSharedFlow -import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.asSharedFlow -import kotlinx.coroutines.flow.asStateFlow -import kotlinx.coroutines.flow.debounce -import kotlinx.coroutines.flow.map -import kotlinx.coroutines.flow.merge -import kotlinx.coroutines.flow.update -import kotlinx.coroutines.launch -import kotlinx.coroutines.withContext -import kotlin.time.Duration.Companion.milliseconds - -class FakeLocationViewModel( - private val getQuickPrivacyStateUseCase: GetQuickPrivacyStateUseCase, - private val fakeLocationStateUseCase: FakeLocationStateUseCase -) : ViewModel() { - companion object { - private val SET_SPECIFIC_LOCATION_DELAY = 200.milliseconds - } - - private val _state = MutableStateFlow(FakeLocationState()) - val state = _state.asStateFlow() - - private val _singleEvents = MutableSharedFlow<SingleEvent>() - val singleEvents = _singleEvents.asSharedFlow() - - private val specificLocationInputFlow = MutableSharedFlow<Action.SetSpecificLocationAction>() - - @OptIn(FlowPreview::class) - suspend fun doOnStartedState() = withContext(Dispatchers.Main) { - launch { - merge( - fakeLocationStateUseCase.configuredLocationMode.map { (mode, lat, lon) -> - _state.update { s -> - s.copy( - mode = mode, - specificLatitude = lat, - specificLongitude = lon - ) - } - }, - specificLocationInputFlow - .debounce(SET_SPECIFIC_LOCATION_DELAY).map { action -> - fakeLocationStateUseCase.setSpecificLocation(action.latitude, action.longitude) - } - ).collect {} - } - - launch { - fakeLocationStateUseCase.currentLocation.collect { location -> - _singleEvents.emit( - SingleEvent.LocationUpdatedEvent( - mode = _state.value.mode, - location = location - ) - ) - } - } - } - - fun submitAction(action: Action) = viewModelScope.launch { - when (action) { - is Action.StartListeningLocation -> actionStartListeningLocation() - is Action.StopListeningLocation -> fakeLocationStateUseCase.stopListeningLocation() - is Action.SetSpecificLocationAction -> setSpecificLocation(action) - is Action.UseRandomLocationAction -> fakeLocationStateUseCase.setRandomLocation() - is Action.UseRealLocationAction -> - fakeLocationStateUseCase.stopFakeLocation() - } - } - - private suspend fun actionStartListeningLocation() { - val started = fakeLocationStateUseCase.startListeningLocation() - if (!started) { - _singleEvents.emit(SingleEvent.RequestLocationPermission) - } - } - - private suspend fun setSpecificLocation(action: Action.SetSpecificLocationAction) { - specificLocationInputFlow.emit(action) - } - - sealed class SingleEvent { - data class LocationUpdatedEvent(val mode: LocationMode, val location: Location?) : SingleEvent() - object RequestLocationPermission : SingleEvent() - data class ErrorEvent(val error: String) : SingleEvent() - } - - sealed class Action { - object StartListeningLocation : Action() - object StopListeningLocation : Action() - object UseRealLocationAction : Action() - object UseRandomLocationAction : Action() - data class SetSpecificLocationAction( - val latitude: Float, - val longitude: Float - ) : Action() - } -} diff --git a/app/src/main/java/foundation/e/privacycentralapp/features/trackers/TrackersFragment.kt b/app/src/main/java/foundation/e/privacycentralapp/features/trackers/TrackersFragment.kt deleted file mode 100644 index cb32c2c..0000000 --- a/app/src/main/java/foundation/e/privacycentralapp/features/trackers/TrackersFragment.kt +++ /dev/null @@ -1,218 +0,0 @@ -/* - * Copyright (C) 2021 E FOUNDATION, 2022 MURENA SAS - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see <https://www.gnu.org/licenses/>. - */ - -package foundation.e.privacycentralapp.features.trackers - -import android.content.ActivityNotFoundException -import android.content.Intent -import android.os.Bundle -import android.text.Spannable -import android.text.SpannableString -import android.text.method.LinkMovementMethod -import android.text.style.ClickableSpan -import android.text.style.ForegroundColorSpan -import android.text.style.UnderlineSpan -import android.view.View -import android.widget.Toast -import androidx.core.content.ContextCompat -import androidx.core.view.isVisible -import androidx.fragment.app.commit -import androidx.fragment.app.replace -import androidx.fragment.app.viewModels -import androidx.lifecycle.Lifecycle -import androidx.lifecycle.lifecycleScope -import androidx.lifecycle.repeatOnLifecycle -import androidx.recyclerview.widget.LinearLayoutManager -import foundation.e.privacycentralapp.DependencyContainer -import foundation.e.privacycentralapp.PrivacyCentralApplication -import foundation.e.privacycentralapp.R -import foundation.e.privacycentralapp.common.AppsAdapter -import foundation.e.privacycentralapp.common.GraphHolder -import foundation.e.privacycentralapp.common.NavToolbarFragment -import foundation.e.privacycentralapp.common.setToolTipForAsterisk -import foundation.e.privacycentralapp.databinding.FragmentTrackersBinding -import foundation.e.privacycentralapp.databinding.TrackersItemGraphBinding -import foundation.e.privacycentralapp.domain.entities.TrackersPeriodicStatistics -import foundation.e.privacycentralapp.features.trackers.apptrackers.AppTrackersFragment -import kotlinx.coroutines.launch - -class TrackersFragment : - NavToolbarFragment(R.layout.fragment_trackers) { - - private val dependencyContainer: DependencyContainer by lazy { - (this.requireActivity().application as PrivacyCentralApplication).dependencyContainer - } - - private val viewModel: TrackersViewModel by viewModels { dependencyContainer.viewModelsFactory } - - private var _binding: FragmentTrackersBinding? = null - private val binding get() = _binding!! - - private var dayGraphHolder: GraphHolder? = null - private var monthGraphHolder: GraphHolder? = null - private var yearGraphHolder: GraphHolder? = null - - override fun onViewCreated(view: View, savedInstanceState: Bundle?) { - super.onViewCreated(view, savedInstanceState) - - _binding = FragmentTrackersBinding.bind(view) - - dayGraphHolder = GraphHolder(binding.graphDay.graph, requireContext(), false) - monthGraphHolder = GraphHolder(binding.graphMonth.graph, requireContext(), false) - yearGraphHolder = GraphHolder(binding.graphYear.graph, requireContext(), false) - - binding.apps.apply { - layoutManager = LinearLayoutManager(requireContext()) - setHasFixedSize(true) - adapter = AppsAdapter(R.layout.trackers_item_app) { appUid -> - viewModel.submitAction( - TrackersViewModel.Action.ClickAppAction(appUid) - ) - } - } - - val infoText = getString(R.string.trackers_info) - val moreText = getString(R.string.trackers_info_more) - - val spannable = SpannableString("$infoText $moreText") - val startIndex = infoText.length + 1 - val endIndex = spannable.length - spannable.setSpan( - ForegroundColorSpan(ContextCompat.getColor(requireContext(), R.color.accent)), - startIndex, - endIndex, - Spannable.SPAN_INCLUSIVE_EXCLUSIVE - ) - spannable.setSpan(UnderlineSpan(), startIndex, endIndex, Spannable.SPAN_INCLUSIVE_EXCLUSIVE) - spannable.setSpan( - object : ClickableSpan() { - override fun onClick(p0: View) { - viewModel.submitAction(TrackersViewModel.Action.ClickLearnMore) - } - }, - startIndex, endIndex, Spannable.SPAN_INCLUSIVE_EXCLUSIVE - ) - - with(binding.trackersInfo) { - linksClickable = true - isClickable = true - movementMethod = LinkMovementMethod.getInstance() - text = spannable - } - - setToolTipForAsterisk( - textView = binding.trackersAppsListTitle, - textId = R.string.trackers_applist_title, - tooltipTextId = R.string.trackers_applist_infos - ) - - viewLifecycleOwner.lifecycleScope.launch { - viewLifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) { - render(viewModel.state.value) - viewModel.state.collect(::render) - } - } - - viewLifecycleOwner.lifecycleScope.launch { - viewLifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) { - viewModel.singleEvents.collect { event -> - when (event) { - is TrackersViewModel.SingleEvent.ErrorEvent -> { - displayToast(event.error) - } - is TrackersViewModel.SingleEvent.OpenAppDetailsEvent -> { - requireActivity().supportFragmentManager.commit { - replace<AppTrackersFragment>( - R.id.container, - args = AppTrackersFragment.buildArgs( - event.appDesc.label.toString(), - event.appDesc.packageName, - event.appDesc.uid - ) - ) - setReorderingAllowed(true) - addToBackStack("apptrackers") - } - } - is TrackersViewModel.SingleEvent.OpenUrl -> { - try { - startActivity(Intent(Intent.ACTION_VIEW, event.url)) - } catch (e: ActivityNotFoundException) { - Toast.makeText( - requireContext(), - R.string.error_no_activity_view_url, - Toast.LENGTH_SHORT - ).show() - } - } - } - } - } - } - - viewLifecycleOwner.lifecycleScope.launch { - viewLifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) { - viewModel.doOnStartedState() - } - } - } - - private fun displayToast(message: String) { - Toast.makeText(requireContext(), message, Toast.LENGTH_SHORT) - .show() - } - - override fun getTitle() = getString(R.string.trackers_title) - - private fun render(state: TrackersState) { - state.dayStatistics?.let { renderGraph(it, dayGraphHolder!!, binding.graphDay) } - state.monthStatistics?.let { renderGraph(it, monthGraphHolder!!, binding.graphMonth) } - state.yearStatistics?.let { renderGraph(it, yearGraphHolder!!, binding.graphYear) } - - state.apps?.let { - binding.apps.post { - (binding.apps.adapter as AppsAdapter?)?.dataSet = it - } - } - } - - private fun renderGraph( - statistics: TrackersPeriodicStatistics, - graphHolder: GraphHolder, - graphBinding: TrackersItemGraphBinding - ) { - if (statistics.callsBlockedNLeaked.all { it.first == 0 && it.second == 0 }) { - graphBinding.graph.visibility = View.INVISIBLE - graphBinding.graphEmpty.isVisible = true - } else { - graphBinding.graph.isVisible = true - graphBinding.graphEmpty.isVisible = false - graphHolder.data = statistics.callsBlockedNLeaked - graphHolder.labels = statistics.periods - graphBinding.trackersCountLabel.text = - getString(R.string.trackers_count_label, statistics.trackersCount) - } - } - - override fun onDestroyView() { - super.onDestroyView() - dayGraphHolder = null - monthGraphHolder = null - yearGraphHolder = null - _binding = null - } -} diff --git a/app/src/main/java/foundation/e/privacycentralapp/features/trackers/TrackersState.kt b/app/src/main/java/foundation/e/privacycentralapp/features/trackers/TrackersState.kt deleted file mode 100644 index a3bb80a..0000000 --- a/app/src/main/java/foundation/e/privacycentralapp/features/trackers/TrackersState.kt +++ /dev/null @@ -1,28 +0,0 @@ -/* - * Copyright (C) 2022 E FOUNDATION - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see <https://www.gnu.org/licenses/>. - */ - -package foundation.e.privacycentralapp.features.trackers - -import foundation.e.privacycentralapp.domain.entities.AppWithCounts -import foundation.e.privacycentralapp.domain.entities.TrackersPeriodicStatistics - -data class TrackersState( - val dayStatistics: TrackersPeriodicStatistics? = null, - val monthStatistics: TrackersPeriodicStatistics? = null, - val yearStatistics: TrackersPeriodicStatistics? = null, - val apps: List<AppWithCounts>? = null, -) diff --git a/app/src/main/java/foundation/e/privacycentralapp/features/trackers/TrackersViewModel.kt b/app/src/main/java/foundation/e/privacycentralapp/features/trackers/TrackersViewModel.kt deleted file mode 100644 index 8b5cc32..0000000 --- a/app/src/main/java/foundation/e/privacycentralapp/features/trackers/TrackersViewModel.kt +++ /dev/null @@ -1,95 +0,0 @@ -/* - * Copyright (C) 2021 E FOUNDATION, 2022 MURENA SAS - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see <https://www.gnu.org/licenses/>. - */ - -package foundation.e.privacycentralapp.features.trackers - -import android.net.Uri -import androidx.lifecycle.ViewModel -import androidx.lifecycle.viewModelScope -import foundation.e.privacycentralapp.domain.entities.AppWithCounts -import foundation.e.privacycentralapp.domain.usecases.TrackersStatisticsUseCase -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.flow.MutableSharedFlow -import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.asSharedFlow -import kotlinx.coroutines.flow.asStateFlow -import kotlinx.coroutines.flow.map -import kotlinx.coroutines.flow.merge -import kotlinx.coroutines.flow.update -import kotlinx.coroutines.launch -import kotlinx.coroutines.withContext - -class TrackersViewModel( - private val trackersStatisticsUseCase: TrackersStatisticsUseCase -) : ViewModel() { - - companion object { - private const val URL_LEARN_MORE_ABOUT_TRACKERS = - "https://doc.e.foundation/support-topics/advanced_privacy#trackers-blocker" - } - - private val _state = MutableStateFlow(TrackersState()) - val state = _state.asStateFlow() - - private val _singleEvents = MutableSharedFlow<SingleEvent>() - val singleEvents = _singleEvents.asSharedFlow() - - suspend fun doOnStartedState() = withContext(Dispatchers.IO) { - merge( - trackersStatisticsUseCase.listenUpdates().map { - trackersStatisticsUseCase.getDayMonthYearStatistics() - .let { (day, month, year) -> - _state.update { s -> - s.copy( - dayStatistics = day, - monthStatistics = month, - yearStatistics = year - ) - } - } - }, - trackersStatisticsUseCase.getAppsWithCounts().map { - _state.update { s -> s.copy(apps = it) } - } - ).collect {} - } - - fun submitAction(action: Action) = viewModelScope.launch { - when (action) { - is Action.ClickAppAction -> actionClickApp(action) - is Action.ClickLearnMore -> - _singleEvents.emit(SingleEvent.OpenUrl(Uri.parse(URL_LEARN_MORE_ABOUT_TRACKERS))) - } - } - - private suspend fun actionClickApp(action: Action.ClickAppAction) { - state.value.apps?.find { it.uid == action.appUid }?.let { - _singleEvents.emit(SingleEvent.OpenAppDetailsEvent(it)) - } - } - - sealed class SingleEvent { - data class ErrorEvent(val error: String) : SingleEvent() - data class OpenAppDetailsEvent(val appDesc: AppWithCounts) : SingleEvent() - data class OpenUrl(val url: Uri) : SingleEvent() - } - - sealed class Action { - data class ClickAppAction(val appUid: Int) : Action() - object ClickLearnMore : Action() - } -} diff --git a/app/src/main/java/foundation/e/privacycentralapp/features/trackers/apptrackers/AppTrackersFragment.kt b/app/src/main/java/foundation/e/privacycentralapp/features/trackers/apptrackers/AppTrackersFragment.kt deleted file mode 100644 index 888c140..0000000 --- a/app/src/main/java/foundation/e/privacycentralapp/features/trackers/apptrackers/AppTrackersFragment.kt +++ /dev/null @@ -1,189 +0,0 @@ -/* - * Copyright (C) 2023 MURENA SAS - * 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.trackers.apptrackers - -import android.content.ActivityNotFoundException -import android.content.Intent -import android.os.Bundle -import android.view.View -import android.widget.Toast -import androidx.core.os.bundleOf -import androidx.core.view.isVisible -import androidx.fragment.app.viewModels -import androidx.lifecycle.Lifecycle -import androidx.lifecycle.lifecycleScope -import androidx.lifecycle.repeatOnLifecycle -import androidx.recyclerview.widget.LinearLayoutManager -import com.google.android.material.snackbar.Snackbar -import foundation.e.privacycentralapp.DependencyContainer -import foundation.e.privacycentralapp.PrivacyCentralApplication -import foundation.e.privacycentralapp.R -import foundation.e.privacycentralapp.common.NavToolbarFragment -import foundation.e.privacycentralapp.databinding.ApptrackersFragmentBinding -import kotlinx.coroutines.launch - -class AppTrackersFragment : NavToolbarFragment(R.layout.apptrackers_fragment) { - companion object { - private val PARAM_LABEL = "PARAM_LABEL" - private val PARAM_PACKAGE_NAME = "PARAM_PACKAGE_NAME" - - const val PARAM_APP_UID = "PARAM_APP_UID" - - fun buildArgs(label: String, packageName: String, appUid: Int): Bundle = bundleOf( - PARAM_LABEL to label, - PARAM_PACKAGE_NAME to packageName, - PARAM_APP_UID to appUid - ) - } - - private val dependencyContainer: DependencyContainer by lazy { - (this.requireActivity().application as PrivacyCentralApplication).dependencyContainer - } - - private val viewModel: AppTrackersViewModel by viewModels { - dependencyContainer.viewModelsFactory - } - - private var _binding: ApptrackersFragmentBinding? = null - private val binding get() = _binding!! - - override fun onCreate(savedInstanceState: Bundle?) { - super.onCreate(savedInstanceState) - if (arguments == null || - requireArguments().getInt(PARAM_APP_UID, Int.MIN_VALUE) == Int.MIN_VALUE - ) { - activity?.supportFragmentManager?.popBackStack() - } - } - - private fun displayToast(message: String) { - Toast.makeText(requireContext(), message, Toast.LENGTH_SHORT) - .show() - } - - override fun getTitle(): String = requireArguments().getString(PARAM_LABEL) ?: "" - - override fun onViewCreated(view: View, savedInstanceState: Bundle?) { - super.onViewCreated(view, savedInstanceState) - _binding = ApptrackersFragmentBinding.bind(view) - - binding.blockAllToggle.setOnClickListener { - viewModel.submitAction(AppTrackersViewModel.Action.BlockAllToggleAction(binding.blockAllToggle.isChecked)) - } - binding.btnReset.setOnClickListener { - viewModel.submitAction(AppTrackersViewModel.Action.ResetAllTrackers) - } - - binding.trackers.apply { - layoutManager = LinearLayoutManager(requireContext()) - setHasFixedSize(true) - adapter = ToggleTrackersAdapter( - R.layout.apptrackers_item_tracker_toggle, - onToggleSwitch = { tracker, isBlocked -> - viewModel.submitAction(AppTrackersViewModel.Action.ToggleTrackerAction(tracker, isBlocked)) - }, - onClickTitle = { viewModel.submitAction(AppTrackersViewModel.Action.ClickTracker(it)) }, - ) - } - - viewLifecycleOwner.lifecycleScope.launch { - viewLifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) { - viewModel.singleEvents.collect { event -> - when (event) { - is AppTrackersViewModel.SingleEvent.ErrorEvent -> - displayToast(getString(event.errorResId)) - is AppTrackersViewModel.SingleEvent.OpenUrl -> - try { - startActivity(Intent(Intent.ACTION_VIEW, event.url)) - } catch (e: ActivityNotFoundException) { - Toast.makeText( - requireContext(), - R.string.error_no_activity_view_url, - Toast.LENGTH_SHORT - ).show() - } - is AppTrackersViewModel.SingleEvent.ToastTrackersControlDisabled -> - Snackbar.make( - binding.root, - R.string.apptrackers_tracker_control_disabled_message, - Snackbar.LENGTH_LONG - ).show() - } - } - } - } - - viewLifecycleOwner.lifecycleScope.launch { - viewLifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) { - viewModel.doOnStartedState() - } - } - - viewLifecycleOwner.lifecycleScope.launch { - viewLifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) { - render(viewModel.state.value) - viewModel.state.collect(::render) - } - } - } - - private fun render(state: AppTrackersState) { - binding.trackersCountSummary.text = if (state.getTrackersCount() == 0) "" - else getString( - R.string.apptrackers_trackers_count_summary, - state.getBlockedTrackersCount(), - state.getTrackersCount(), - state.blocked, - state.leaked - ) - - binding.blockAllToggle.isChecked = state.isBlockingActivated - - val trackersStatus = state.getTrackersStatus() - if (!trackersStatus.isNullOrEmpty()) { - binding.trackersListTitle.isVisible = state.isBlockingActivated - binding.trackers.isVisible = true - binding.trackers.post { - (binding.trackers.adapter as ToggleTrackersAdapter?)?.updateDataSet( - trackersStatus, - state.isBlockingActivated - ) - } - binding.noTrackersYet.isVisible = false - binding.btnReset.isVisible = true - } else { - binding.trackersListTitle.isVisible = false - binding.trackers.isVisible = false - binding.noTrackersYet.isVisible = true - binding.noTrackersYet.text = getString( - when { - !state.isBlockingActivated -> R.string.apptrackers_no_trackers_yet_block_off - state.isWhitelistEmpty -> R.string.apptrackers_no_trackers_yet_block_on - else -> R.string.app_trackers_no_trackers_yet_remaining_whitelist - } - ) - binding.btnReset.isVisible = state.isBlockingActivated && !state.isWhitelistEmpty - } - } - - override fun onDestroyView() { - super.onDestroyView() - _binding = null - } -} diff --git a/app/src/main/java/foundation/e/privacycentralapp/features/trackers/apptrackers/AppTrackersState.kt b/app/src/main/java/foundation/e/privacycentralapp/features/trackers/apptrackers/AppTrackersState.kt deleted file mode 100644 index a190a74..0000000 --- a/app/src/main/java/foundation/e/privacycentralapp/features/trackers/apptrackers/AppTrackersState.kt +++ /dev/null @@ -1,42 +0,0 @@ -/* - * Copyright (C) 2023 MURENA SAS - * Copyright (C) 2022 E FOUNDATION - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see <https://www.gnu.org/licenses/>. - */ - -package foundation.e.privacycentralapp.features.trackers.apptrackers - -import foundation.e.privacymodules.permissions.data.ApplicationDescription -import foundation.e.privacymodules.trackers.api.Tracker - -data class AppTrackersState( - val appDesc: ApplicationDescription? = null, - val isBlockingActivated: Boolean = false, - val trackersWithWhiteList: List<Pair<Tracker, Boolean>>? = null, - val leaked: Int = 0, - val blocked: Int = 0, - val isTrackersBlockingEnabled: Boolean = false, - val isWhitelistEmpty: Boolean = true, - val showQuickPrivacyDisabledMessage: Boolean = false, -) { - fun getTrackersStatus(): List<Pair<Tracker, Boolean>>? { - return trackersWithWhiteList?.map { it.first to !it.second } - } - - fun getTrackersCount() = trackersWithWhiteList?.size ?: 0 - fun getBlockedTrackersCount(): Int = if (isTrackersBlockingEnabled && isBlockingActivated) - trackersWithWhiteList?.count { !it.second } ?: 0 - else 0 -} diff --git a/app/src/main/java/foundation/e/privacycentralapp/features/trackers/apptrackers/AppTrackersViewModel.kt b/app/src/main/java/foundation/e/privacycentralapp/features/trackers/apptrackers/AppTrackersViewModel.kt deleted file mode 100644 index e5a94f9..0000000 --- a/app/src/main/java/foundation/e/privacycentralapp/features/trackers/apptrackers/AppTrackersViewModel.kt +++ /dev/null @@ -1,172 +0,0 @@ -/* - * Copyright (C) 2023 MURENA SAS - * 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.trackers.apptrackers - -import android.net.Uri -import androidx.annotation.StringRes -import androidx.lifecycle.ViewModel -import androidx.lifecycle.viewModelScope -import foundation.e.privacycentralapp.domain.entities.TrackerMode -import foundation.e.privacycentralapp.domain.usecases.GetQuickPrivacyStateUseCase -import foundation.e.privacycentralapp.domain.usecases.TrackersStateUseCase -import foundation.e.privacycentralapp.domain.usecases.TrackersStatisticsUseCase -import foundation.e.privacymodules.permissions.data.ApplicationDescription -import foundation.e.privacymodules.trackers.api.Tracker -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.flow.MutableSharedFlow -import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.asSharedFlow -import kotlinx.coroutines.flow.asStateFlow -import kotlinx.coroutines.flow.map -import kotlinx.coroutines.flow.merge -import kotlinx.coroutines.flow.update -import kotlinx.coroutines.launch -import kotlinx.coroutines.withContext - -class AppTrackersViewModel( - private val app: ApplicationDescription, - private val trackersStateUseCase: TrackersStateUseCase, - private val trackersStatisticsUseCase: TrackersStatisticsUseCase, - private val getQuickPrivacyStateUseCase: GetQuickPrivacyStateUseCase -) : ViewModel() { - companion object { - private const val exodusBaseUrl = "https://reports.exodus-privacy.eu.org/trackers/" - } - - private val _state = MutableStateFlow(AppTrackersState()) - val state = _state.asStateFlow() - - private val _singleEvents = MutableSharedFlow<SingleEvent>() - val singleEvents = _singleEvents.asSharedFlow() - - init { - viewModelScope.launch(Dispatchers.IO) { - _state.update { - it.copy( - appDesc = app, - isBlockingActivated = !trackersStateUseCase.isWhitelisted(app), - trackersWithWhiteList = trackersStatisticsUseCase.getTrackersWithWhiteList( - app - ), - isWhitelistEmpty = trackersStatisticsUseCase.isWhiteListEmpty(app) - ) - } - } - } - - suspend fun doOnStartedState() = withContext(Dispatchers.IO) { - merge( - getQuickPrivacyStateUseCase.trackerMode.map { - _state.update { s -> s.copy(isTrackersBlockingEnabled = it != TrackerMode.VULNERABLE) } - }, - trackersStatisticsUseCase.listenUpdates().map { fetchStatistics() } - ).collect { } - } - - fun submitAction(action: Action) = viewModelScope.launch { - when (action) { - is Action.BlockAllToggleAction -> blockAllToggleAction(action) - is Action.ToggleTrackerAction -> toggleTrackerAction(action) - is Action.ClickTracker -> actionClickTracker(action) - is Action.ResetAllTrackers -> resetAllTrackers() - } - } - - private suspend fun blockAllToggleAction(action: Action.BlockAllToggleAction) { - withContext(Dispatchers.IO) { - if (!state.value.isTrackersBlockingEnabled) { - _singleEvents.emit(SingleEvent.ToastTrackersControlDisabled) - } - trackersStateUseCase.toggleAppWhitelist(app, !action.isBlocked) - _state.update { - it.copy( - isBlockingActivated = !trackersStateUseCase.isWhitelisted(app) - ) - } - } - } - - private suspend fun toggleTrackerAction(action: Action.ToggleTrackerAction) { - withContext(Dispatchers.IO) { - if (!state.value.isTrackersBlockingEnabled) { - _singleEvents.emit(SingleEvent.ToastTrackersControlDisabled) - } - - if (state.value.isBlockingActivated) { - trackersStateUseCase.blockTracker(app, action.tracker, action.isBlocked) - updateWhitelist() - } - } - } - - private suspend fun actionClickTracker(action: Action.ClickTracker) { - withContext(Dispatchers.IO) { - action.tracker.exodusId?.let { - try { - _singleEvents.emit( - SingleEvent.OpenUrl( - Uri.parse(exodusBaseUrl + it) - ) - ) - } catch (e: Exception) { - } - } - } - } - - private suspend fun resetAllTrackers() { - withContext(Dispatchers.IO) { - trackersStateUseCase.clearWhitelist(app) - updateWhitelist() - } - } - private fun fetchStatistics() { - val (blocked, leaked) = trackersStatisticsUseCase.getCalls(app) - return _state.update { s -> - s.copy( - trackersWithWhiteList = trackersStatisticsUseCase.getTrackersWithWhiteList(app), - leaked = leaked, - blocked = blocked, - isWhitelistEmpty = trackersStatisticsUseCase.isWhiteListEmpty(app) - ) - } - } - - private fun updateWhitelist() { - _state.update { s -> - s.copy( - trackersWithWhiteList = trackersStatisticsUseCase.getTrackersWithWhiteList(app), - isWhitelistEmpty = trackersStatisticsUseCase.isWhiteListEmpty(app) - ) - } - } - - sealed class SingleEvent { - data class ErrorEvent(@StringRes val errorResId: Int) : SingleEvent() - data class OpenUrl(val url: Uri) : SingleEvent() - object ToastTrackersControlDisabled : SingleEvent() - } - - sealed class Action { - data class BlockAllToggleAction(val isBlocked: Boolean) : Action() - data class ToggleTrackerAction(val tracker: Tracker, val isBlocked: Boolean) : Action() - data class ClickTracker(val tracker: Tracker) : Action() - object ResetAllTrackers : Action() - } -} diff --git a/app/src/main/java/foundation/e/privacycentralapp/features/trackers/apptrackers/ToggleTrackersAdapter.kt b/app/src/main/java/foundation/e/privacycentralapp/features/trackers/apptrackers/ToggleTrackersAdapter.kt deleted file mode 100644 index 197f13f..0000000 --- a/app/src/main/java/foundation/e/privacycentralapp/features/trackers/apptrackers/ToggleTrackersAdapter.kt +++ /dev/null @@ -1,92 +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.trackers.apptrackers - -import android.text.SpannableString -import android.text.style.UnderlineSpan -import android.view.LayoutInflater -import android.view.View -import android.view.ViewGroup -import android.widget.Switch -import android.widget.TextView -import androidx.core.content.ContextCompat -import androidx.recyclerview.widget.RecyclerView -import foundation.e.privacycentralapp.R -import foundation.e.privacymodules.trackers.api.Tracker - -class ToggleTrackersAdapter( - private val itemsLayout: Int, - private val onToggleSwitch: (Tracker, Boolean) -> Unit, - private val onClickTitle: (Tracker) -> Unit -) : RecyclerView.Adapter<ToggleTrackersAdapter.ViewHolder>() { - - var isEnabled = true - - class ViewHolder( - view: View, - private val onToggleSwitch: (Tracker, Boolean) -> Unit, - private val onClickTitle: (Tracker) -> Unit - ) : RecyclerView.ViewHolder(view) { - val title: TextView = view.findViewById(R.id.title) - - val toggle: Switch = view.findViewById(R.id.toggle) - - fun bind(item: Pair<Tracker, Boolean>, isEnabled: Boolean) { - val text = item.first.label - if (item.first.exodusId != null) { - title.setTextColor(ContextCompat.getColor(title.context, R.color.accent)) - val spannable = SpannableString(text) - spannable.setSpan(UnderlineSpan(), 0, spannable.length, 0) - title.text = spannable - } else { - title.setTextColor(ContextCompat.getColor(title.context, R.color.primary_text)) - title.text = text - } - - toggle.isChecked = item.second - toggle.isEnabled = isEnabled - - toggle.setOnClickListener { - onToggleSwitch(item.first, toggle.isChecked) - } - - title.setOnClickListener { onClickTitle(item.first) } - } - } - - private var dataSet: List<Pair<Tracker, Boolean>> = emptyList() - - fun updateDataSet(new: List<Pair<Tracker, Boolean>>, isEnabled: Boolean) { - this.isEnabled = isEnabled - dataSet = new - notifyDataSetChanged() - } - - override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder { - val view = LayoutInflater.from(parent.context) - .inflate(itemsLayout, parent, false) - return ViewHolder(view, onToggleSwitch, onClickTitle) - } - - override fun onBindViewHolder(holder: ViewHolder, position: Int) { - val permission = dataSet[position] - holder.bind(permission, isEnabled) - } - - override fun getItemCount(): Int = dataSet.size -} diff --git a/app/src/main/java/foundation/e/privacycentralapp/main/MainActivity.kt b/app/src/main/java/foundation/e/privacycentralapp/main/MainActivity.kt deleted file mode 100644 index 92dc326..0000000 --- a/app/src/main/java/foundation/e/privacycentralapp/main/MainActivity.kt +++ /dev/null @@ -1,106 +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.main - -import android.app.Activity -import android.content.Context -import android.content.Intent -import android.os.Bundle -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.dashboard.DashboardFragment -import foundation.e.privacycentralapp.features.internetprivacy.InternetPrivacyFragment -import foundation.e.privacycentralapp.features.location.FakeLocationFragment -import foundation.e.privacycentralapp.features.trackers.TrackersFragment - -open class MainActivity : FragmentActivity(R.layout.activity_main) { - override fun onPostCreate(savedInstanceState: Bundle?) { - super.onPostCreate(savedInstanceState) - handleIntent(intent) - } - - override fun onNewIntent(intent: Intent) { - super.onNewIntent(intent) - handleIntent(intent) - } - - open fun handleIntent(intent: Intent) { - supportFragmentManager.commit { - setReorderingAllowed(true) - when (intent.action) { - ACTION_HIGHLIGHT_LEAKS -> add<DashboardFragment>( - containerViewId = R.id.container, - args = intent.extras - ) - ACTION_VIEW_TRACKERS -> { - add<TrackersFragment>(R.id.container) - } - ACTION_VIEW_FAKE_LOCATION -> { - add<FakeLocationFragment>(R.id.container) - } - ACTION_VIEW_IPSCRAMBLING -> { - add<InternetPrivacyFragment>(R.id.container) - } - else -> add<DashboardFragment>(R.id.container) - } - disallowAddToBackStack() - } - } - - override fun finishAfterTransition() { - val resultData = Intent() - val result = onPopulateResultIntent(resultData) - setResult(result, resultData) - - super.finishAfterTransition() - } - - open fun onPopulateResultIntent(intent: Intent): Int = Activity.RESULT_OK - - companion object { - private const val ACTION_HIGHLIGHT_LEAKS = "ACTION_HIGHLIGHT_LEAKS" - private const val ACTION_VIEW_TRACKERS = "ACTION_VIEW_TRACKERS" - private const val ACTION_VIEW_FAKE_LOCATION = "ACTION_VIEW_FAKE_LOCATION" - private const val ACTION_VIEW_IPSCRAMBLING = "ACTION_VIEW_IPSCRAMBLING" - - fun createHighlightLeaksIntent(context: Context, highlightIndex: Int) = - Intent(context, MainActivity::class.java).apply { - action = ACTION_HIGHLIGHT_LEAKS - putExtras(DashboardFragment.buildArgs(highlightIndex)) - } - - fun createTrackersIntent(context: Context) = - Intent(context, MainActivity::class.java).apply { - action = ACTION_VIEW_TRACKERS - } - - fun createFakeLocationIntent(context: Context): Intent { - return Intent(context, MainActivity::class.java).apply { - action = ACTION_VIEW_FAKE_LOCATION - } - } - - fun createIpScramblingIntent(context: Context): Intent { - return Intent(context, MainActivity::class.java).apply { - action = ACTION_VIEW_IPSCRAMBLING - } - } - } -} diff --git a/app/src/main/java/foundation/e/privacycentralapp/widget/Widget.kt b/app/src/main/java/foundation/e/privacycentralapp/widget/Widget.kt deleted file mode 100644 index 3abe21b..0000000 --- a/app/src/main/java/foundation/e/privacycentralapp/widget/Widget.kt +++ /dev/null @@ -1,156 +0,0 @@ -/* - * Copyright (C) 2022 E FOUNDATION - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see <https://www.gnu.org/licenses/>. - */ - -package foundation.e.privacycentralapp - -import android.appwidget.AppWidgetManager -import android.appwidget.AppWidgetProvider -import android.content.Context -import android.os.Bundle -import foundation.e.privacycentralapp.domain.usecases.GetQuickPrivacyStateUseCase -import foundation.e.privacycentralapp.domain.usecases.TrackersStatisticsUseCase -import foundation.e.privacycentralapp.widget.State -import foundation.e.privacycentralapp.widget.render -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.DelicateCoroutinesApi -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.FlowPreview -import kotlinx.coroutines.GlobalScope -import kotlinx.coroutines.Job -import kotlinx.coroutines.delay -import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.SharingStarted -import kotlinx.coroutines.flow.StateFlow -import kotlinx.coroutines.flow.combine -import kotlinx.coroutines.flow.debounce -import kotlinx.coroutines.flow.flow -import kotlinx.coroutines.flow.merge -import kotlinx.coroutines.flow.onStart -import kotlinx.coroutines.flow.sample -import kotlinx.coroutines.flow.stateIn -import kotlinx.coroutines.launch -import java.time.temporal.ChronoUnit - -/** - * Implementation of App Widget functionality. - */ -class Widget : AppWidgetProvider() { - - override fun onUpdate( - context: Context, - appWidgetManager: AppWidgetManager, - appWidgetIds: IntArray - ) { - render(context, state.value, appWidgetManager) - } - - override fun onEnabled(context: Context) { - // Enter relevant functionality for when the first widget is created - } - - override fun onDisabled(context: Context) { - // Enter relevant functionality for when the last widget is disabled - } - - companion object { - private var updateWidgetJob: Job? = null - - private var state: StateFlow<State> = MutableStateFlow(State()) - - private const val DARK_TEXT_KEY = "foundation.e.blisslauncher.WIDGET_OPTION_DARK_TEXT" - var isDarkText = false - - @OptIn(FlowPreview::class) - private fun initState( - getPrivacyStateUseCase: GetQuickPrivacyStateUseCase, - trackersStatisticsUseCase: TrackersStatisticsUseCase, - coroutineScope: CoroutineScope - ): StateFlow<State> { - - return combine( - getPrivacyStateUseCase.quickPrivacyState, - getPrivacyStateUseCase.trackerMode, - getPrivacyStateUseCase.isLocationHidden, - getPrivacyStateUseCase.ipScramblingMode, - ) { quickPrivacyState, trackerMode, isLocationHidden, ipScramblingMode -> - - State( - quickPrivacyState = quickPrivacyState, - trackerMode = trackerMode, - isLocationHidden = isLocationHidden, - ipScramblingMode = ipScramblingMode - ) - }.sample(50) - .combine( - merge( - trackersStatisticsUseCase.listenUpdates() - .onStart { emit(Unit) } - .debounce(5000), - flow { - while (true) { - emit(Unit) - delay(ChronoUnit.HOURS.duration.toMillis()) - } - } - - ) - ) { state, _ -> - state.copy( - dayStatistics = trackersStatisticsUseCase.getDayTrackersCalls(), - activeTrackersCount = trackersStatisticsUseCase.getDayTrackersCount() - ) - }.stateIn( - scope = coroutineScope, - started = SharingStarted.Eagerly, - initialValue = State() - ) - } - - @OptIn(DelicateCoroutinesApi::class) - fun startListening( - appContext: Context, - getPrivacyStateUseCase: GetQuickPrivacyStateUseCase, - trackersStatisticsUseCase: TrackersStatisticsUseCase, - ) { - state = initState( - getPrivacyStateUseCase, - trackersStatisticsUseCase, - GlobalScope - ) - - updateWidgetJob?.cancel() - updateWidgetJob = GlobalScope.launch(Dispatchers.Main) { - state.collect { - render(appContext, it, AppWidgetManager.getInstance(appContext)) - } - } - } - } - - override fun onAppWidgetOptionsChanged( - context: Context, - appWidgetManager: AppWidgetManager, - appWidgetId: Int, - newOptions: Bundle? - ) { - super.onAppWidgetOptionsChanged(context, appWidgetManager, appWidgetId, newOptions) - if (newOptions != null) { - isDarkText = newOptions.getBoolean(DARK_TEXT_KEY) - } - render(context, state.value, appWidgetManager) - } -} diff --git a/app/src/main/java/foundation/e/privacycentralapp/widget/WidgetCommandReceiver.kt b/app/src/main/java/foundation/e/privacycentralapp/widget/WidgetCommandReceiver.kt deleted file mode 100644 index e01f47f..0000000 --- a/app/src/main/java/foundation/e/privacycentralapp/widget/WidgetCommandReceiver.kt +++ /dev/null @@ -1,42 +0,0 @@ -/* - * Copyright (C) 2022 E FOUNDATION - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see <https://www.gnu.org/licenses/>. - */ - -package foundation.e.privacycentralapp.widget - -import android.content.BroadcastReceiver -import android.content.Context -import android.content.Intent -import foundation.e.privacycentralapp.PrivacyCentralApplication - -class WidgetCommandReceiver : BroadcastReceiver() { - override fun onReceive(context: Context?, intent: Intent?) { - val getQuickPrivacyStateUseCase = (context?.applicationContext as? PrivacyCentralApplication)?.dependencyContainer?.getQuickPrivacyStateUseCase - - when (intent?.action) { - ACTION_TOGGLE_TRACKERS -> getQuickPrivacyStateUseCase?.toggleTrackers() - ACTION_TOGGLE_LOCATION -> getQuickPrivacyStateUseCase?.toggleLocation() - ACTION_TOGGLE_IPSCRAMBLING -> getQuickPrivacyStateUseCase?.toggleIpScrambling() - else -> {} - } - } - - companion object { - const val ACTION_TOGGLE_TRACKERS = "toggle_trackers" - const val ACTION_TOGGLE_LOCATION = "toggle_location" - const val ACTION_TOGGLE_IPSCRAMBLING = "toggle_ipscrambling" - } -} diff --git a/app/src/main/java/foundation/e/privacycentralapp/widget/WidgetUI.kt b/app/src/main/java/foundation/e/privacycentralapp/widget/WidgetUI.kt deleted file mode 100644 index fccfd48..0000000 --- a/app/src/main/java/foundation/e/privacycentralapp/widget/WidgetUI.kt +++ /dev/null @@ -1,381 +0,0 @@ -/* - * Copyright (C) 2022 E FOUNDATION - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see <https://www.gnu.org/licenses/>. - */ - -package foundation.e.privacycentralapp.widget - -import android.app.PendingIntent -import android.app.PendingIntent.FLAG_IMMUTABLE -import android.app.PendingIntent.FLAG_UPDATE_CURRENT -import android.appwidget.AppWidgetManager -import android.content.ComponentName -import android.content.Context -import android.content.Intent -import android.view.View -import android.widget.RemoteViews -import foundation.e.privacycentralapp.R -import foundation.e.privacycentralapp.Widget -import foundation.e.privacycentralapp.Widget.Companion.isDarkText -import foundation.e.privacycentralapp.common.extensions.dpToPxF -import foundation.e.privacycentralapp.domain.entities.InternetPrivacyMode -import foundation.e.privacycentralapp.domain.entities.QuickPrivacyState -import foundation.e.privacycentralapp.domain.entities.TrackerMode -import foundation.e.privacycentralapp.main.MainActivity -import foundation.e.privacycentralapp.widget.WidgetCommandReceiver.Companion.ACTION_TOGGLE_IPSCRAMBLING -import foundation.e.privacycentralapp.widget.WidgetCommandReceiver.Companion.ACTION_TOGGLE_LOCATION -import foundation.e.privacycentralapp.widget.WidgetCommandReceiver.Companion.ACTION_TOGGLE_TRACKERS - -data class State( - val quickPrivacyState: QuickPrivacyState = QuickPrivacyState.DISABLED, - val trackerMode: TrackerMode = TrackerMode.VULNERABLE, - val isLocationHidden: Boolean = false, - val ipScramblingMode: InternetPrivacyMode = InternetPrivacyMode.REAL_IP_LOADING, - val dayStatistics: List<Pair<Int, Int>> = emptyList(), - val activeTrackersCount: Int = 0, -) - -fun render( - context: Context, - state: State, - appWidgetManager: AppWidgetManager, -) { - val views = RemoteViews(context.packageName, R.layout.widget) - applyDarkText(context, state, views) - views.apply { - val openPIntent = PendingIntent.getActivity( - context, - REQUEST_CODE_DASHBOARD, - Intent(context, MainActivity::class.java), - FLAG_IMMUTABLE or FLAG_UPDATE_CURRENT - ) - setOnClickPendingIntent(R.id.settings_btn, openPIntent) - setOnClickPendingIntent(R.id.widget_container, openPIntent) - - setTextViewText( - R.id.state_label, - context.getString( - when (state.quickPrivacyState) { - QuickPrivacyState.DISABLED -> R.string.widget_state_title_off - QuickPrivacyState.FULL_ENABLED -> R.string.widget_state_title_on - QuickPrivacyState.ENABLED -> R.string.widget_state_title_custom - } - ) - ) - - setImageViewResource( - R.id.toggle_trackers, - if (state.trackerMode == TrackerMode.VULNERABLE) - R.drawable.ic_switch_disabled - else R.drawable.ic_switch_enabled - ) - - setOnClickPendingIntent( - R.id.toggle_trackers, - PendingIntent.getBroadcast( - context, - REQUEST_CODE_TOGGLE_TRACKERS, - Intent(context, WidgetCommandReceiver::class.java).apply { - action = ACTION_TOGGLE_TRACKERS - }, - FLAG_IMMUTABLE or FLAG_UPDATE_CURRENT - ) - ) - - setTextViewText( - R.id.state_trackers, - context.getString( - when (state.trackerMode) { - TrackerMode.DENIED -> R.string.widget_state_trackers_on - TrackerMode.VULNERABLE -> R.string.widget_state_trackers_off - TrackerMode.CUSTOM -> R.string.widget_state_trackers_custom - } - ) - ) - - setImageViewResource( - R.id.toggle_location, - if (state.isLocationHidden) R.drawable.ic_switch_enabled - else R.drawable.ic_switch_disabled - ) - - setOnClickPendingIntent( - R.id.toggle_location, - PendingIntent.getBroadcast( - context, - REQUEST_CODE_TOGGLE_LOCATION, - Intent(context, WidgetCommandReceiver::class.java).apply { - action = ACTION_TOGGLE_LOCATION - }, - FLAG_IMMUTABLE or FLAG_UPDATE_CURRENT - ) - ) - - setTextViewText( - R.id.state_geolocation, - context.getString( - if (state.isLocationHidden) R.string.widget_state_geolocation_on - else R.string.widget_state_geolocation_off - ) - ) - - setImageViewResource( - R.id.toggle_ipscrambling, - if (state.ipScramblingMode.isChecked) R.drawable.ic_switch_enabled - else R.drawable.ic_switch_disabled - ) - - setOnClickPendingIntent( - R.id.toggle_ipscrambling, - PendingIntent.getBroadcast( - context, - REQUEST_CODE_TOGGLE_IPSCRAMBLING, - Intent(context, WidgetCommandReceiver::class.java).apply { - action = ACTION_TOGGLE_IPSCRAMBLING - }, - FLAG_IMMUTABLE or FLAG_UPDATE_CURRENT - ) - ) - - setTextViewText( - R.id.state_ip_address, - context.getString( - if (state.ipScramblingMode == InternetPrivacyMode.HIDE_IP) R.string.widget_state_ipaddress_on - else R.string.widget_state_ipaddress_off - ) - ) - - val loading = state.ipScramblingMode.isLoading - - setViewVisibility(R.id.state_ip_address, if (loading) View.GONE else View.VISIBLE) - - setViewVisibility(R.id.state_ip_address_loader, if (loading) View.VISIBLE else View.GONE) - - if (state.dayStatistics.all { it.first == 0 && it.second == 0 }) { - setViewVisibility(R.id.graph, View.GONE) - setViewVisibility(R.id.graph_legend, View.GONE) - setViewVisibility(R.id.graph_empty, View.VISIBLE) - setViewVisibility(R.id.graph_legend_values, View.GONE) - setViewVisibility(R.id.graph_view_trackers_btn, View.GONE) - } else { - setViewVisibility(R.id.graph, View.VISIBLE) - setViewVisibility(R.id.graph_legend, View.VISIBLE) - setViewVisibility(R.id.graph_empty, View.GONE) - setViewVisibility(R.id.graph_legend_values, View.VISIBLE) - setViewVisibility(R.id.graph_view_trackers_btn, View.VISIBLE) - - val pIntent = PendingIntent.getActivity( - context, - REQUEST_CODE_TRACKERS, - MainActivity.createTrackersIntent(context), - FLAG_IMMUTABLE or FLAG_UPDATE_CURRENT - ) - - setOnClickPendingIntent(R.id.graph_view_trackers_btn, pIntent) - - val graphHeightPx = 26.dpToPxF(context) - val maxValue = - state.dayStatistics - .map { it.first + it.second } - .maxOrNull() - .let { if (it == null || it == 0) 1 else it } - val ratio = graphHeightPx / maxValue - - state.dayStatistics.forEachIndexed { index, (blocked, leaked) -> - // blocked (the bar below) - val middlePadding = graphHeightPx - blocked * ratio - setViewPadding(blockedBarIds[index], 0, middlePadding.toInt(), 0, 0) - - // leaked (the bar above) - val topPadding = graphHeightPx - (blocked + leaked) * ratio - setViewPadding(leakedBarIds[index], 0, topPadding.toInt(), 0, 0) - - val highlightPIntent = PendingIntent.getActivity( - context, REQUEST_CODE_HIGHLIGHT + index, - MainActivity.createHighlightLeaksIntent(context, index), - FLAG_IMMUTABLE or FLAG_UPDATE_CURRENT - ) - setOnClickPendingIntent(containerBarIds[index], highlightPIntent) - } - - setTextViewText( - R.id.graph_legend, - context.getString( - R.string.widget_graph_trackers_legend, - state.activeTrackersCount.toString() - ) - ) - } - } - - appWidgetManager.updateAppWidget(ComponentName(context, Widget::class.java), views) -} - -private val containerBarIds = listOf( - R.id.widget_graph_bar_container_0, - R.id.widget_graph_bar_container_1, - R.id.widget_graph_bar_container_2, - R.id.widget_graph_bar_container_3, - R.id.widget_graph_bar_container_4, - R.id.widget_graph_bar_container_5, - R.id.widget_graph_bar_container_6, - R.id.widget_graph_bar_container_7, - R.id.widget_graph_bar_container_8, - R.id.widget_graph_bar_container_9, - R.id.widget_graph_bar_container_10, - R.id.widget_graph_bar_container_11, - R.id.widget_graph_bar_container_12, - R.id.widget_graph_bar_container_13, - R.id.widget_graph_bar_container_14, - R.id.widget_graph_bar_container_15, - R.id.widget_graph_bar_container_16, - R.id.widget_graph_bar_container_17, - R.id.widget_graph_bar_container_18, - R.id.widget_graph_bar_container_19, - R.id.widget_graph_bar_container_20, - R.id.widget_graph_bar_container_21, - R.id.widget_graph_bar_container_22, - R.id.widget_graph_bar_container_23, -) - -private val blockedBarIds = listOf( - R.id.widget_graph_bar_0, - R.id.widget_graph_bar_1, - R.id.widget_graph_bar_2, - R.id.widget_graph_bar_3, - R.id.widget_graph_bar_4, - R.id.widget_graph_bar_5, - R.id.widget_graph_bar_6, - R.id.widget_graph_bar_7, - R.id.widget_graph_bar_8, - R.id.widget_graph_bar_9, - R.id.widget_graph_bar_10, - R.id.widget_graph_bar_11, - R.id.widget_graph_bar_12, - R.id.widget_graph_bar_13, - R.id.widget_graph_bar_14, - R.id.widget_graph_bar_15, - R.id.widget_graph_bar_16, - R.id.widget_graph_bar_17, - R.id.widget_graph_bar_18, - R.id.widget_graph_bar_19, - R.id.widget_graph_bar_20, - R.id.widget_graph_bar_21, - R.id.widget_graph_bar_22, - R.id.widget_graph_bar_23 -) - -private val leakedBarIds = listOf( - R.id.widget_leaked_graph_bar_0, - R.id.widget_leaked_graph_bar_1, - R.id.widget_leaked_graph_bar_2, - R.id.widget_leaked_graph_bar_3, - R.id.widget_leaked_graph_bar_4, - R.id.widget_leaked_graph_bar_5, - R.id.widget_leaked_graph_bar_6, - R.id.widget_leaked_graph_bar_7, - R.id.widget_leaked_graph_bar_8, - R.id.widget_leaked_graph_bar_9, - R.id.widget_leaked_graph_bar_10, - R.id.widget_leaked_graph_bar_11, - R.id.widget_leaked_graph_bar_12, - R.id.widget_leaked_graph_bar_13, - R.id.widget_leaked_graph_bar_14, - R.id.widget_leaked_graph_bar_15, - R.id.widget_leaked_graph_bar_16, - R.id.widget_leaked_graph_bar_17, - R.id.widget_leaked_graph_bar_18, - R.id.widget_leaked_graph_bar_19, - R.id.widget_leaked_graph_bar_20, - R.id.widget_leaked_graph_bar_21, - R.id.widget_leaked_graph_bar_22, - R.id.widget_leaked_graph_bar_23 -) - -private const val REQUEST_CODE_DASHBOARD = 1 -private const val REQUEST_CODE_TRACKERS = 3 -private const val REQUEST_CODE_TOGGLE_TRACKERS = 4 -private const val REQUEST_CODE_TOGGLE_LOCATION = 5 -private const val REQUEST_CODE_TOGGLE_IPSCRAMBLING = 6 -private const val REQUEST_CODE_HIGHLIGHT = 100 - -fun applyDarkText(context: Context, state: State, views: RemoteViews) { - views.apply { - listOf( - R.id.state_label, - R.id.graph_legend_blocked, - R.id.graph_legend_allowed, - - ) - .forEach { - setTextColor( - it, - context.getColor(if (isDarkText) R.color.on_surface_disabled_light else R.color.on_primary_medium_emphasis) - ) - } - setTextColor( - R.id.widget_title, - context.getColor(if (isDarkText) R.color.on_surface_medium_emphasis_light else R.color.on_surface_high_emphasis) - ) - listOf( - R.id.state_trackers, - R.id.state_geolocation, - R.id.state_ip_address, - R.id.graph_legend, - R.id.graph_view_trackers_btn - ) - .forEach { - setTextColor( - it, - context.getColor(if (isDarkText) R.color.on_surface_medium_emphasis_light else R.color.on_primary_high_emphasis) - ) - } - - listOf( - R.id.trackers_label, - R.id.geolocation_label, - R.id.ip_address_label, - R.id.graph_empty - - ) - .forEach { - setTextColor( - it, - context.getColor(if (isDarkText) R.color.on_surface_disabled_light else R.color.on_primary_disabled) - ) - } - setTextViewCompoundDrawables( - R.id.graph_view_trackers_btn, - 0, - 0, - if (isDarkText) R.drawable.ic_chevron_right_24dp_light else R.drawable.ic_chevron_right_24dp, - 0 - ) - setImageViewResource( - R.id.settings_btn, - if (isDarkText) R.drawable.ic_settings_light else R.drawable.ic_settings - ) - setImageViewResource( - R.id.state_icon, - if (isDarkText) { - if (state.quickPrivacyState.isEnabled()) R.drawable.ic_shield_on_light - else R.drawable.ic_shield_off_light - } else { - if (state.quickPrivacyState.isEnabled()) R.drawable.ic_shield_on_white - else R.drawable.ic_shield_off_white - } - ) - } -} |