Loading play-services-core/build.gradle +1 −0 Original line number Diff line number Diff line Loading @@ -137,6 +137,7 @@ android { buildConfigField "String", "RECAPTCHA_ENTERPRISE_PROJECT_ID", "\"${localProperties.get("recaptchaEnterpreise.projectId", "")}\"" buildConfigField "String", "RECAPTCHA_ENTERPRISE_SITE_KEY", "\"${localProperties.get("recaptchaEnterpreise.siteKey", "")}\"" buildConfigField "String", "RECAPTCHA_ENTERPRISE_API_KEY", "\"${localProperties.get("recaptchaEnterpreise.apiKey", "")}\"" buildConfigField "String", "HIDE_MICROG_PACKAGES_URL", "\"${localProperties.getProperty("hidemicrog.url", System.getenv('HIDE_MICROG_PACKAGES_URL') ?: "")}\"" } splits { Loading play-services-core/src/main/AndroidManifest.xml +11 −0 Original line number Diff line number Diff line Loading @@ -15,6 +15,9 @@ --> <manifest xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools"> <uses-permission android:name="android.permission.WRITE_SECURE_SETTINGS" tools:ignore="ProtectedPermissions" /> <uses-permission android:name="android.permission.PROVIDE_DEFAULT_ENABLED_CREDENTIAL_SERVICE" tools:ignore="ProtectedPermissions" /> Loading Loading @@ -427,6 +430,14 @@ </intent-filter> </receiver> <receiver android:name="org.microg.gms.ui.HideMicrogSyncReceiver"> <intent-filter> <action android:name="android.intent.action.BOOT_COMPLETED" /> <action android:name="android.intent.action.MY_PACKAGE_REPLACED" /> </intent-filter> </receiver> <!-- Car --> <service android:name="org.microg.gms.car.CarService"> Loading play-services-core/src/main/kotlin/org/microg/gms/ui/HideMicrogFragment.kt 0 → 100644 +186 −0 Original line number Diff line number Diff line /* * SPDX-FileCopyrightText: 2026 microG Project Team * SPDX-License-Identifier: Apache-2.0 */ package org.microg.gms.ui import android.content.Context import android.content.pm.ApplicationInfo import android.content.pm.PackageManager import android.os.Bundle import android.widget.Toast import androidx.lifecycle.lifecycleScope import androidx.preference.CheckBoxPreference import androidx.preference.ListPreference import androidx.preference.Preference import androidx.preference.PreferenceCategory import androidx.preference.PreferenceFragmentCompat import com.google.android.gms.R import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext class HideMicrogFragment : PreferenceFragmentCompat() { private lateinit var mode: ListPreference private lateinit var progress: Preference private lateinit var apps: PreferenceCategory private lateinit var none: Preference private val selectedPackages = linkedSetOf<String>() override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) { addPreferencesFromResource(R.xml.preferences_hide_microg) mode = preferenceScreen.findPreference("pref_hide_microg_mode") ?: mode progress = preferenceScreen.findPreference("pref_hide_microg_progress") ?: progress apps = preferenceScreen.findPreference("prefcat_hide_microg_apps") ?: apps none = preferenceScreen.findPreference("pref_hide_microg_none") ?: none configureStaticPreferences() } override fun onResume() { super.onResume() updatePreferenceState() updateContent() } private fun configureStaticPreferences() { val context = requireContext() mode.isPersistent = false mode.entries = arrayOf( getString(R.string.pref_hide_microg_mode_manual), getString(R.string.pref_hide_microg_mode_automatic) ) mode.entryValues = arrayOf(HideMicrogSettings.MODE_MANUAL, HideMicrogSettings.MODE_AUTOMATIC) mode.value = HideMicrogSettings.getMode(context) mode.setOnPreferenceChangeListener { _, newValue -> val newMode = newValue as? String ?: return@setOnPreferenceChangeListener false HideMicrogSettings.setMode(context, newMode) updatePreferenceState(newMode) updateContent(showFeedback = newMode == HideMicrogSettings.MODE_AUTOMATIC) true } } private fun updatePreferenceState( modeValue: String = HideMicrogSettings.getMode(requireContext()) ) { val automaticMode = modeValue == HideMicrogSettings.MODE_AUTOMATIC mode.summary = if (automaticMode) { getString(R.string.pref_hide_microg_mode_automatic) } else { getString(R.string.pref_hide_microg_mode_manual) } } private fun updateContent(showFeedback: Boolean = false) { lifecycleScope.launchWhenResumed { val context = requireContext() val automaticMode = HideMicrogSettings.isAutomaticMode(context) updatePreferenceState(HideMicrogSettings.getMode(context)) progress.isVisible = true val selected = try { if (automaticMode) { HideMicrogSettings.syncPackagesFromUrl(context) } else { HideMicrogSettings.readPackages(context) } } catch (e: Exception) { if (showFeedback || automaticMode) { Toast.makeText( context, e.message?.takeIf { it.isNotBlank() } ?: getString(R.string.pref_hide_microg_fetch_failed), Toast.LENGTH_LONG ).show() } HideMicrogSettings.readPackages(context) } selectedPackages.clear() selectedPackages.addAll(selected) val appRows = loadAppRows(context, selected) renderAppRows( appRows = appRows, automaticMode = automaticMode, emptyMessage = getString(org.microg.gms.base.core.R.string.list_no_item_none) ) progress.isVisible = false } } private suspend fun loadAppRows(context: Context, selected: Set<String>): List<AppRow> = withContext(Dispatchers.IO) { val packageManager = context.packageManager packageManager.getInstalledApplications(PackageManager.GET_META_DATA) .asSequence() .filter { applicationInfo -> applicationInfo.packageName != context.packageName && applicationInfo.flags and (ApplicationInfo.FLAG_SYSTEM or ApplicationInfo.FLAG_UPDATED_SYSTEM_APP) == 0 } .map { applicationInfo -> val label = applicationInfo.loadLabel(packageManager).toString().trim() val displayName = if (label.isNotEmpty()) label else applicationInfo.packageName AppRow( packageName = applicationInfo.packageName, label = displayName ) } .sortedWith(compareByDescending<AppRow> { it.packageName in selected } .thenBy { it.label.lowercase() } .thenBy { it.packageName }) .toList() } private fun renderAppRows(appRows: List<AppRow>, automaticMode: Boolean, emptyMessage: String) { apps.removeAll() apps.isVisible = true if (appRows.isEmpty()) { none.title = emptyMessage none.isVisible = true apps.addPreference(none) return } none.isVisible = false appRows.forEachIndexed { index, app -> apps.addPreference(createAppPreference(app, index, automaticMode)) } } private fun createAppPreference(app: AppRow, order: Int, automaticMode: Boolean): CheckBoxPreference { return CheckBoxPreference(requireContext()).apply { key = "pref_hide_microg_${app.packageName}" title = app.label summary = app.packageName icon = requireContext().packageManager.getApplicationIcon(app.packageName) isPersistent = false isChecked = app.packageName in selectedPackages isEnabled = !automaticMode this.order = order if (!automaticMode) { setOnPreferenceChangeListener { preference, newValue -> if (newValue !is Boolean) return@setOnPreferenceChangeListener false val updatedPackages = selectedPackages.toMutableSet().apply { if (newValue) add(app.packageName) else remove(app.packageName) } val success = runCatching { HideMicrogSettings.writePackages(preference.context, updatedPackages) }.getOrDefault(false) if (!success) { Toast.makeText(preference.context, R.string.pref_hide_microg_write_failed, Toast.LENGTH_LONG).show() return@setOnPreferenceChangeListener false } selectedPackages.clear() selectedPackages.addAll(updatedPackages) updateContent() true } } } } private data class AppRow( val packageName: String, val label: String ) } play-services-core/src/main/kotlin/org/microg/gms/ui/HideMicrogSettings.kt 0 → 100644 +195 −0 Original line number Diff line number Diff line /* * SPDX-FileCopyrightText: 2026 microG Project Team * SPDX-License-Identifier: Apache-2.0 */ package org.microg.gms.ui import android.content.Context import android.provider.Settings import androidx.preference.PreferenceManager import androidx.work.BackoffPolicy import androidx.work.Constraints import androidx.work.ExistingWorkPolicy import androidx.work.NetworkType import androidx.work.OneTimeWorkRequestBuilder import androidx.work.WorkManager import com.google.android.gms.BuildConfig import com.google.android.gms.R import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext import org.json.JSONArray import org.json.JSONObject import java.io.IOException import java.net.HttpURLConnection import java.net.URL import java.util.TreeSet import java.util.concurrent.TimeUnit object HideMicrogSettings { const val KEY = "hide_microg_packages_for_package" const val MODE_MANUAL = "manual" const val MODE_AUTOMATIC = "automatic" private const val PREF_MODE = "pref_hide_microg_mode" private val DEFAULT_URL = BuildConfig.HIDE_MICROG_PACKAGES_URL.trim() private val PACKAGE_NAME_REGEX = Regex("^[A-Za-z][A-Za-z0-9_]*(\\.[A-Za-z][A-Za-z0-9_]*)+$") fun readPackages(context: Context): Set<String> { val rawValue = Settings.System.getString(context.contentResolver, KEY).orEmpty() return rawValue .split(',', ';', '\n') .map { it.trim() } .filter { it.isNotEmpty() } .toCollection(TreeSet()) } fun writePackages(context: Context, packages: Set<String>): Boolean { val normalized = packages .map { it.trim() } .filter { it.isNotEmpty() } .toSortedSet() .joinToString(",") return Settings.System.putString(context.contentResolver, KEY, normalized) } fun getMode(context: Context): String { val value = PreferenceManager.getDefaultSharedPreferences(context) .getString(PREF_MODE, MODE_AUTOMATIC) .orEmpty() return if (value == MODE_AUTOMATIC) MODE_AUTOMATIC else MODE_MANUAL } fun setMode(context: Context, mode: String) { PreferenceManager.getDefaultSharedPreferences(context) .edit() .putString(PREF_MODE, if (mode == MODE_AUTOMATIC) MODE_AUTOMATIC else MODE_MANUAL) .apply() } fun getUrl(): String = DEFAULT_URL fun isAutomaticMode(context: Context): Boolean = getMode(context) == MODE_AUTOMATIC fun isSupportedUrl(url: String): Boolean = url.startsWith("https://") || url.startsWith("http://") fun enqueueAutomaticSync(context: Context) { if (!isAutomaticMode(context)) return val url = getUrl() if (url.isBlank() || !isSupportedUrl(url)) return val constraints = Constraints.Builder() .setRequiredNetworkType(NetworkType.CONNECTED) .build() val request = OneTimeWorkRequestBuilder<HideMicrogSyncWorker>() .setConstraints(constraints) .setBackoffCriteria(BackoffPolicy.EXPONENTIAL, 10, TimeUnit.MINUTES) .build() WorkManager.getInstance(context.applicationContext).enqueueUniqueWork( HideMicrogSyncWorker.WORK_NAME, ExistingWorkPolicy.REPLACE, request ) } suspend fun syncPackagesFromUrl(context: Context): Set<String> = withContext(Dispatchers.IO) { val url = getUrl() if (url.isBlank() || !isSupportedUrl(url)) { throw IOException(context.getString(R.string.pref_hide_microg_fetch_failed)) } val packages = fetchPackages(url) if (!writePackages(context, packages)) { throw IOException(context.getString(R.string.pref_hide_microg_write_failed)) } packages } fun getSummary(context: Context): String { val count = readPackages(context).size return if (count == 0) { context.getString(R.string.pref_hide_microg_summary_none) } else { context.resources.getQuantityString(R.plurals.pref_hide_microg_summary_count, count, count) } } private fun fetchPackages(urlString: String): Set<String> { val connection = (URL(urlString).openConnection() as HttpURLConnection).apply { requestMethod = "GET" doInput = true connectTimeout = 15000 readTimeout = 15000 setRequestProperty("Accept", "application/json, text/plain;q=0.9, */*;q=0.8") } return try { val responseCode = connection.responseCode val responseBody = (if (responseCode in 200..299) connection.inputStream else connection.errorStream) ?.bufferedReader() ?.use { it.readText() } .orEmpty() if (responseCode !in 200..299) { throw IOException(responseBody.ifBlank { connection.responseMessage?.takeIf { it.isNotBlank() } ?: "HTTP $responseCode" }) } parsePackageResponse(responseBody) } finally { connection.disconnect() } } private fun parsePackageResponse(responseBody: String): Set<String> { val trimmed = responseBody.trim() if (trimmed.isEmpty()) return emptySet() return runCatching { parseJsonPackages(trimmed) }.getOrElse { parseDelimitedPackages(trimmed) } } private fun parseJsonPackages(rawValue: String): Set<String> { if (rawValue.startsWith("[")) { return normalizePackages(readJsonArray(JSONArray(rawValue))) } if (!rawValue.startsWith("{")) { throw IOException("Response is not JSON") } val jsonObject = JSONObject(rawValue) for (key in listOf("packages", "apps", "packageNames", "items")) { val array = jsonObject.optJSONArray(key) ?: continue return normalizePackages(readJsonArray(array)) } throw IOException("No package list found in JSON response") } private fun readJsonArray(array: JSONArray): Sequence<String> = sequence { for (index in 0 until array.length()) { when (val value = array.opt(index)) { is String -> yield(value) is JSONObject -> { val packageName = value.optString("packageName") .ifBlank { value.optString("package") } if (packageName.isNotBlank()) yield(packageName) } } } } private fun parseDelimitedPackages(rawValue: String): Set<String> = normalizePackages( rawValue.lineSequence().flatMap { line -> line.substringBefore('#').split(',', ';').asSequence() } ) private fun normalizePackages(packages: Sequence<String>): Set<String> { val tokens = packages .map { it.trim().removeSurrounding("\"").removeSurrounding("'") } .filter { it.isNotBlank() } .toList() val normalized = tokens .filter { PACKAGE_NAME_REGEX.matches(it) } .toCollection(TreeSet()) if (tokens.isNotEmpty() && normalized.isEmpty()) { throw IOException("No valid package names found in response") } return normalized } } play-services-core/src/main/kotlin/org/microg/gms/ui/HideMicrogSyncReceiver.kt 0 → 100644 +27 −0 Original line number Diff line number Diff line /* * SPDX-FileCopyrightText: 2026 microG Project Team * SPDX-License-Identifier: Apache-2.0 */ package org.microg.gms.ui import android.content.BroadcastReceiver import android.content.Context import android.content.Intent import android.util.Log class HideMicrogSyncReceiver : BroadcastReceiver() { override fun onReceive(context: Context, intent: Intent?) { when (intent?.action) { Intent.ACTION_BOOT_COMPLETED, Intent.ACTION_MY_PACKAGE_REPLACED -> { Log.d(TAG, "Scheduling automatic Hide from apps sync for ${intent.action}") HideMicrogSettings.enqueueAutomaticSync(context) } } } companion object { private const val TAG = "HideMicrogSyncReceiver" } } Loading
play-services-core/build.gradle +1 −0 Original line number Diff line number Diff line Loading @@ -137,6 +137,7 @@ android { buildConfigField "String", "RECAPTCHA_ENTERPRISE_PROJECT_ID", "\"${localProperties.get("recaptchaEnterpreise.projectId", "")}\"" buildConfigField "String", "RECAPTCHA_ENTERPRISE_SITE_KEY", "\"${localProperties.get("recaptchaEnterpreise.siteKey", "")}\"" buildConfigField "String", "RECAPTCHA_ENTERPRISE_API_KEY", "\"${localProperties.get("recaptchaEnterpreise.apiKey", "")}\"" buildConfigField "String", "HIDE_MICROG_PACKAGES_URL", "\"${localProperties.getProperty("hidemicrog.url", System.getenv('HIDE_MICROG_PACKAGES_URL') ?: "")}\"" } splits { Loading
play-services-core/src/main/AndroidManifest.xml +11 −0 Original line number Diff line number Diff line Loading @@ -15,6 +15,9 @@ --> <manifest xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools"> <uses-permission android:name="android.permission.WRITE_SECURE_SETTINGS" tools:ignore="ProtectedPermissions" /> <uses-permission android:name="android.permission.PROVIDE_DEFAULT_ENABLED_CREDENTIAL_SERVICE" tools:ignore="ProtectedPermissions" /> Loading Loading @@ -427,6 +430,14 @@ </intent-filter> </receiver> <receiver android:name="org.microg.gms.ui.HideMicrogSyncReceiver"> <intent-filter> <action android:name="android.intent.action.BOOT_COMPLETED" /> <action android:name="android.intent.action.MY_PACKAGE_REPLACED" /> </intent-filter> </receiver> <!-- Car --> <service android:name="org.microg.gms.car.CarService"> Loading
play-services-core/src/main/kotlin/org/microg/gms/ui/HideMicrogFragment.kt 0 → 100644 +186 −0 Original line number Diff line number Diff line /* * SPDX-FileCopyrightText: 2026 microG Project Team * SPDX-License-Identifier: Apache-2.0 */ package org.microg.gms.ui import android.content.Context import android.content.pm.ApplicationInfo import android.content.pm.PackageManager import android.os.Bundle import android.widget.Toast import androidx.lifecycle.lifecycleScope import androidx.preference.CheckBoxPreference import androidx.preference.ListPreference import androidx.preference.Preference import androidx.preference.PreferenceCategory import androidx.preference.PreferenceFragmentCompat import com.google.android.gms.R import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext class HideMicrogFragment : PreferenceFragmentCompat() { private lateinit var mode: ListPreference private lateinit var progress: Preference private lateinit var apps: PreferenceCategory private lateinit var none: Preference private val selectedPackages = linkedSetOf<String>() override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) { addPreferencesFromResource(R.xml.preferences_hide_microg) mode = preferenceScreen.findPreference("pref_hide_microg_mode") ?: mode progress = preferenceScreen.findPreference("pref_hide_microg_progress") ?: progress apps = preferenceScreen.findPreference("prefcat_hide_microg_apps") ?: apps none = preferenceScreen.findPreference("pref_hide_microg_none") ?: none configureStaticPreferences() } override fun onResume() { super.onResume() updatePreferenceState() updateContent() } private fun configureStaticPreferences() { val context = requireContext() mode.isPersistent = false mode.entries = arrayOf( getString(R.string.pref_hide_microg_mode_manual), getString(R.string.pref_hide_microg_mode_automatic) ) mode.entryValues = arrayOf(HideMicrogSettings.MODE_MANUAL, HideMicrogSettings.MODE_AUTOMATIC) mode.value = HideMicrogSettings.getMode(context) mode.setOnPreferenceChangeListener { _, newValue -> val newMode = newValue as? String ?: return@setOnPreferenceChangeListener false HideMicrogSettings.setMode(context, newMode) updatePreferenceState(newMode) updateContent(showFeedback = newMode == HideMicrogSettings.MODE_AUTOMATIC) true } } private fun updatePreferenceState( modeValue: String = HideMicrogSettings.getMode(requireContext()) ) { val automaticMode = modeValue == HideMicrogSettings.MODE_AUTOMATIC mode.summary = if (automaticMode) { getString(R.string.pref_hide_microg_mode_automatic) } else { getString(R.string.pref_hide_microg_mode_manual) } } private fun updateContent(showFeedback: Boolean = false) { lifecycleScope.launchWhenResumed { val context = requireContext() val automaticMode = HideMicrogSettings.isAutomaticMode(context) updatePreferenceState(HideMicrogSettings.getMode(context)) progress.isVisible = true val selected = try { if (automaticMode) { HideMicrogSettings.syncPackagesFromUrl(context) } else { HideMicrogSettings.readPackages(context) } } catch (e: Exception) { if (showFeedback || automaticMode) { Toast.makeText( context, e.message?.takeIf { it.isNotBlank() } ?: getString(R.string.pref_hide_microg_fetch_failed), Toast.LENGTH_LONG ).show() } HideMicrogSettings.readPackages(context) } selectedPackages.clear() selectedPackages.addAll(selected) val appRows = loadAppRows(context, selected) renderAppRows( appRows = appRows, automaticMode = automaticMode, emptyMessage = getString(org.microg.gms.base.core.R.string.list_no_item_none) ) progress.isVisible = false } } private suspend fun loadAppRows(context: Context, selected: Set<String>): List<AppRow> = withContext(Dispatchers.IO) { val packageManager = context.packageManager packageManager.getInstalledApplications(PackageManager.GET_META_DATA) .asSequence() .filter { applicationInfo -> applicationInfo.packageName != context.packageName && applicationInfo.flags and (ApplicationInfo.FLAG_SYSTEM or ApplicationInfo.FLAG_UPDATED_SYSTEM_APP) == 0 } .map { applicationInfo -> val label = applicationInfo.loadLabel(packageManager).toString().trim() val displayName = if (label.isNotEmpty()) label else applicationInfo.packageName AppRow( packageName = applicationInfo.packageName, label = displayName ) } .sortedWith(compareByDescending<AppRow> { it.packageName in selected } .thenBy { it.label.lowercase() } .thenBy { it.packageName }) .toList() } private fun renderAppRows(appRows: List<AppRow>, automaticMode: Boolean, emptyMessage: String) { apps.removeAll() apps.isVisible = true if (appRows.isEmpty()) { none.title = emptyMessage none.isVisible = true apps.addPreference(none) return } none.isVisible = false appRows.forEachIndexed { index, app -> apps.addPreference(createAppPreference(app, index, automaticMode)) } } private fun createAppPreference(app: AppRow, order: Int, automaticMode: Boolean): CheckBoxPreference { return CheckBoxPreference(requireContext()).apply { key = "pref_hide_microg_${app.packageName}" title = app.label summary = app.packageName icon = requireContext().packageManager.getApplicationIcon(app.packageName) isPersistent = false isChecked = app.packageName in selectedPackages isEnabled = !automaticMode this.order = order if (!automaticMode) { setOnPreferenceChangeListener { preference, newValue -> if (newValue !is Boolean) return@setOnPreferenceChangeListener false val updatedPackages = selectedPackages.toMutableSet().apply { if (newValue) add(app.packageName) else remove(app.packageName) } val success = runCatching { HideMicrogSettings.writePackages(preference.context, updatedPackages) }.getOrDefault(false) if (!success) { Toast.makeText(preference.context, R.string.pref_hide_microg_write_failed, Toast.LENGTH_LONG).show() return@setOnPreferenceChangeListener false } selectedPackages.clear() selectedPackages.addAll(updatedPackages) updateContent() true } } } } private data class AppRow( val packageName: String, val label: String ) }
play-services-core/src/main/kotlin/org/microg/gms/ui/HideMicrogSettings.kt 0 → 100644 +195 −0 Original line number Diff line number Diff line /* * SPDX-FileCopyrightText: 2026 microG Project Team * SPDX-License-Identifier: Apache-2.0 */ package org.microg.gms.ui import android.content.Context import android.provider.Settings import androidx.preference.PreferenceManager import androidx.work.BackoffPolicy import androidx.work.Constraints import androidx.work.ExistingWorkPolicy import androidx.work.NetworkType import androidx.work.OneTimeWorkRequestBuilder import androidx.work.WorkManager import com.google.android.gms.BuildConfig import com.google.android.gms.R import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext import org.json.JSONArray import org.json.JSONObject import java.io.IOException import java.net.HttpURLConnection import java.net.URL import java.util.TreeSet import java.util.concurrent.TimeUnit object HideMicrogSettings { const val KEY = "hide_microg_packages_for_package" const val MODE_MANUAL = "manual" const val MODE_AUTOMATIC = "automatic" private const val PREF_MODE = "pref_hide_microg_mode" private val DEFAULT_URL = BuildConfig.HIDE_MICROG_PACKAGES_URL.trim() private val PACKAGE_NAME_REGEX = Regex("^[A-Za-z][A-Za-z0-9_]*(\\.[A-Za-z][A-Za-z0-9_]*)+$") fun readPackages(context: Context): Set<String> { val rawValue = Settings.System.getString(context.contentResolver, KEY).orEmpty() return rawValue .split(',', ';', '\n') .map { it.trim() } .filter { it.isNotEmpty() } .toCollection(TreeSet()) } fun writePackages(context: Context, packages: Set<String>): Boolean { val normalized = packages .map { it.trim() } .filter { it.isNotEmpty() } .toSortedSet() .joinToString(",") return Settings.System.putString(context.contentResolver, KEY, normalized) } fun getMode(context: Context): String { val value = PreferenceManager.getDefaultSharedPreferences(context) .getString(PREF_MODE, MODE_AUTOMATIC) .orEmpty() return if (value == MODE_AUTOMATIC) MODE_AUTOMATIC else MODE_MANUAL } fun setMode(context: Context, mode: String) { PreferenceManager.getDefaultSharedPreferences(context) .edit() .putString(PREF_MODE, if (mode == MODE_AUTOMATIC) MODE_AUTOMATIC else MODE_MANUAL) .apply() } fun getUrl(): String = DEFAULT_URL fun isAutomaticMode(context: Context): Boolean = getMode(context) == MODE_AUTOMATIC fun isSupportedUrl(url: String): Boolean = url.startsWith("https://") || url.startsWith("http://") fun enqueueAutomaticSync(context: Context) { if (!isAutomaticMode(context)) return val url = getUrl() if (url.isBlank() || !isSupportedUrl(url)) return val constraints = Constraints.Builder() .setRequiredNetworkType(NetworkType.CONNECTED) .build() val request = OneTimeWorkRequestBuilder<HideMicrogSyncWorker>() .setConstraints(constraints) .setBackoffCriteria(BackoffPolicy.EXPONENTIAL, 10, TimeUnit.MINUTES) .build() WorkManager.getInstance(context.applicationContext).enqueueUniqueWork( HideMicrogSyncWorker.WORK_NAME, ExistingWorkPolicy.REPLACE, request ) } suspend fun syncPackagesFromUrl(context: Context): Set<String> = withContext(Dispatchers.IO) { val url = getUrl() if (url.isBlank() || !isSupportedUrl(url)) { throw IOException(context.getString(R.string.pref_hide_microg_fetch_failed)) } val packages = fetchPackages(url) if (!writePackages(context, packages)) { throw IOException(context.getString(R.string.pref_hide_microg_write_failed)) } packages } fun getSummary(context: Context): String { val count = readPackages(context).size return if (count == 0) { context.getString(R.string.pref_hide_microg_summary_none) } else { context.resources.getQuantityString(R.plurals.pref_hide_microg_summary_count, count, count) } } private fun fetchPackages(urlString: String): Set<String> { val connection = (URL(urlString).openConnection() as HttpURLConnection).apply { requestMethod = "GET" doInput = true connectTimeout = 15000 readTimeout = 15000 setRequestProperty("Accept", "application/json, text/plain;q=0.9, */*;q=0.8") } return try { val responseCode = connection.responseCode val responseBody = (if (responseCode in 200..299) connection.inputStream else connection.errorStream) ?.bufferedReader() ?.use { it.readText() } .orEmpty() if (responseCode !in 200..299) { throw IOException(responseBody.ifBlank { connection.responseMessage?.takeIf { it.isNotBlank() } ?: "HTTP $responseCode" }) } parsePackageResponse(responseBody) } finally { connection.disconnect() } } private fun parsePackageResponse(responseBody: String): Set<String> { val trimmed = responseBody.trim() if (trimmed.isEmpty()) return emptySet() return runCatching { parseJsonPackages(trimmed) }.getOrElse { parseDelimitedPackages(trimmed) } } private fun parseJsonPackages(rawValue: String): Set<String> { if (rawValue.startsWith("[")) { return normalizePackages(readJsonArray(JSONArray(rawValue))) } if (!rawValue.startsWith("{")) { throw IOException("Response is not JSON") } val jsonObject = JSONObject(rawValue) for (key in listOf("packages", "apps", "packageNames", "items")) { val array = jsonObject.optJSONArray(key) ?: continue return normalizePackages(readJsonArray(array)) } throw IOException("No package list found in JSON response") } private fun readJsonArray(array: JSONArray): Sequence<String> = sequence { for (index in 0 until array.length()) { when (val value = array.opt(index)) { is String -> yield(value) is JSONObject -> { val packageName = value.optString("packageName") .ifBlank { value.optString("package") } if (packageName.isNotBlank()) yield(packageName) } } } } private fun parseDelimitedPackages(rawValue: String): Set<String> = normalizePackages( rawValue.lineSequence().flatMap { line -> line.substringBefore('#').split(',', ';').asSequence() } ) private fun normalizePackages(packages: Sequence<String>): Set<String> { val tokens = packages .map { it.trim().removeSurrounding("\"").removeSurrounding("'") } .filter { it.isNotBlank() } .toList() val normalized = tokens .filter { PACKAGE_NAME_REGEX.matches(it) } .toCollection(TreeSet()) if (tokens.isNotEmpty() && normalized.isEmpty()) { throw IOException("No valid package names found in response") } return normalized } }
play-services-core/src/main/kotlin/org/microg/gms/ui/HideMicrogSyncReceiver.kt 0 → 100644 +27 −0 Original line number Diff line number Diff line /* * SPDX-FileCopyrightText: 2026 microG Project Team * SPDX-License-Identifier: Apache-2.0 */ package org.microg.gms.ui import android.content.BroadcastReceiver import android.content.Context import android.content.Intent import android.util.Log class HideMicrogSyncReceiver : BroadcastReceiver() { override fun onReceive(context: Context, intent: Intent?) { when (intent?.action) { Intent.ACTION_BOOT_COMPLETED, Intent.ACTION_MY_PACKAGE_REPLACED -> { Log.d(TAG, "Scheduling automatic Hide from apps sync for ${intent.action}") HideMicrogSettings.enqueueAutomaticSync(context) } } } companion object { private const val TAG = "HideMicrogSyncReceiver" } }