diff --git a/app/src/main/kotlin/com/looker/droidify/compose/settings/SettingsScreen.kt b/app/src/main/kotlin/com/looker/droidify/compose/settings/SettingsScreen.kt index b6eed2396..dca22d834 100644 --- a/app/src/main/kotlin/com/looker/droidify/compose/settings/SettingsScreen.kt +++ b/app/src/main/kotlin/com/looker/droidify/compose/settings/SettingsScreen.kt @@ -252,6 +252,24 @@ fun SettingsScreen( } } + item { + SwitchSettingItem( + title = stringResource(R.string.download_stats), + description = stringResource(R.string.download_statistics_summary), + checked = settings.dlStatsEnabled, + onCheckedChange = viewModel::setDownloadStatisticsEnabled, + ) + } + + item { + SwitchSettingItem( + title = stringResource(R.string.reproducibility_logs), + description = stringResource(R.string.reproducibility_logs_summary), + checked = settings.rbLogsEnabled, + onCheckedChange = viewModel::setReproducibilityLogsEnabled, + ) + } + item { SettingHeader(title = stringResource(R.string.install_types)) } item { diff --git a/app/src/main/kotlin/com/looker/droidify/compose/settings/SettingsViewModel.kt b/app/src/main/kotlin/com/looker/droidify/compose/settings/SettingsViewModel.kt index ad05e2517..d894bb6ba 100644 --- a/app/src/main/kotlin/com/looker/droidify/compose/settings/SettingsViewModel.kt +++ b/app/src/main/kotlin/com/looker/droidify/compose/settings/SettingsViewModel.kt @@ -10,6 +10,7 @@ import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import com.looker.droidify.BuildConfig import com.looker.droidify.R +import com.looker.droidify.data.PrivacyRepository import com.looker.droidify.data.StringHandler import com.looker.droidify.database.Database import com.looker.droidify.database.RepositoryExporter @@ -45,6 +46,7 @@ import kotlinx.coroutines.launch @HiltViewModel class SettingsViewModel @Inject constructor( private val settingsRepository: SettingsRepository, + private val privacyRepository: PrivacyRepository, private val repositoryExporter: RepositoryExporter, private val customButtonRepository: CustomButtonRepository, private val handler: StringHandler, @@ -188,6 +190,24 @@ class SettingsViewModel @Inject constructor( } } + fun setDownloadStatisticsEnabled(enabled: Boolean) { + viewModelScope.launch { + settingsRepository.setDownloadStatisticsEnabled(enabled) + if (!enabled) { + privacyRepository.clearDownloadStats() + } + } + } + + fun setReproducibilityLogsEnabled(enabled: Boolean) { + viewModelScope.launch { + settingsRepository.setRBLogsEnabled(enabled) + if (!enabled) { + privacyRepository.clearRbLogs() + } + } + } + fun setProxyType(proxyType: ProxyType) { viewModelScope.launch { settingsRepository.setProxyType(proxyType) diff --git a/app/src/main/kotlin/com/looker/droidify/data/PrivacyRepository.kt b/app/src/main/kotlin/com/looker/droidify/data/PrivacyRepository.kt index 91052c018..898f6befc 100644 --- a/app/src/main/kotlin/com/looker/droidify/data/PrivacyRepository.kt +++ b/app/src/main/kotlin/com/looker/droidify/data/PrivacyRepository.kt @@ -31,4 +31,14 @@ class PrivacyRepository( suspend fun save(downloadStats: List) { downloadStatsDao.insert(downloadStats) } + + suspend fun clearDownloadStats() { + downloadStatsDao.deleteAll() + settingsRepo.clearDownloadStatsLastModified() + } + + suspend fun clearRbLogs() { + rbDao.deleteAll() + settingsRepo.clearRbLogLastModified() + } } diff --git a/app/src/main/kotlin/com/looker/droidify/data/local/dao/DownloadStatsDao.kt b/app/src/main/kotlin/com/looker/droidify/data/local/dao/DownloadStatsDao.kt index 1dcc1333b..c0dbe2d88 100644 --- a/app/src/main/kotlin/com/looker/droidify/data/local/dao/DownloadStatsDao.kt +++ b/app/src/main/kotlin/com/looker/droidify/data/local/dao/DownloadStatsDao.kt @@ -29,4 +29,7 @@ interface DownloadStatsDao { @Insert(onConflict = REPLACE) suspend fun insert(stats: List) + + @Query("DELETE FROM download_stats") + suspend fun deleteAll() } diff --git a/app/src/main/kotlin/com/looker/droidify/data/local/dao/RBLogDao.kt b/app/src/main/kotlin/com/looker/droidify/data/local/dao/RBLogDao.kt index 99dbcf5aa..1ffb448d8 100644 --- a/app/src/main/kotlin/com/looker/droidify/data/local/dao/RBLogDao.kt +++ b/app/src/main/kotlin/com/looker/droidify/data/local/dao/RBLogDao.kt @@ -20,4 +20,7 @@ interface RBLogDao { @Query("SELECT * FROM rblog WHERE packageName = :packageName") fun getFlow(packageName: String): Flow> + + @Query("DELETE FROM rblog") + suspend fun deleteAll() } diff --git a/app/src/main/kotlin/com/looker/droidify/datastore/PreferenceSettingsRepository.kt b/app/src/main/kotlin/com/looker/droidify/datastore/PreferenceSettingsRepository.kt index eaaa32f36..303204cb8 100644 --- a/app/src/main/kotlin/com/looker/droidify/datastore/PreferenceSettingsRepository.kt +++ b/app/src/main/kotlin/com/looker/droidify/datastore/PreferenceSettingsRepository.kt @@ -186,6 +186,24 @@ class PreferenceSettingsRepository( override suspend fun setDeleteApkOnInstall(enable: Boolean) = DELETE_APK_ON_INSTALL.update(enable) + override suspend fun setDownloadStatisticsEnabled(enabled: Boolean) = + DOWNLOAD_STATISTICS_ENABLED.update(enabled) + + override suspend fun clearDownloadStatsLastModified() { + dataStore.edit { pref -> + pref.remove(LAST_MODIFIED_DS) + } + } + + override suspend fun setRBLogsEnabled(enabled: Boolean) = + REPRODUCIBILITY_LOGS_ENABLED.update(enabled) + + override suspend fun clearRbLogLastModified() { + dataStore.edit { pref -> + pref.remove(LAST_RB_FETCH) + } + } + private fun mapSettings(preferences: Preferences): Settings { val installerType = InstallerType.valueOf(preferences[INSTALLER_TYPE] ?: InstallerType.Default.name) @@ -228,6 +246,8 @@ class PreferenceSettingsRepository( val enabledRepoIds = preferences[ENABLED_REPO_IDS]?.mapNotNull { it.toIntOrNull() }?.toSet() ?: emptySet() val deleteApkOnInstall = preferences[DELETE_APK_ON_INSTALL] ?: false + val downloadStatisticsEnabled = preferences[DOWNLOAD_STATISTICS_ENABLED] ?: true + val reproducibilityLogsEnabled = preferences[REPRODUCIBILITY_LOGS_ENABLED] ?: true return Settings( language = language, @@ -251,6 +271,8 @@ class PreferenceSettingsRepository( homeScreenSwiping = homeScreenSwiping, enabledRepoIds = enabledRepoIds, deleteApkOnInstall = deleteApkOnInstall, + dlStatsEnabled = downloadStatisticsEnabled, + rbLogsEnabled = reproducibilityLogsEnabled, ) } @@ -277,6 +299,8 @@ class PreferenceSettingsRepository( val FAVOURITE_APPS = stringSetPreferencesKey("key_favourite_apps") val HOME_SCREEN_SWIPING = booleanPreferencesKey("key_home_swiping") val DELETE_APK_ON_INSTALL = booleanPreferencesKey("key_delete_apk_on_install") + val DOWNLOAD_STATISTICS_ENABLED = booleanPreferencesKey("key_download_statistics_enabled") + val REPRODUCIBILITY_LOGS_ENABLED = booleanPreferencesKey("key_reproducibility_logs_enabled") val LEGACY_INSTALLER_COMPONENT_CLASS = stringPreferencesKey("key_legacy_installer_component_class") val LEGACY_INSTALLER_COMPONENT_ACTIVITY = @@ -338,6 +362,8 @@ class PreferenceSettingsRepository( set(HOME_SCREEN_SWIPING, settings.homeScreenSwiping) set(ENABLED_REPO_IDS, settings.enabledRepoIds.map { it.toString() }.toSet()) set(DELETE_APK_ON_INSTALL, settings.deleteApkOnInstall) + set(DOWNLOAD_STATISTICS_ENABLED, settings.dlStatsEnabled) + set(REPRODUCIBILITY_LOGS_ENABLED, settings.rbLogsEnabled) return this.toPreferences() } } diff --git a/app/src/main/kotlin/com/looker/droidify/datastore/Settings.kt b/app/src/main/kotlin/com/looker/droidify/datastore/Settings.kt index 20a240fff..1c4903ae8 100644 --- a/app/src/main/kotlin/com/looker/droidify/datastore/Settings.kt +++ b/app/src/main/kotlin/com/looker/droidify/datastore/Settings.kt @@ -47,6 +47,8 @@ data class Settings( val homeScreenSwiping: Boolean = true, val enabledRepoIds: Set = emptySet(), val deleteApkOnInstall: Boolean = false, + val dlStatsEnabled: Boolean = true, + val rbLogsEnabled: Boolean = true, ) @OptIn(ExperimentalSerializationApi::class) diff --git a/app/src/main/kotlin/com/looker/droidify/datastore/SettingsRepository.kt b/app/src/main/kotlin/com/looker/droidify/datastore/SettingsRepository.kt index 94ed879d4..850b02cd0 100644 --- a/app/src/main/kotlin/com/looker/droidify/datastore/SettingsRepository.kt +++ b/app/src/main/kotlin/com/looker/droidify/datastore/SettingsRepository.kt @@ -72,6 +72,14 @@ interface SettingsRepository { suspend fun isRepoEnabled(repoId: Int): Boolean suspend fun setDeleteApkOnInstall(enable: Boolean) + + suspend fun setDownloadStatisticsEnabled(enabled: Boolean) + + suspend fun clearDownloadStatsLastModified() + + suspend fun setRBLogsEnabled(enabled: Boolean) + + suspend fun clearRbLogLastModified() } inline fun SettingsRepository.get(crossinline block: suspend Settings.() -> T): Flow { diff --git a/app/src/main/kotlin/com/looker/droidify/service/SyncService.kt b/app/src/main/kotlin/com/looker/droidify/service/SyncService.kt index 1044f83b2..0b46fdcbb 100644 --- a/app/src/main/kotlin/com/looker/droidify/service/SyncService.kt +++ b/app/src/main/kotlin/com/looker/droidify/service/SyncService.kt @@ -15,6 +15,7 @@ import androidx.fragment.app.Fragment import com.looker.droidify.BuildConfig import com.looker.droidify.R import com.looker.droidify.database.Database +import com.looker.droidify.datastore.Settings import com.looker.droidify.datastore.SettingsRepository import com.looker.droidify.index.RepositoryUpdater import com.looker.droidify.model.ProductItem @@ -51,6 +52,7 @@ import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.flow.filter import kotlinx.coroutines.flow.sample import kotlinx.coroutines.launch +import kotlinx.coroutines.runBlocking import kotlinx.coroutines.sync.Mutex import kotlinx.coroutines.sync.withLock import kotlinx.coroutines.withContext @@ -129,7 +131,7 @@ class SyncService : ConnectionService() { val state: SharedFlow get() = syncState.asSharedFlow() - private fun sync(ids: List, request: SyncRequest) { + private fun sync(ids: List, request: SyncRequest, settings: Settings) { val cancelledTask = cancelCurrentTask { request == SyncRequest.FORCE && it.task?.repositoryId in ids } cancelTasks { !it.manual && it.repositoryId in ids } @@ -139,7 +141,7 @@ class SyncService : ConnectionService() { it !in currentIds && it != currentTask?.task?.repositoryId }.map { Task(it, manual) } - handleNextTask(cancelledTask?.hasUpdates == true) + handleNextTask(cancelledTask?.hasUpdates == true, settings) if (request != SyncRequest.AUTO && started == Started.AUTO) { started = Started.MANUAL startServiceCompat() @@ -149,19 +151,21 @@ class SyncService : ConnectionService() { } fun sync(request: SyncRequest) { - RBLogWorker.fetchRBLogs(applicationContext) - DownloadStatsWorker.fetchDownloadStats(applicationContext) + val settings = runBlocking { settingsRepository.getInitial() } + if (settings.rbLogsEnabled) RBLogWorker.fetchRBLogs(applicationContext) + if (settings.dlStatsEnabled) DownloadStatsWorker.fetchDownloadStats(applicationContext) val ids = Database.RepositoryAdapter.getAll() .filter { it.enabled } .map { it.id } - sync(ids, request) + sync(ids, request, settings) } fun sync(repository: Repository) { + val settings = runBlocking { settingsRepository.getInitial() } if (repository.enabled) { - sync(listOf(repository.id), SyncRequest.FORCE) + sync(listOf(repository.id), SyncRequest.FORCE, settings) } } @@ -180,19 +184,20 @@ class SyncService : ConnectionService() { fun setEnabled(repository: Repository, enabled: Boolean): Boolean { Database.RepositoryAdapter.put(repository.enable(enabled)) + val settings = runBlocking { settingsRepository.getInitial() } if (enabled) { val isRepoInTasks = repository.id != currentTask?.task?.repositoryId && !tasks.any { it.repositoryId == repository.id } if (isRepoInTasks) { tasks += Task(repository.id, true) - handleNextTask(false) + handleNextTask(false, settings) } } else { cancelTasks { it.repositoryId == repository.id } val cancelledTask = cancelCurrentTask { it.task?.repositoryId == repository.id } - handleNextTask(cancelledTask?.hasUpdates == true) + handleNextTask(cancelledTask?.hasUpdates == true, settings) } return true } @@ -211,9 +216,10 @@ class SyncService : ConnectionService() { } fun cancelAuto(): Boolean { + val settings = runBlocking { settingsRepository.getInitial() } val removed = cancelTasks { !it.manual } val currentTask = cancelCurrentTask { it.task?.manual == false } - handleNextTask(currentTask?.hasUpdates == true) + handleNextTask(currentTask?.hasUpdates == true, settings) return removed || currentTask != null } } @@ -258,10 +264,11 @@ class SyncService : ConnectionService() { } override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int { + val settings = runBlocking { settingsRepository.getInitial() } if (intent?.action == ACTION_CANCEL) { tasks.clear() val cancelledTask = cancelCurrentTask { it.task != null } - handleNextTask(cancelledTask?.hasUpdates == true) + handleNextTask(cancelledTask?.hasUpdates == true, settings) stopSelf() } else { startForeground( @@ -460,24 +467,19 @@ class SyncService : ConnectionService() { stateNotificationBuilder.setWhen(System.currentTimeMillis()) } - private fun handleNextTask(isIndexModified: Boolean) { + private fun handleNextTask(isIndexModified: Boolean, settings: Settings) { if (currentTask != null) return if (tasks.isEmpty()) { if (started != Started.NO) { lifecycleScope.launch { - val setting = settingsRepository.getInitial() - handleUpdates( - notifyUpdates = setting.notifyUpdate, - autoUpdate = setting.autoUpdate, - skipSignature = setting.ignoreSignature, - ) + handleUpdates(settings) } } return } val task = tasks.removeAt(0) val repository = Database.RepositoryAdapter.get(task.repositoryId) - if (repository == null || !repository.enabled) handleNextTask(isIndexModified) + if (repository == null || !repository.enabled) handleNextTask(isIndexModified, settings) val lastStarted = started val newStarted = if (task.manual || lastStarted == Started.MANUAL) { Started.MANUAL @@ -492,13 +494,11 @@ class SyncService : ConnectionService() { val initialState = State.Connecting(repository!!.name) publishForegroundState(true, initialState) lifecycleScope.launch { - val unstableUpdates = - settingsRepository.getInitial().unstableUpdate val downloadJob = downloadFile( task = task, repository = repository, isIndexModified = isIndexModified, - unstableUpdates = unstableUpdates, + settings = settings ) currentTask = CurrentTask(task, downloadJob, isIndexModified, initialState) } @@ -508,14 +508,14 @@ class SyncService : ConnectionService() { task: Task, repository: Repository, isIndexModified: Boolean, - unstableUpdates: Boolean, + settings: Settings, ): CoroutinesJob = launch(Dispatchers.Default) { var isNewlyModified = isIndexModified try { val response = RepositoryUpdater.update( this@SyncService, repository, - unstableUpdates, + settings.unstableUpdate, ) { stage, progress, total -> launch { syncState.emit( @@ -542,28 +542,24 @@ class SyncService : ConnectionService() { } finally { withContext(NonCancellable) { lock.withLock { currentTask = null } - handleNextTask(isNewlyModified) + handleNextTask(isNewlyModified, settings) } } } - suspend fun handleUpdates( - notifyUpdates: Boolean, - autoUpdate: Boolean, - skipSignature: Boolean, - ) { + suspend fun handleUpdates(settings: Settings) { try { val blocked = updateNotificationBlockerFragment?.get()?.isAdded == true - val updates = Database.ProductAdapter.getUpdates(skipSignature) + val updates = Database.ProductAdapter.getUpdates(settings.ignoreSignature) if (!blocked && updates.isNotEmpty()) { - if (notifyUpdates) { + if (settings.notifyUpdate) { notificationManager?.notify( Constants.NOTIFICATION_ID_UPDATES, updatesAvailableNotification(this, updates), ) } - if (autoUpdate) { + if (settings.autoUpdate) { autoUpdating = true autoUpdateStartedFor = updates.map { it.packageName } updateAllAppsInternal(updates) @@ -577,7 +573,7 @@ class SyncService : ConnectionService() { } finally { withContext(NonCancellable) { lock.withLock { currentTask = null } - handleNextTask(false) + handleNextTask(false, settings) } } } diff --git a/app/src/main/kotlin/com/looker/droidify/work/DownloadStatsWorker.kt b/app/src/main/kotlin/com/looker/droidify/work/DownloadStatsWorker.kt index 639c6b2f9..d1fc57692 100644 --- a/app/src/main/kotlin/com/looker/droidify/work/DownloadStatsWorker.kt +++ b/app/src/main/kotlin/com/looker/droidify/work/DownloadStatsWorker.kt @@ -11,6 +11,7 @@ import androidx.work.WorkerParameters import com.looker.droidify.data.PrivacyRepository import com.looker.droidify.data.local.model.DownloadStatsData import com.looker.droidify.data.local.model.DownloadStatsData.Companion.toEpochMillis +import com.looker.droidify.datastore.Settings import com.looker.droidify.datastore.SettingsRepository import com.looker.droidify.network.Downloader import com.looker.droidify.network.NetworkResponse @@ -49,13 +50,18 @@ class DownloadStatsWorker @AssistedInject constructor( val downloadSemaphores = Semaphore(2) override suspend fun doWork(): Result = withContext(Dispatchers.IO) { + val settings = settingsRepo.getInitial() + if (!settings.dlStatsEnabled) { + Log.i(TAG, "Download statistics disabled, skipping") + return@withContext Result.success() + } try { setForegroundAsync( context .createDownloadStatsNotification() .toForegroundInfo(Constants.NOTIFICATION_ID_STATS_DOWNLOAD) ) - fetchData() + fetchData(settings) Log.i(TAG, "Successfully processed download stats monthly files") Result.success() } catch (e: Exception) { @@ -66,14 +72,14 @@ class DownloadStatsWorker @AssistedInject constructor( } @OptIn(ExperimentalAtomicApi::class) - private suspend fun fetchData() = withContext(Dispatchers.IO) { + private suspend fun fetchData(settings: Settings) = withContext(Dispatchers.IO) { supervisorScope { - val lastModified = settingsRepo.getInitial().lastModifiedDownloadStats - val fileNames = ConcurrentLinkedQueue(generateMonthlyFileNames(lastModified)) + val lastModified = settings.lastModifiedDownloadStats + val fileNames = ConcurrentLinkedQueue( + generateMonthlyFileNames(lastModified) + ) val successfulResults = AtomicInt(0) val updatedResults = AtomicInt(0) - val calendar = Calendar.getInstance() - calendar.timeInMillis = System.currentTimeMillis() Log.d(TAG, "Fetching ${fileNames.size} monthly files") while (fileNames.isNotEmpty()) { diff --git a/app/src/main/kotlin/com/looker/droidify/work/RBLogWorker.kt b/app/src/main/kotlin/com/looker/droidify/work/RBLogWorker.kt index 6f6a05c6b..bd6a7211f 100644 --- a/app/src/main/kotlin/com/looker/droidify/work/RBLogWorker.kt +++ b/app/src/main/kotlin/com/looker/droidify/work/RBLogWorker.kt @@ -39,9 +39,14 @@ class RBLogWorker @AssistedInject constructor( @OptIn(ExperimentalTime::class) override suspend fun doWork(): Result = withContext(Dispatchers.IO) { + val settings = settingsRepository.getInitial() + if (!settings.rbLogsEnabled) { + Log.i(TAG, "Reproducibility logs disabled, skipping") + return@withContext Result.success() + } val target = Cache.getTemporaryFile(context) try { - val lastModified = settingsRepository.getInitial().lastRbLogFetch + val lastModified = settings.lastRbLogFetch val response = downloader.downloadToFile( url = BASE_URL, target = target, diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index fc5b0358b..a937bdc53 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -269,6 +269,9 @@ Monthly Downloading reproducibility logs Downloading download stats + Fetch and display monthly download counts from IzzyOnDroid. Disabling clears existing download stats. + Reproducibility Logs + Fetch and display reproducible build verification logs from IzzyOnDroid. Disabling clears existing reproducibility logs. Custom buttons Add quick links to external services in app details