Loading play-services-base/core/src/main/kotlin/org/microg/gms/settings/SettingsContract.kt +2 −0 Original line number Diff line number Diff line Loading @@ -279,6 +279,7 @@ object SettingsContract { const val ASSET_DEVICE_SYNC = "vending_device_sync" const val APPS_INSTALL = "vending_apps_install" const val APPS_INSTALLER_LIST = "vending_apps_installer_list" const val PLAY_INTEGRITY_APP_LIST = "vending_play_integrity_apps" val PROJECTION = arrayOf( LICENSING, Loading @@ -289,6 +290,7 @@ object SettingsContract { ASSET_DEVICE_SYNC, APPS_INSTALL, APPS_INSTALLER_LIST, PLAY_INTEGRITY_APP_LIST ) } Loading play-services-base/core/src/main/kotlin/org/microg/gms/settings/SettingsProvider.kt +2 −0 Original line number Diff line number Diff line Loading @@ -369,6 +369,7 @@ class SettingsProvider : ContentProvider() { Vending.SPLIT_INSTALL -> getSettingsBoolean(key, false) Vending.APPS_INSTALL -> getSettingsBoolean(key, false) Vending.APPS_INSTALLER_LIST -> getSettingsString(key, "") Vending.PLAY_INTEGRITY_APP_LIST -> getSettingsString(key, "") else -> throw IllegalArgumentException("Unknown key: $key") } } Loading @@ -386,6 +387,7 @@ class SettingsProvider : ContentProvider() { Vending.ASSET_DEVICE_SYNC -> editor.putBoolean(key, value as Boolean) Vending.APPS_INSTALL -> editor.putBoolean(key, value as Boolean) Vending.APPS_INSTALLER_LIST -> editor.putString(key, value as String) Vending.PLAY_INTEGRITY_APP_LIST -> editor.putString(key, value as String) else -> throw IllegalArgumentException("Unknown key: $key") } } Loading play-services-base/core/src/main/kotlin/org/microg/gms/vending/PlayIntegrityData.kt 0 → 100644 +71 −0 Original line number Diff line number Diff line /** * SPDX-FileCopyrightText: 2025 microG Project Team * SPDX-License-Identifier: Apache-2.0 */ package org.microg.gms.vending import org.json.JSONException import org.json.JSONObject class PlayIntegrityData(var allowed: Boolean, val packageName: String, val pkgSignSha256: String, var lastTime: Long, var lastResult: String? = null, var lastStatus: Boolean = false) { override fun toString(): String { return JSONObject() .put(ALLOWED, allowed) .put(PACKAGE_NAME, packageName) .put(SIGNATURE, pkgSignSha256) .put(LAST_VISIT_TIME, lastTime) .put(LAST_VISIT_RESULT, lastResult) .put(LAST_VISIT_STATUS, lastStatus) .toString() } companion object { private const val PACKAGE_NAME = "packageName" private const val ALLOWED = "allowed" private const val SIGNATURE = "signature" private const val LAST_VISIT_TIME = "lastVisitTime" private const val LAST_VISIT_RESULT = "lastVisitResult" private const val LAST_VISIT_STATUS = "lastVisitStatus" private fun parse(jsonString: String): PlayIntegrityData? { try { val json = JSONObject(jsonString) return PlayIntegrityData( json.getBoolean(ALLOWED), json.getString(PACKAGE_NAME), json.getString(SIGNATURE), json.getLong(LAST_VISIT_TIME), json.getString(LAST_VISIT_RESULT), json.getBoolean(LAST_VISIT_STATUS) ) } catch (e: JSONException) { return null } } fun loadDataSet(content: String): Set<PlayIntegrityData> { return content.split("|").mapNotNull { parse(it) }.toSet() } fun updateDataSetString(channelList: Set<PlayIntegrityData>, channel: PlayIntegrityData): String { val channelData = channelList.find { it.packageName == channel.packageName && it.pkgSignSha256 == channel.pkgSignSha256 } val newChannelList = if (channelData != null) { channelData.allowed = channel.allowed channelData.lastTime = channel.lastTime channelData.lastResult = channel.lastResult channelData.lastStatus = channel.lastStatus channelList } else { channelList + channel } return newChannelList.let { it -> it.joinToString(separator = "|") { it.toString() } } } } } No newline at end of file play-services-core/src/main/kotlin/org/microg/gms/ui/SafetyNetAllAppsFragment.kt +5 −1 Original line number Diff line number Diff line Loading @@ -18,6 +18,8 @@ import com.google.android.gms.R import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext import org.microg.gms.safetynet.SafetyNetDatabase import org.microg.gms.vending.PlayIntegrityData import org.microg.gms.vending.VendingPreferences class SafetyNetAllAppsFragment : PreferenceFragmentCompat() { private lateinit var database: SafetyNetDatabase Loading Loading @@ -50,8 +52,10 @@ class SafetyNetAllAppsFragment : PreferenceFragmentCompat() { private fun updateContent() { val context = requireContext() lifecycleScope.launchWhenResumed { val playIntegrityData = VendingPreferences.getPlayIntegrityAppList(context) val apps = withContext(Dispatchers.IO) { val res = database.recentApps.map { app -> val playPairs = PlayIntegrityData.loadDataSet(playIntegrityData).map { it.packageName to it.lastTime } val res = (database.recentApps + playPairs).map { app -> val pref = AppIconPreference(context) pref.packageName = app.first pref.summary = when { Loading play-services-core/src/main/kotlin/org/microg/gms/ui/SafetyNetAppFragment.kt +44 −3 Original line number Diff line number Diff line Loading @@ -8,16 +8,26 @@ package org.microg.gms.ui import android.annotation.SuppressLint import android.os.Bundle import android.text.format.DateUtils import androidx.core.content.ContextCompat import androidx.lifecycle.lifecycleScope import androidx.preference.* import androidx.preference.Preference import androidx.preference.PreferenceCategory import androidx.preference.PreferenceFragmentCompat import androidx.preference.SwitchPreferenceCompat import androidx.preference.isEmpty import com.google.android.gms.R import org.microg.gms.safetynet.SafetyNetDatabase import org.microg.gms.safetynet.SafetyNetRequestType.* import org.microg.gms.safetynet.SafetyNetRequestType.ATTESTATION import org.microg.gms.safetynet.SafetyNetRequestType.RECAPTCHA import org.microg.gms.safetynet.SafetyNetRequestType.RECAPTCHA_ENTERPRISE import org.microg.gms.vending.PlayIntegrityData import org.microg.gms.vending.VendingPreferences class SafetyNetAppFragment : PreferenceFragmentCompat() { private lateinit var appHeadingPreference: AppHeadingPreference private lateinit var recents: PreferenceCategory private lateinit var recentsNone: Preference private lateinit var allowRequests: SwitchPreferenceCompat private val packageName: String? get() = arguments?.getString("package") Loading @@ -30,6 +40,16 @@ class SafetyNetAppFragment : PreferenceFragmentCompat() { appHeadingPreference = preferenceScreen.findPreference("pref_safetynet_app_heading") ?: appHeadingPreference recents = preferenceScreen.findPreference("prefcat_safetynet_recent_list") ?: recents recentsNone = preferenceScreen.findPreference("pref_safetynet_recent_none") ?: recentsNone allowRequests = preferenceScreen.findPreference("pref_device_attestation_app_allow_requests") ?: allowRequests allowRequests.setOnPreferenceChangeListener { _, newValue -> val playIntegrityDataSet = loadPlayIntegrityData() val integrityData = packageName?.let { packageName -> playIntegrityDataSet.find { packageName == it.packageName } } if (newValue is Boolean && integrityData != null) { val content = PlayIntegrityData.updateDataSetString(playIntegrityDataSet, integrityData.apply { this.allowed = newValue }) VendingPreferences.setPlayIntegrityAppList(requireContext(), content) } true } } override fun onResume() { Loading @@ -37,6 +57,11 @@ class SafetyNetAppFragment : PreferenceFragmentCompat() { updateContent() } private fun loadPlayIntegrityData(): Set<PlayIntegrityData> { val playIntegrityData = VendingPreferences.getPlayIntegrityAppList(requireContext()) return PlayIntegrityData.loadDataSet(playIntegrityData) } fun updateContent() { lifecycleScope.launchWhenResumed { appHeadingPreference.packageName = packageName Loading @@ -52,7 +77,6 @@ class SafetyNetAppFragment : PreferenceFragmentCompat() { }.orEmpty() recents.removeAll() recents.addPreference(recentsNone) recentsNone.isVisible = summaries.isEmpty() for (summary in summaries) { val preference = Preference(requireContext()) preference.onPreferenceClickListener = Preference.OnPreferenceClickListener { Loading Loading @@ -84,6 +108,23 @@ class SafetyNetAppFragment : PreferenceFragmentCompat() { } recents.addPreference(preference) } val piContent = packageName?.let { packageName -> loadPlayIntegrityData().find { packageName == it.packageName } } if (piContent != null) { val preference = Preference(requireContext()) val date = DateUtils.getRelativeDateTimeString( context, piContent.lastTime, DateUtils.MINUTE_IN_MILLIS, DateUtils.WEEK_IN_MILLIS, DateUtils.FORMAT_SHOW_TIME ) preference.title = date preference.summary = piContent.lastResult preference.icon = if (piContent.lastStatus) ContextCompat.getDrawable(context, R.drawable.ic_circle_check) else ContextCompat.getDrawable(context, R.drawable.ic_circle_warn) recents.addPreference(preference) } recentsNone.isVisible = summaries.isEmpty() && piContent == null allowRequests.isChecked = piContent?.allowed == true } } Loading Loading
play-services-base/core/src/main/kotlin/org/microg/gms/settings/SettingsContract.kt +2 −0 Original line number Diff line number Diff line Loading @@ -279,6 +279,7 @@ object SettingsContract { const val ASSET_DEVICE_SYNC = "vending_device_sync" const val APPS_INSTALL = "vending_apps_install" const val APPS_INSTALLER_LIST = "vending_apps_installer_list" const val PLAY_INTEGRITY_APP_LIST = "vending_play_integrity_apps" val PROJECTION = arrayOf( LICENSING, Loading @@ -289,6 +290,7 @@ object SettingsContract { ASSET_DEVICE_SYNC, APPS_INSTALL, APPS_INSTALLER_LIST, PLAY_INTEGRITY_APP_LIST ) } Loading
play-services-base/core/src/main/kotlin/org/microg/gms/settings/SettingsProvider.kt +2 −0 Original line number Diff line number Diff line Loading @@ -369,6 +369,7 @@ class SettingsProvider : ContentProvider() { Vending.SPLIT_INSTALL -> getSettingsBoolean(key, false) Vending.APPS_INSTALL -> getSettingsBoolean(key, false) Vending.APPS_INSTALLER_LIST -> getSettingsString(key, "") Vending.PLAY_INTEGRITY_APP_LIST -> getSettingsString(key, "") else -> throw IllegalArgumentException("Unknown key: $key") } } Loading @@ -386,6 +387,7 @@ class SettingsProvider : ContentProvider() { Vending.ASSET_DEVICE_SYNC -> editor.putBoolean(key, value as Boolean) Vending.APPS_INSTALL -> editor.putBoolean(key, value as Boolean) Vending.APPS_INSTALLER_LIST -> editor.putString(key, value as String) Vending.PLAY_INTEGRITY_APP_LIST -> editor.putString(key, value as String) else -> throw IllegalArgumentException("Unknown key: $key") } } Loading
play-services-base/core/src/main/kotlin/org/microg/gms/vending/PlayIntegrityData.kt 0 → 100644 +71 −0 Original line number Diff line number Diff line /** * SPDX-FileCopyrightText: 2025 microG Project Team * SPDX-License-Identifier: Apache-2.0 */ package org.microg.gms.vending import org.json.JSONException import org.json.JSONObject class PlayIntegrityData(var allowed: Boolean, val packageName: String, val pkgSignSha256: String, var lastTime: Long, var lastResult: String? = null, var lastStatus: Boolean = false) { override fun toString(): String { return JSONObject() .put(ALLOWED, allowed) .put(PACKAGE_NAME, packageName) .put(SIGNATURE, pkgSignSha256) .put(LAST_VISIT_TIME, lastTime) .put(LAST_VISIT_RESULT, lastResult) .put(LAST_VISIT_STATUS, lastStatus) .toString() } companion object { private const val PACKAGE_NAME = "packageName" private const val ALLOWED = "allowed" private const val SIGNATURE = "signature" private const val LAST_VISIT_TIME = "lastVisitTime" private const val LAST_VISIT_RESULT = "lastVisitResult" private const val LAST_VISIT_STATUS = "lastVisitStatus" private fun parse(jsonString: String): PlayIntegrityData? { try { val json = JSONObject(jsonString) return PlayIntegrityData( json.getBoolean(ALLOWED), json.getString(PACKAGE_NAME), json.getString(SIGNATURE), json.getLong(LAST_VISIT_TIME), json.getString(LAST_VISIT_RESULT), json.getBoolean(LAST_VISIT_STATUS) ) } catch (e: JSONException) { return null } } fun loadDataSet(content: String): Set<PlayIntegrityData> { return content.split("|").mapNotNull { parse(it) }.toSet() } fun updateDataSetString(channelList: Set<PlayIntegrityData>, channel: PlayIntegrityData): String { val channelData = channelList.find { it.packageName == channel.packageName && it.pkgSignSha256 == channel.pkgSignSha256 } val newChannelList = if (channelData != null) { channelData.allowed = channel.allowed channelData.lastTime = channel.lastTime channelData.lastResult = channel.lastResult channelData.lastStatus = channel.lastStatus channelList } else { channelList + channel } return newChannelList.let { it -> it.joinToString(separator = "|") { it.toString() } } } } } No newline at end of file
play-services-core/src/main/kotlin/org/microg/gms/ui/SafetyNetAllAppsFragment.kt +5 −1 Original line number Diff line number Diff line Loading @@ -18,6 +18,8 @@ import com.google.android.gms.R import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext import org.microg.gms.safetynet.SafetyNetDatabase import org.microg.gms.vending.PlayIntegrityData import org.microg.gms.vending.VendingPreferences class SafetyNetAllAppsFragment : PreferenceFragmentCompat() { private lateinit var database: SafetyNetDatabase Loading Loading @@ -50,8 +52,10 @@ class SafetyNetAllAppsFragment : PreferenceFragmentCompat() { private fun updateContent() { val context = requireContext() lifecycleScope.launchWhenResumed { val playIntegrityData = VendingPreferences.getPlayIntegrityAppList(context) val apps = withContext(Dispatchers.IO) { val res = database.recentApps.map { app -> val playPairs = PlayIntegrityData.loadDataSet(playIntegrityData).map { it.packageName to it.lastTime } val res = (database.recentApps + playPairs).map { app -> val pref = AppIconPreference(context) pref.packageName = app.first pref.summary = when { Loading
play-services-core/src/main/kotlin/org/microg/gms/ui/SafetyNetAppFragment.kt +44 −3 Original line number Diff line number Diff line Loading @@ -8,16 +8,26 @@ package org.microg.gms.ui import android.annotation.SuppressLint import android.os.Bundle import android.text.format.DateUtils import androidx.core.content.ContextCompat import androidx.lifecycle.lifecycleScope import androidx.preference.* import androidx.preference.Preference import androidx.preference.PreferenceCategory import androidx.preference.PreferenceFragmentCompat import androidx.preference.SwitchPreferenceCompat import androidx.preference.isEmpty import com.google.android.gms.R import org.microg.gms.safetynet.SafetyNetDatabase import org.microg.gms.safetynet.SafetyNetRequestType.* import org.microg.gms.safetynet.SafetyNetRequestType.ATTESTATION import org.microg.gms.safetynet.SafetyNetRequestType.RECAPTCHA import org.microg.gms.safetynet.SafetyNetRequestType.RECAPTCHA_ENTERPRISE import org.microg.gms.vending.PlayIntegrityData import org.microg.gms.vending.VendingPreferences class SafetyNetAppFragment : PreferenceFragmentCompat() { private lateinit var appHeadingPreference: AppHeadingPreference private lateinit var recents: PreferenceCategory private lateinit var recentsNone: Preference private lateinit var allowRequests: SwitchPreferenceCompat private val packageName: String? get() = arguments?.getString("package") Loading @@ -30,6 +40,16 @@ class SafetyNetAppFragment : PreferenceFragmentCompat() { appHeadingPreference = preferenceScreen.findPreference("pref_safetynet_app_heading") ?: appHeadingPreference recents = preferenceScreen.findPreference("prefcat_safetynet_recent_list") ?: recents recentsNone = preferenceScreen.findPreference("pref_safetynet_recent_none") ?: recentsNone allowRequests = preferenceScreen.findPreference("pref_device_attestation_app_allow_requests") ?: allowRequests allowRequests.setOnPreferenceChangeListener { _, newValue -> val playIntegrityDataSet = loadPlayIntegrityData() val integrityData = packageName?.let { packageName -> playIntegrityDataSet.find { packageName == it.packageName } } if (newValue is Boolean && integrityData != null) { val content = PlayIntegrityData.updateDataSetString(playIntegrityDataSet, integrityData.apply { this.allowed = newValue }) VendingPreferences.setPlayIntegrityAppList(requireContext(), content) } true } } override fun onResume() { Loading @@ -37,6 +57,11 @@ class SafetyNetAppFragment : PreferenceFragmentCompat() { updateContent() } private fun loadPlayIntegrityData(): Set<PlayIntegrityData> { val playIntegrityData = VendingPreferences.getPlayIntegrityAppList(requireContext()) return PlayIntegrityData.loadDataSet(playIntegrityData) } fun updateContent() { lifecycleScope.launchWhenResumed { appHeadingPreference.packageName = packageName Loading @@ -52,7 +77,6 @@ class SafetyNetAppFragment : PreferenceFragmentCompat() { }.orEmpty() recents.removeAll() recents.addPreference(recentsNone) recentsNone.isVisible = summaries.isEmpty() for (summary in summaries) { val preference = Preference(requireContext()) preference.onPreferenceClickListener = Preference.OnPreferenceClickListener { Loading Loading @@ -84,6 +108,23 @@ class SafetyNetAppFragment : PreferenceFragmentCompat() { } recents.addPreference(preference) } val piContent = packageName?.let { packageName -> loadPlayIntegrityData().find { packageName == it.packageName } } if (piContent != null) { val preference = Preference(requireContext()) val date = DateUtils.getRelativeDateTimeString( context, piContent.lastTime, DateUtils.MINUTE_IN_MILLIS, DateUtils.WEEK_IN_MILLIS, DateUtils.FORMAT_SHOW_TIME ) preference.title = date preference.summary = piContent.lastResult preference.icon = if (piContent.lastStatus) ContextCompat.getDrawable(context, R.drawable.ic_circle_check) else ContextCompat.getDrawable(context, R.drawable.ic_circle_warn) recents.addPreference(preference) } recentsNone.isVisible = summaries.isEmpty() && piContent == null allowRequests.isChecked = piContent?.allowed == true } } Loading