From 34048e6a47914ef38cb65654cde4b67d9b28f3b5 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 10 Dec 2025 17:50:56 +0000 Subject: [PATCH 01/19] Bump actions/checkout from 5 to 6 Bumps [actions/checkout](https://github.com/actions/checkout) from 5 to 6. - [Release notes](https://github.com/actions/checkout/releases) - [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md) - [Commits](https://github.com/actions/checkout/compare/v5...v6) --- updated-dependencies: - dependency-name: actions/checkout dependency-version: '6' dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] --- .github/workflows/build.yml | 2 +- .github/workflows/dependency-submission.yml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index d45230acf1..0ae962d846 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -26,7 +26,7 @@ jobs: # Log available space df -h - name: "Checkout sources" - uses: actions/checkout@v5 + uses: actions/checkout@v6 with: fetch-depth: 0 - name: "Setup Java" diff --git a/.github/workflows/dependency-submission.yml b/.github/workflows/dependency-submission.yml index 923e95793e..55d11e560f 100644 --- a/.github/workflows/dependency-submission.yml +++ b/.github/workflows/dependency-submission.yml @@ -13,7 +13,7 @@ jobs: steps: - name: "Checkout sources" - uses: actions/checkout@v5 + uses: actions/checkout@v6 with: fetch-depth: 0 - name: "Setup Java" From 09f6146ac2e54535ed9a946ca804b5a4179201ba Mon Sep 17 00:00:00 2001 From: SBALAVIGNESH123 Date: Sun, 14 Dec 2025 21:15:55 +0530 Subject: [PATCH 02/19] Fix: Resolve all compilation errors for WearOS Bluetooth support - Created missing wearable_device_item.xml layout - Fixed closeConnection() visibility (private to public) - Fixed peerAndroidId type mismatch (String to Long) - Fixed ConnectionConfiguration constructor parameter order - Implemented reflection workaround for Wire library incompatibility The Wire library incompatibility was resolved using reflection to dynamically invoke parseFrom() method, allowing compatibility between Wire 1.6.1 (wearable library) and Wire 4.9.9 (GmsCore) without breaking other modules. Verified with local build - wearable-core module compiles successfully. --- .../wearable/BluetoothWearableConnection.java | 15 ++++-- .../org/microg/gms/wearable/WearableImpl.java | 6 +-- .../main/res/layout/wearable_device_item.xml | 48 +++++++++++++++++++ 3 files changed, 62 insertions(+), 7 deletions(-) create mode 100644 play-services-wearable/core/src/main/res/layout/wearable_device_item.xml diff --git a/play-services-wearable/core/src/main/java/org/microg/gms/wearable/BluetoothWearableConnection.java b/play-services-wearable/core/src/main/java/org/microg/gms/wearable/BluetoothWearableConnection.java index 4d8ae03ff6..ce6bbf7930 100644 --- a/play-services-wearable/core/src/main/java/org/microg/gms/wearable/BluetoothWearableConnection.java +++ b/play-services-wearable/core/src/main/java/org/microg/gms/wearable/BluetoothWearableConnection.java @@ -18,14 +18,13 @@ import android.bluetooth.BluetoothSocket; -import com.squareup.wire.Wire; - import org.microg.wearable.WearableConnection; import org.microg.wearable.proto.MessagePiece; import java.io.DataInputStream; import java.io.DataOutputStream; import java.io.IOException; +import java.lang.reflect.Method; /** * Bluetooth transport implementation for Wearable connections. @@ -46,7 +45,7 @@ public BluetoothWearableConnection(BluetoothSocket socket, Listener listener) th @Override protected void writeMessagePiece(MessagePiece piece) throws IOException { - byte[] bytes = piece.toByteArray(); + byte[] bytes = piece.encode(); os.writeInt(bytes.length); os.write(bytes); os.flush(); @@ -60,7 +59,15 @@ protected MessagePiece readMessagePiece() throws IOException { } byte[] bytes = new byte[len]; is.readFully(bytes); - return new Wire().parseFrom(bytes, MessagePiece.class); + + // Use reflection to call wire.parseFrom() to work around Wire version incompatibility + // The inherited 'wire' instance from WearableConnection uses Wire 1.6.1 API + try { + Method parseFrom = wire.getClass().getMethod("parseFrom", byte[].class, Class.class); + return (MessagePiece) parseFrom.invoke(wire, bytes, MessagePiece.class); + } catch (Exception e) { + throw new IOException("Failed to deserialize MessagePiece: " + e.getMessage(), e); + } } public String getRemoteAddress() { diff --git a/play-services-wearable/core/src/main/java/org/microg/gms/wearable/WearableImpl.java b/play-services-wearable/core/src/main/java/org/microg/gms/wearable/WearableImpl.java index dbc0b89883..722a2837aa 100644 --- a/play-services-wearable/core/src/main/java/org/microg/gms/wearable/WearableImpl.java +++ b/play-services-wearable/core/src/main/java/org/microg/gms/wearable/WearableImpl.java @@ -633,7 +633,7 @@ public String getNodeIdByAddress(String address) { return null; } - private void closeConnection(String nodeId) { + public void closeConnection(String nodeId) { WearableConnection connection; synchronized (activeConnections) { connection = activeConnections.get(nodeId); @@ -765,7 +765,7 @@ public void run() { Log.d(TAG, "Successfully connected via Bluetooth to " + device.getName()); // Create wearable connection wrapper - ConnectionConfiguration config = new ConnectionConfiguration(null, device.getAddress(), device.getName(), 3, true); + ConnectionConfiguration config = new ConnectionConfiguration(device.getName(), device.getAddress(), 3, 0, true); MessageHandler messageHandler = new MessageHandler(context, WearableImpl.this, config); BluetoothWearableConnection connection = new BluetoothWearableConnection(socket, messageHandler); @@ -787,7 +787,7 @@ public void run() { .id(localId) .name("Phone") .networkId(localId) - .peerAndroidId(localId) + .peerAndroidId(0L) .peerVersion(2) // Need at least version 2 for modern WearOS .build()) .build() diff --git a/play-services-wearable/core/src/main/res/layout/wearable_device_item.xml b/play-services-wearable/core/src/main/res/layout/wearable_device_item.xml new file mode 100644 index 0000000000..0e88c4dbed --- /dev/null +++ b/play-services-wearable/core/src/main/res/layout/wearable_device_item.xml @@ -0,0 +1,48 @@ + + + + + + + + + + + + + + + + From c59523d13b70a5b4710a96bffcd530331693acd6 Mon Sep 17 00:00:00 2001 From: SBALAVIGNESH123 Date: Mon, 15 Dec 2025 17:51:29 +0530 Subject: [PATCH 03/19] Fix: Add missing BLUETOOTH_CONNECT permission Added android.permission.BLUETOOTH_CONNECT to AndroidManifest.xml as required by Android 12+ for BluetoothAdapter.getBondedDevices() API call. Fixes lint error identified by ale5000-git in code review. --- play-services-wearable/core/src/main/AndroidManifest.xml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/play-services-wearable/core/src/main/AndroidManifest.xml b/play-services-wearable/core/src/main/AndroidManifest.xml index 311528eaa7..e254e84af0 100644 --- a/play-services-wearable/core/src/main/AndroidManifest.xml +++ b/play-services-wearable/core/src/main/AndroidManifest.xml @@ -6,6 +6,8 @@ + + Date: Sun, 7 Dec 2025 15:09:08 +0000 Subject: [PATCH 04/19] Add permission string for interacting across profiles in Japanese --- .../microg-ui-tools/src/main/res/values-ja/strings.xml | 1 + 1 file changed, 1 insertion(+) diff --git a/play-services-core/microg-ui-tools/src/main/res/values-ja/strings.xml b/play-services-core/microg-ui-tools/src/main/res/values-ja/strings.xml index 2b6a1d512b..6c396dbd32 100644 --- a/play-services-core/microg-ui-tools/src/main/res/values-ja/strings.xml +++ b/play-services-core/microg-ui-tools/src/main/res/values-ja/strings.xml @@ -30,6 +30,7 @@ 権限付与 %1$sの権限: + 仕事用プロファイルを操作する権限: ここをタップして権限を付与してください。 権限を付与しないと、アプリが正しく動作しない可能性があります。 microG UIデモ From cfa7088860839865955d735a8df98590310361a9 Mon Sep 17 00:00:00 2001 From: grenadin Date: Tue, 16 Dec 2025 20:59:21 +0700 Subject: [PATCH 05/19] Add Thai translations for microG-UI-tools component (#3179) --- .../src/main/res/values-th/strings.xml | 45 +++++++++++++++++++ 1 file changed, 45 insertions(+) create mode 100644 play-services-core/microg-ui-tools/src/main/res/values-th/strings.xml diff --git a/play-services-core/microg-ui-tools/src/main/res/values-th/strings.xml b/play-services-core/microg-ui-tools/src/main/res/values-th/strings.xml new file mode 100644 index 0000000000..cfc9669d4c --- /dev/null +++ b/play-services-core/microg-ui-tools/src/main/res/values-th/strings.xml @@ -0,0 +1,45 @@ + + + + + เครื่องมือส่วนต่อประสานกับผู้ใช้ของ microG + Apache License 2.0, ทีม microG + + เวอร์ชัน %1$s + %1$s %2$s + สงวนลิขสิทธิ์ทุกประการ + + ตั้งค่า + + โมดูลการตรวจสอบด้วยตนเอง + ตรวจสอบว่าระบบได้รับการตั้งค่าอย่างถูกต้องเพื่อใช้งาน microG หรือไม่ + + ได้รับสิทธิ์แล้ว + สิทธิ์ในการเข้าถึง %1$s: + สิทธิ์ในการโต้ตอบกับโปรไฟล์งานของบริษัท: + แตะที่นี้เพื่อทำการให้สิทธิ์ การไม่ให้สิทธิ์อาจจะส่งผลให้เกิดพฤติกรรมไม่เหมาะสม + + สาธิตส่วนต่อประสานกับผู้ใช้ของ microG + สรุป + เวอร์ชัน v0.1.0 + ไลบรารีสนับสนุน + + ไลบรารีสนับสนุนเวอร์ชัน 4 + ไลบรารีสนับสนุน appcompat เวอร์ชัน 7 + ไลบรารีสนับสนุนการตั้งค่า เวอร์ชัน 7 + Apache License 2.0, The Android Open Source Project + \ No newline at end of file From def05a68b4a4bee73847bccf12a47c6482cc47b3 Mon Sep 17 00:00:00 2001 From: DaVinci9196 <150454414+DaVinci9196@users.noreply.github.com> Date: Tue, 16 Dec 2025 22:00:16 +0800 Subject: [PATCH 06/19] Fixed the inability to change profile picture (#3170) --- .../org/microg/gms/accountsettings/ui/bridge/OcUiBridge.kt | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/play-services-core/src/main/kotlin/org/microg/gms/accountsettings/ui/bridge/OcUiBridge.kt b/play-services-core/src/main/kotlin/org/microg/gms/accountsettings/ui/bridge/OcUiBridge.kt index a18429194d..c7169fcb18 100644 --- a/play-services-core/src/main/kotlin/org/microg/gms/accountsettings/ui/bridge/OcUiBridge.kt +++ b/play-services-core/src/main/kotlin/org/microg/gms/accountsettings/ui/bridge/OcUiBridge.kt @@ -18,6 +18,7 @@ import org.microg.gms.accountsettings.ui.EXTRA_SCREEN_ID import org.microg.gms.accountsettings.ui.KEY_UPDATED_PHOTO_URL import org.microg.gms.accountsettings.ui.MainActivity import org.microg.gms.accountsettings.ui.finishActivity +import org.microg.gms.accountsettings.ui.runOnMainLooper class OcUiBridge(val activity: MainActivity, val accountName:String?, val webView: WebView?) { @@ -105,7 +106,7 @@ class OcUiBridge(val activity: MainActivity, val accountName:String?, val webVie @JavascriptInterface fun setBackStop() { Log.d(TAG, "setBackStop: ") - webView?.clearHistory() + runOnMainLooper { webView?.clearHistory() } } @JavascriptInterface From 22b88953c86ef3599c2640aa1bbd571f066e8432 Mon Sep 17 00:00:00 2001 From: SBALAVIGNESH123 Date: Tue, 16 Dec 2025 20:50:47 +0530 Subject: [PATCH 07/19] Fix: Add runtime permission checks for BLUETOOTH_CONNECT Added explicit permission checks before calling Bluetooth APIs that require BLUETOOTH_CONNECT permission on Android 12+: - WearableImpl.java: Check permission before getBondedDevices() - WearableSettingsActivity.java: Check permission before getBondedDevices() - Added SuppressLint for device.getName() since permission already checked Fixes all MissingPermission lint errors. Build verified with lintDebug. --- .../java/org/microg/gms/wearable/WearableImpl.java | 10 ++++++++++ .../microg/gms/wearable/WearableSettingsActivity.java | 10 ++++++++++ 2 files changed, 20 insertions(+) diff --git a/play-services-wearable/core/src/main/java/org/microg/gms/wearable/WearableImpl.java b/play-services-wearable/core/src/main/java/org/microg/gms/wearable/WearableImpl.java index 722a2837aa..45361c43c2 100644 --- a/play-services-wearable/core/src/main/java/org/microg/gms/wearable/WearableImpl.java +++ b/play-services-wearable/core/src/main/java/org/microg/gms/wearable/WearableImpl.java @@ -725,6 +725,16 @@ public void run() { try { BluetoothAdapter adapter = BluetoothAdapter.getDefaultAdapter(); if (adapter != null && adapter.isEnabled()) { + // Check BLUETOOTH_CONNECT permission for Android 12+ + if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.S) { + if (context.checkSelfPermission(android.Manifest.permission.BLUETOOTH_CONNECT) + != android.content.pm.PackageManager.PERMISSION_GRANTED) { + Log.w(TAG, "BLUETOOTH_CONNECT permission not granted, skipping device scan"); + Thread.sleep(10000); + continue; + } + } + Set bondedDevices = adapter.getBondedDevices(); if (bondedDevices != null) { for (BluetoothDevice device : bondedDevices) { diff --git a/play-services-wearable/core/src/main/java/org/microg/gms/wearable/WearableSettingsActivity.java b/play-services-wearable/core/src/main/java/org/microg/gms/wearable/WearableSettingsActivity.java index 6fa6b0d336..7dabc5abe7 100644 --- a/play-services-wearable/core/src/main/java/org/microg/gms/wearable/WearableSettingsActivity.java +++ b/play-services-wearable/core/src/main/java/org/microg/gms/wearable/WearableSettingsActivity.java @@ -46,6 +46,15 @@ private void refreshList() { return; } + // Check BLUETOOTH_CONNECT permission for Android 12+ + if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.S) { + if (checkSelfPermission(android.Manifest.permission.BLUETOOTH_CONNECT) + != android.content.pm.PackageManager.PERMISSION_GRANTED) { + emptyView.setText("Bluetooth permission not granted"); + return; + } + } + Set bondedDevices = adapter.getBondedDevices(); if (bondedDevices != null) { deviceList.addAll(bondedDevices); @@ -133,6 +142,7 @@ public WearableDeviceAdapter(android.content.Context context, ArrayList Date: Mon, 29 Dec 2025 15:23:07 +0100 Subject: [PATCH 08/19] Wifi: Handle invalid WifiInfo objects without bssid Also fix some warnings in NetworkLocationService Incorporates work from #3210 --- .../network/NetworkLocationService.kt | 14 +++++++------ .../gms/location/network/wifi/extensions.kt | 20 ++++++++++--------- 2 files changed, 19 insertions(+), 15 deletions(-) diff --git a/play-services-location/core/provider/src/main/kotlin/org/microg/gms/location/network/NetworkLocationService.kt b/play-services-location/core/provider/src/main/kotlin/org/microg/gms/location/network/NetworkLocationService.kt index 8a1036943c..928051ad77 100644 --- a/play-services-location/core/provider/src/main/kotlin/org/microg/gms/location/network/NetworkLocationService.kt +++ b/play-services-location/core/provider/src/main/kotlin/org/microg/gms/location/network/NetworkLocationService.kt @@ -16,6 +16,7 @@ import android.os.* import android.os.Build.VERSION.SDK_INT import android.util.Log import androidx.annotation.GuardedBy +import androidx.core.content.IntentCompat import androidx.core.content.getSystemService import androidx.core.location.LocationListenerCompat import androidx.core.location.LocationManagerCompat @@ -122,6 +123,7 @@ class NetworkLocationService : LifecycleService(), WifiDetailsCallback, CellDeta } } + @Suppress("DEPRECATION") @SuppressLint("WrongConstant") private fun scan(lowPower: Boolean) { if (!lowPower) lastHighPowerScanRealtime = SystemClock.elapsedRealtime() @@ -147,7 +149,7 @@ class NetworkLocationService : LifecycleService(), WifiDetailsCallback, CellDeta // No need to scan if only moving wifi enabled, instead simulate scan based on current connection info val connectionInfo = getSystemService()?.connectionInfo if (SDK_INT >= 31 && connectionInfo != null) { - onWifiDetailsAvailable(listOf(connectionInfo.toWifiDetails())) + onWifiDetailsAvailable(listOfNotNull(connectionInfo.toWifiDetails())) } else if (currentLocalMovingWifi != null && connectionInfo?.bssid == currentLocalMovingWifi.macAddress) { onWifiDetailsAvailable(listOf(currentLocalMovingWifi.copy(timestamp = System.currentTimeMillis()))) } else { @@ -205,7 +207,7 @@ class NetworkLocationService : LifecycleService(), WifiDetailsCallback, CellDeta override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int { if (intent?.action == ACTION_NETWORK_LOCATION_SERVICE) { handler.post { - val pendingIntent = intent.getParcelableExtra(EXTRA_PENDING_INTENT) ?: return@post + val pendingIntent = IntentCompat.getParcelableExtra(intent, EXTRA_PENDING_INTENT, PendingIntent::class.java) ?: return@post val enable = intent.getBooleanExtra(EXTRA_ENABLE, false) if (enable) { val intervalMillis = intent.getLongExtra(EXTRA_INTERVAL_MILLIS, -1L) @@ -213,7 +215,7 @@ class NetworkLocationService : LifecycleService(), WifiDetailsCallback, CellDeta var forceNow = intent.getBooleanExtra(EXTRA_FORCE_NOW, false) val lowPower = intent.getBooleanExtra(EXTRA_LOW_POWER, true) val bypass = intent.getBooleanExtra(EXTRA_BYPASS, false) - val workSource = intent.getParcelableExtra(EXTRA_WORK_SOURCE) ?: WorkSource() + val workSource = IntentCompat.getParcelableExtra(intent, EXTRA_WORK_SOURCE, WorkSource::class.java) ?: WorkSource() synchronized(activeRequests) { if (activeRequests.any { it.pendingIntent == pendingIntent }) { forceNow = false @@ -231,7 +233,7 @@ class NetworkLocationService : LifecycleService(), WifiDetailsCallback, CellDeta } } else if (intent?.action == ACTION_NETWORK_IMPORT_EXPORT) { handler.post { - val callback = intent.getParcelableExtra(EXTRA_MESSENGER) + val callback = IntentCompat.getParcelableExtra(intent, EXTRA_MESSENGER, Messenger::class.java) val replyWhat = intent.getIntExtra(EXTRA_REPLY_WHAT, 0) when (intent.getStringExtra(EXTRA_DIRECTION)) { DIRECTION_EXPORT -> { @@ -247,7 +249,7 @@ class NetworkLocationService : LifecycleService(), WifiDetailsCallback, CellDeta }) } DIRECTION_IMPORT -> { - val uri = intent.getParcelableExtra(EXTRA_URI) + val uri = IntentCompat.getParcelableExtra(intent, EXTRA_URI, Uri::class.java) val counter = uri?.let { database.importLearned(it) } ?: 0 callback?.send(Message.obtain().apply { what = replyWhat @@ -266,7 +268,7 @@ class NetworkLocationService : LifecycleService(), WifiDetailsCallback, CellDeta } override fun onDestroy() { - handlerThread.stop() + handlerThread.quitSafely() wifiDetailsSource?.disable() wifiDetailsSource = null cellDetailsSource?.disable() diff --git a/play-services-location/core/provider/src/main/kotlin/org/microg/gms/location/network/wifi/extensions.kt b/play-services-location/core/provider/src/main/kotlin/org/microg/gms/location/network/wifi/extensions.kt index f991762bf0..d0be1c7376 100644 --- a/play-services-location/core/provider/src/main/kotlin/org/microg/gms/location/network/wifi/extensions.kt +++ b/play-services-location/core/provider/src/main/kotlin/org/microg/gms/location/network/wifi/extensions.kt @@ -23,15 +23,17 @@ internal fun ScanResult.toWifiDetails(): WifiDetails = WifiDetails( ) @RequiresApi(31) -internal fun WifiInfo.toWifiDetails(): WifiDetails = WifiDetails( - macAddress = bssid, - ssid = ssid.takeIf { it != WifiManager.UNKNOWN_SSID && it.startsWith("\"") && it.endsWith("\"") } - ?.let { it.substring(1, it.length - 1) }, - timestamp = System.currentTimeMillis(), - frequency = frequency, - signalStrength = rssi, - open = currentSecurityType == WifiInfo.SECURITY_TYPE_OPEN -) +internal fun WifiInfo.toWifiDetails(): WifiDetails? { + return WifiDetails( + macAddress = bssid ?: return null, + ssid = ssid?.removeSurrounding("\"") + ?.takeIf { it != WifiManager.UNKNOWN_SSID }, + timestamp = System.currentTimeMillis(), + frequency = frequency, + signalStrength = rssi, + open = currentSecurityType == WifiInfo.SECURITY_TYPE_OPEN + ) +} private const val BAND_24_GHZ_FIRST_CH_NUM = 1 private const val BAND_24_GHZ_LAST_CH_NUM = 14 From 52deaee8c5da095f8214d5c046cab1a9d8fb3356 Mon Sep 17 00:00:00 2001 From: althafvly Date: Tue, 30 Dec 2025 19:30:34 +0530 Subject: [PATCH 09/19] location: allow IP fallback for Wi-Fi-only geolocation --- .../gms/location/network/ichnaea/IchnaeaServiceClient.kt | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/play-services-location/core/provider/src/main/kotlin/org/microg/gms/location/network/ichnaea/IchnaeaServiceClient.kt b/play-services-location/core/provider/src/main/kotlin/org/microg/gms/location/network/ichnaea/IchnaeaServiceClient.kt index a4471a29d3..2ecc435803 100644 --- a/play-services-location/core/provider/src/main/kotlin/org/microg/gms/location/network/ichnaea/IchnaeaServiceClient.kt +++ b/play-services-location/core/provider/src/main/kotlin/org/microg/gms/location/network/ichnaea/IchnaeaServiceClient.kt @@ -6,6 +6,7 @@ package org.microg.gms.location.network.ichnaea import android.content.Context +import android.content.pm.PackageManager import android.location.Location import android.net.Uri import android.os.Bundle @@ -41,6 +42,10 @@ class IchnaeaServiceClient(private val context: Context) { private val cache = LruCache(REQUEST_CACHE_SIZE) private val start = SystemClock.elapsedRealtime() + private val hasTelephony by lazy { + context.packageManager.hasSystemFeature(PackageManager.FEATURE_TELEPHONY) + } + private fun GeolocateRequest.hash(): ByteArray? { if (cellTowers.isNullOrEmpty() && (wifiAccessPoints?.size ?: 0) < 3 || bluetoothBeacons?.isNotEmpty() == true) return null val minAge = min( @@ -74,7 +79,7 @@ class IchnaeaServiceClient(private val context: Context) { suspend fun retrieveMultiWifiLocation(wifis: List, rawHandler: ((WifiDetails, Location) -> Unit)? = null): Location? = geoLocate( GeolocateRequest( - considerIp = false, + considerIp = !hasTelephony, wifiAccessPoints = wifis.filter { isRequestable(it) }.map(WifiDetails::toWifiAccessPoint), fallbacks = Fallback(lacf = false, ipf = false) ), From 2aae6afff2400e65ed053dee6578b598eb2710ea Mon Sep 17 00:00:00 2001 From: Marvin W Date: Tue, 6 Jan 2026 12:03:56 +0100 Subject: [PATCH 10/19] Location: Don't trigger moving wifi shortcut if connection info is broken --- .../org/microg/gms/location/network/NetworkLocationService.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/play-services-location/core/provider/src/main/kotlin/org/microg/gms/location/network/NetworkLocationService.kt b/play-services-location/core/provider/src/main/kotlin/org/microg/gms/location/network/NetworkLocationService.kt index 928051ad77..2292a84966 100644 --- a/play-services-location/core/provider/src/main/kotlin/org/microg/gms/location/network/NetworkLocationService.kt +++ b/play-services-location/core/provider/src/main/kotlin/org/microg/gms/location/network/NetworkLocationService.kt @@ -148,7 +148,7 @@ class NetworkLocationService : LifecycleService(), WifiDetailsCallback, CellDeta } else if (settings.wifiMoving) { // No need to scan if only moving wifi enabled, instead simulate scan based on current connection info val connectionInfo = getSystemService()?.connectionInfo - if (SDK_INT >= 31 && connectionInfo != null) { + if (SDK_INT >= 31 && connectionInfo != null && connectionInfo.toWifiDetails() != null) { onWifiDetailsAvailable(listOfNotNull(connectionInfo.toWifiDetails())) } else if (currentLocalMovingWifi != null && connectionInfo?.bssid == currentLocalMovingWifi.macAddress) { onWifiDetailsAvailable(listOf(currentLocalMovingWifi.copy(timestamp = System.currentTimeMillis()))) From 95d5fc19fb4dbd5b47252b89c37d7f926ae250d0 Mon Sep 17 00:00:00 2001 From: DaVinci9196 <150454414+DaVinci9196@users.noreply.github.com> Date: Thu, 8 Jan 2026 03:01:08 +0800 Subject: [PATCH 11/19] Vending: Added PI access management (#3108) Co-authored-by: Marvin W --- .../microg/gms/settings/SettingsContract.kt | 2 + .../microg/gms/settings/SettingsProvider.kt | 2 + .../microg/gms/vending/PlayIntegrityData.kt | 71 ++++++++++ .../microg/gms/ui/SafetyNetAllAppsFragment.kt | 6 +- .../org/microg/gms/ui/SafetyNetAppFragment.kt | 47 ++++++- .../org/microg/gms/ui/SafetyNetFragment.kt | 8 +- .../microg/gms/vending/VendingPreferences.kt | 15 ++ .../src/main/res/navigation/nav_settings.xml | 8 +- .../src/main/res/values-zh-rCN/strings.xml | 4 + .../src/main/res/values-zh-rTW/strings.xml | 4 + .../src/main/res/values/strings.xml | 4 + .../res/xml/preferences_safetynet_app.xml | 8 ++ .../src/main/res/xml/preferences_start.xml | 2 +- .../com/android/vending/VendingPreferences.kt | 22 +++ .../android/finsky/IntegrityExtensions.kt | 60 ++++++-- .../ExpressIntegrityService.kt | 48 ++++++- .../integrityservice/IntegrityService.kt | 133 ++++++++++-------- 17 files changed, 356 insertions(+), 88 deletions(-) create mode 100644 play-services-base/core/src/main/kotlin/org/microg/gms/vending/PlayIntegrityData.kt diff --git a/play-services-base/core/src/main/kotlin/org/microg/gms/settings/SettingsContract.kt b/play-services-base/core/src/main/kotlin/org/microg/gms/settings/SettingsContract.kt index 8a9ff68474..5ef726412c 100644 --- a/play-services-base/core/src/main/kotlin/org/microg/gms/settings/SettingsContract.kt +++ b/play-services-base/core/src/main/kotlin/org/microg/gms/settings/SettingsContract.kt @@ -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, @@ -289,6 +290,7 @@ object SettingsContract { ASSET_DEVICE_SYNC, APPS_INSTALL, APPS_INSTALLER_LIST, + PLAY_INTEGRITY_APP_LIST ) } diff --git a/play-services-base/core/src/main/kotlin/org/microg/gms/settings/SettingsProvider.kt b/play-services-base/core/src/main/kotlin/org/microg/gms/settings/SettingsProvider.kt index fd05f7b639..df0cabfd41 100644 --- a/play-services-base/core/src/main/kotlin/org/microg/gms/settings/SettingsProvider.kt +++ b/play-services-base/core/src/main/kotlin/org/microg/gms/settings/SettingsProvider.kt @@ -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") } } @@ -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") } } diff --git a/play-services-base/core/src/main/kotlin/org/microg/gms/vending/PlayIntegrityData.kt b/play-services-base/core/src/main/kotlin/org/microg/gms/vending/PlayIntegrityData.kt new file mode 100644 index 0000000000..bb5fde337c --- /dev/null +++ b/play-services-base/core/src/main/kotlin/org/microg/gms/vending/PlayIntegrityData.kt @@ -0,0 +1,71 @@ +/** + * 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 { + return content.split("|").mapNotNull { parse(it) }.toSet() + } + + fun updateDataSetString(channelList: Set, 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 diff --git a/play-services-core/src/main/kotlin/org/microg/gms/ui/SafetyNetAllAppsFragment.kt b/play-services-core/src/main/kotlin/org/microg/gms/ui/SafetyNetAllAppsFragment.kt index 78a489d040..cf06c39289 100644 --- a/play-services-core/src/main/kotlin/org/microg/gms/ui/SafetyNetAllAppsFragment.kt +++ b/play-services-core/src/main/kotlin/org/microg/gms/ui/SafetyNetAllAppsFragment.kt @@ -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 @@ -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 { diff --git a/play-services-core/src/main/kotlin/org/microg/gms/ui/SafetyNetAppFragment.kt b/play-services-core/src/main/kotlin/org/microg/gms/ui/SafetyNetAppFragment.kt index 6e3cb295c1..f3c1a1c1d1 100644 --- a/play-services-core/src/main/kotlin/org/microg/gms/ui/SafetyNetAppFragment.kt +++ b/play-services-core/src/main/kotlin/org/microg/gms/ui/SafetyNetAppFragment.kt @@ -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") @@ -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() { @@ -37,6 +57,11 @@ class SafetyNetAppFragment : PreferenceFragmentCompat() { updateContent() } + private fun loadPlayIntegrityData(): Set { + val playIntegrityData = VendingPreferences.getPlayIntegrityAppList(requireContext()) + return PlayIntegrityData.loadDataSet(playIntegrityData) + } + fun updateContent() { lifecycleScope.launchWhenResumed { appHeadingPreference.packageName = packageName @@ -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 { @@ -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 } } diff --git a/play-services-core/src/main/kotlin/org/microg/gms/ui/SafetyNetFragment.kt b/play-services-core/src/main/kotlin/org/microg/gms/ui/SafetyNetFragment.kt index e2a0090ddc..22e6e1e010 100644 --- a/play-services-core/src/main/kotlin/org/microg/gms/ui/SafetyNetFragment.kt +++ b/play-services-core/src/main/kotlin/org/microg/gms/ui/SafetyNetFragment.kt @@ -5,7 +5,6 @@ package org.microg.gms.ui -import android.annotation.SuppressLint import android.os.Bundle import android.util.Base64 import android.util.Log @@ -38,6 +37,8 @@ import org.microg.gms.safetynet.SafetyNetDatabase import org.microg.gms.safetynet.SafetyNetPreferences import org.microg.gms.safetynet.SafetyNetRequestType.* import org.microg.gms.utils.singleInstanceOf +import org.microg.gms.vending.PlayIntegrityData +import org.microg.gms.vending.VendingPreferences import java.net.URLEncoder import kotlin.coroutines.resume import kotlin.coroutines.resumeWithException @@ -231,13 +232,14 @@ class SafetyNetFragment : PreferenceFragmentCompat() { lifecycleScope.launchWhenResumed { val context = requireContext() val (apps, showAll) = withContext(Dispatchers.IO) { + val playIntegrityData = VendingPreferences.getPlayIntegrityAppList(context) val db = SafetyNetDatabase(context) val apps = try { - db.recentApps + db.recentApps + PlayIntegrityData.loadDataSet(playIntegrityData).map { it.packageName to it.lastTime } } finally { db.close() } - apps.map { app -> + apps.sortedByDescending { it.second }.map { app -> app to context.packageManager.getApplicationInfoIfExists(app.first) }.mapNotNull { (app, info) -> if (info == null) null else app to info diff --git a/play-services-core/src/main/kotlin/org/microg/gms/vending/VendingPreferences.kt b/play-services-core/src/main/kotlin/org/microg/gms/vending/VendingPreferences.kt index 4f95a41b18..59de550ea8 100644 --- a/play-services-core/src/main/kotlin/org/microg/gms/vending/VendingPreferences.kt +++ b/play-services-core/src/main/kotlin/org/microg/gms/vending/VendingPreferences.kt @@ -129,4 +129,19 @@ object VendingPreferences { put(SettingsContract.Vending.APPS_INSTALLER_LIST, content) } } + + @JvmStatic + fun getPlayIntegrityAppList(context: Context): String { + val projection = arrayOf(SettingsContract.Vending.PLAY_INTEGRITY_APP_LIST) + return SettingsContract.getSettings(context, SettingsContract.Vending.getContentUri(context), projection) { c -> + c.getString(0) + } + } + + @JvmStatic + fun setPlayIntegrityAppList(context: Context, content: String) { + SettingsContract.setSettings(context, SettingsContract.Vending.getContentUri(context)) { + put(SettingsContract.Vending.PLAY_INTEGRITY_APP_LIST, content) + } + } } \ No newline at end of file diff --git a/play-services-core/src/main/res/navigation/nav_settings.xml b/play-services-core/src/main/res/navigation/nav_settings.xml index 4e202752d1..7aacdbf48f 100644 --- a/play-services-core/src/main/res/navigation/nav_settings.xml +++ b/play-services-core/src/main/res/navigation/nav_settings.xml @@ -125,7 +125,7 @@ + android:label="@string/prefcat_device_attestation_apps_title"> @@ -156,7 +156,7 @@ 自定义:%s 自动:%s 系统:%s + 允许请求 + 允许应用程序请求设备身份验证 "测试 SafetyNet 认证" "Google SafetyNet 是一套设备认证系统,旨在确认设备具有适当安全性,并与 Android CTS 兼容。某些应用会出于安全考虑或是防篡改目的而使用 SafetyNet。 @@ -154,6 +156,7 @@ microG GmsCore 内置一套自由的 SafetyNet 实现,但是官方服务器要 选择配置信息 设备配置信息 使用 SafetyNet 的应用 + 使用设备认证的应用 清除近期的 SafetyNet 请求 最近使用于%1$s 评估类型 @@ -226,6 +229,7 @@ microG GmsCore 内置一套自由的 SafetyNet 实现,但是官方服务器要 添加和管理 Google 账号 读取Google服务配置 Google SafetyNet + 设备认证 ReCaptcha: %s ReCaptcha Enterprise: %s Google 游戏账号 diff --git a/play-services-core/src/main/res/values-zh-rTW/strings.xml b/play-services-core/src/main/res/values-zh-rTW/strings.xml index 2d1b451be5..42209aa75a 100644 --- a/play-services-core/src/main/res/values-zh-rTW/strings.xml +++ b/play-services-core/src/main/res/values-zh-rTW/strings.xml @@ -160,6 +160,7 @@ 執行中… 運作模式 使用 SafetyNet 的應用程式 + 使用設備認證的應用程式 清除最近的請求 原生 實機 @@ -196,6 +197,9 @@ 車用廠商通訊通道 存取您車輛的車廠專屬通道,以交換與車輛相關的專屬資訊 Google SafetyNet + 設備認證 + 允許請求 + 允許應用程式請求裝置身份驗證 啟用此功能後,驗證請求中將不包含裝置名稱,這可能允許未授權的裝置登入,但也可能導致不可預期的後果。 狀態 更多 diff --git a/play-services-core/src/main/res/values/strings.xml b/play-services-core/src/main/res/values/strings.xml index e1d7ecf16a..c71f247fd3 100644 --- a/play-services-core/src/main/res/values/strings.xml +++ b/play-services-core/src/main/res/values/strings.xml @@ -96,6 +96,7 @@ Please set up a password, PIN, or pattern lock screen." Google device registration Cloud Messaging Google SafetyNet + Device Attestation Play Store services Work profile @@ -239,6 +240,9 @@ Please set up a password, PIN, or pattern lock screen." Operation mode DroidGuard execution is unsupported on this device. SafetyNet services may misbehave. Apps using SafetyNet + Apps using Device Attestation + Allow requests + Allow the app to request device attestation Clear recent requests Last use: %1$s diff --git a/play-services-core/src/main/res/xml/preferences_safetynet_app.xml b/play-services-core/src/main/res/xml/preferences_safetynet_app.xml index c799e545f9..131e2e4335 100644 --- a/play-services-core/src/main/res/xml/preferences_safetynet_app.xml +++ b/play-services-core/src/main/res/xml/preferences_safetynet_app.xml @@ -13,6 +13,14 @@ tools:title="@tools:sample/lorem" app:allowDividerBelow="false" /> + + + android:title="@string/service_name_device_attestation" /> + c.getInt(0) != 0 + } + } + + @JvmStatic + fun getPlayIntegrityAppList(context: Context): String { + val projection = arrayOf(SettingsContract.Vending.PLAY_INTEGRITY_APP_LIST) + return SettingsContract.getSettings(context, SettingsContract.Vending.getContentUri(context), projection) { c -> + c.getString(0) + } + } + + @JvmStatic + fun setPlayIntegrityAppList(context: Context, content: String) { + SettingsContract.setSettings(context, SettingsContract.Vending.getContentUri(context)) { + put(SettingsContract.Vending.PLAY_INTEGRITY_APP_LIST, content) + } + } } \ No newline at end of file diff --git a/vending-app/src/main/kotlin/com/google/android/finsky/IntegrityExtensions.kt b/vending-app/src/main/kotlin/com/google/android/finsky/IntegrityExtensions.kt index 2610e00697..8c6d0fab50 100644 --- a/vending-app/src/main/kotlin/com/google/android/finsky/IntegrityExtensions.kt +++ b/vending-app/src/main/kotlin/com/google/android/finsky/IntegrityExtensions.kt @@ -19,11 +19,15 @@ import android.security.keystore.KeyProperties import android.text.TextUtils import android.util.Base64 import android.util.Log +import androidx.core.content.edit +import com.android.vending.VendingPreferences import com.android.vending.buildRequestHeaders import com.android.vending.makeTimestamp import com.google.android.finsky.expressintegrityservice.ExpressIntegritySession import com.google.android.finsky.expressintegrityservice.IntermediateIntegrityResponseData import com.google.android.finsky.expressintegrityservice.PackageInformation +import com.google.android.finsky.model.IntegrityErrorCode +import com.google.android.finsky.model.StandardIntegrityException import com.google.android.gms.droidguard.DroidGuard import com.google.android.gms.droidguard.internal.DroidGuardResultsRequest import com.google.android.gms.tasks.await @@ -36,11 +40,14 @@ import com.google.crypto.tink.aead.AesGcmKeyManager import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext import okio.ByteString +import okio.ByteString.Companion.decodeBase64 import okio.ByteString.Companion.encode import okio.ByteString.Companion.toByteString import org.microg.gms.common.Constants import org.microg.gms.profile.Build import org.microg.gms.profile.ProfileManager +import org.microg.gms.utils.getFirstSignatureDigest +import org.microg.gms.vending.PlayIntegrityData import org.microg.vending.billing.DEFAULT_ACCOUNT_TYPE import org.microg.vending.billing.GServices import org.microg.vending.billing.core.HttpClient @@ -97,6 +104,32 @@ private const val DEVICE_INTEGRITY_HARD_EXPIRATION = 432000L // 5 day const val INTERMEDIATE_INTEGRITY_HARD_EXPIRATION = 86400L // 1 day private const val TAG = "IntegrityExtensions" +fun callerAppToIntegrityData(context: Context, callingPackage: String): PlayIntegrityData { + val pkgSignSha256ByteArray = context.packageManager.getFirstSignatureDigest(callingPackage, "SHA-256") + if (pkgSignSha256ByteArray == null) { + throw StandardIntegrityException(IntegrityErrorCode.APP_NOT_INSTALLED, "$callingPackage signature is null") + } + val pkgSignSha256 = Base64.encodeToString(pkgSignSha256ByteArray, Base64.NO_WRAP) + Log.d(TAG, "callerToVisitData $callingPackage pkgSignSha256: $pkgSignSha256") + val playIntegrityAppList = VendingPreferences.getPlayIntegrityAppList(context) + val loadDataSet = PlayIntegrityData.loadDataSet(playIntegrityAppList) + if (loadDataSet.isEmpty() || loadDataSet.none { it.packageName == callingPackage && it.pkgSignSha256 == pkgSignSha256 }) { + return PlayIntegrityData(true, callingPackage, pkgSignSha256, System.currentTimeMillis()) + } + return loadDataSet.first { it.packageName == callingPackage && it.pkgSignSha256 == pkgSignSha256 } +} + +fun PlayIntegrityData.updateAppIntegrityContent(context: Context, time: Long, result: String, status: Boolean = false) { + val playIntegrityAppList = VendingPreferences.getPlayIntegrityAppList(context) + val loadDataSet = PlayIntegrityData.loadDataSet(playIntegrityAppList) + val dataSetString = PlayIntegrityData.updateDataSetString(loadDataSet, apply { + lastTime = time + lastResult = result + lastStatus = status + }) + VendingPreferences.setPlayIntegrityAppList(context, dataSetString) +} + fun IntegrityRequestWrapper.getExpirationTime() = runCatching { val creationTimeStamp = deviceIntegrityWrapper?.creationTime ?: Timestamp(0, 0) val creationTime = (creationTimeStamp.seconds ?: 0) * 1000 + (creationTimeStamp.nanos ?: 0) / 1_000_000 @@ -395,18 +428,27 @@ suspend fun updateExpressAuthTokenWrapper(context: Context, expressIntegritySess private suspend fun regenerateToken( context: Context, authToken: String, packageName: String, clientKey: ClientKey ): AuthTokenWrapper { + Log.d(TAG, "regenerateToken authToken:$authToken, packageName:$packageName, clientKey:$clientKey") try { - Log.d(TAG, "regenerateToken authToken:$authToken, packageName:$packageName, clientKey:$clientKey") - val droidGuardSessionTokenResponse = requestDroidGuardSessionToken(context, authToken) - - if (droidGuardSessionTokenResponse.tokenWrapper == null) { - throw RuntimeException("regenerateToken droidGuardSessionTokenResponse.tokenWrapper is Empty!") + val prefs = context.getSharedPreferences("droid_guard_token_session_id", Context.MODE_PRIVATE) + val droidGuardTokenSession = try { + val droidGuardSessionTokenResponse = requestDroidGuardSessionToken(context, authToken) + if (droidGuardSessionTokenResponse.tokenWrapper == null) { + throw RuntimeException("regenerateToken droidGuardSessionTokenResponse.tokenWrapper is Empty!") + } + val droidGuardTokenType = droidGuardSessionTokenResponse.tokenWrapper.tokenContent?.tokenType?.firstOrNull { it.type?.toInt() == 5 } + ?: throw RuntimeException("regenerateToken droidGuardTokenType is null!") + val sessionId = droidGuardTokenType.tokenSessionWrapper?.wrapper?.sessionContent?.session?.id + if (sessionId.isNullOrEmpty()) { + throw RuntimeException("regenerateToken sessionId is null") + } + sessionId.also { prefs.edit { putString(packageName, it) } } + } catch (e: Exception) { + Log.d(TAG, "regenerateToken: error ", e) + prefs.getString(packageName, null) } - val droidGuardTokenType = droidGuardSessionTokenResponse.tokenWrapper.tokenContent?.tokenType?.firstOrNull { it.type?.toInt() == 5 } - ?: throw RuntimeException("regenerateToken droidGuardTokenType is null!") - - val droidGuardTokenSession = droidGuardTokenType.tokenSessionWrapper?.wrapper?.sessionContent?.session?.id + Log.d(TAG, "regenerateToken: sessionId: $droidGuardTokenSession") if (droidGuardTokenSession.isNullOrEmpty()) { throw RuntimeException("regenerateToken droidGuardTokenSession is null") } diff --git a/vending-app/src/main/kotlin/com/google/android/finsky/expressintegrityservice/ExpressIntegrityService.kt b/vending-app/src/main/kotlin/com/google/android/finsky/expressintegrityservice/ExpressIntegrityService.kt index 524e389f5e..276a1ebdc9 100644 --- a/vending-app/src/main/kotlin/com/google/android/finsky/expressintegrityservice/ExpressIntegrityService.kt +++ b/vending-app/src/main/kotlin/com/google/android/finsky/expressintegrityservice/ExpressIntegrityService.kt @@ -20,23 +20,22 @@ import androidx.lifecycle.LifecycleOwner import androidx.lifecycle.LifecycleService import androidx.lifecycle.lifecycleScope import com.android.vending.AUTH_TOKEN_SCOPE +import com.android.vending.VendingPreferences import com.android.vending.makeTimestamp import com.google.android.finsky.AuthTokenWrapper import com.google.android.finsky.ClientKey -import com.google.android.finsky.ClientKeyExtend import com.google.android.finsky.DeviceIntegrityWrapper import com.google.android.finsky.ExpressIntegrityResponse -import com.google.android.finsky.IntegrityAdvice import com.google.android.finsky.INTERMEDIATE_INTEGRITY_HARD_EXPIRATION +import com.google.android.finsky.IntegrityAdvice import com.google.android.finsky.IntermediateIntegrityRequest import com.google.android.finsky.IntermediateIntegrityResponse import com.google.android.finsky.IntermediateIntegritySession import com.google.android.finsky.KEY_CLOUD_PROJECT +import com.google.android.finsky.KEY_ERROR import com.google.android.finsky.KEY_NONCE -import com.google.android.finsky.KEY_OPT_PACKAGE import com.google.android.finsky.KEY_PACKAGE_NAME import com.google.android.finsky.KEY_REQUEST_MODE -import com.google.android.finsky.KEY_ERROR import com.google.android.finsky.KEY_REQUEST_TOKEN_SID import com.google.android.finsky.KEY_REQUEST_VERDICT_OPT_OUT import com.google.android.finsky.KEY_TOKEN @@ -48,13 +47,14 @@ import com.google.android.finsky.RequestMode import com.google.android.finsky.TestErrorType import com.google.android.finsky.buildClientKeyExtend import com.google.android.finsky.buildInstallSourceMetaData -import com.google.android.finsky.getPlayCoreVersion +import com.google.android.finsky.callerAppToIntegrityData import com.google.android.finsky.encodeBase64 import com.google.android.finsky.ensureContainsLockBootloader import com.google.android.finsky.getAuthToken import com.google.android.finsky.getExpirationTime import com.google.android.finsky.getIntegrityRequestWrapper import com.google.android.finsky.getPackageInfoCompat +import com.google.android.finsky.getPlayCoreVersion import com.google.android.finsky.isNetworkConnected import com.google.android.finsky.md5 import com.google.android.finsky.model.IntegrityErrorCode @@ -63,6 +63,7 @@ import com.google.android.finsky.readAes128GcmBuilderFromClientKey import com.google.android.finsky.requestIntermediateIntegrity import com.google.android.finsky.sha256 import com.google.android.finsky.signaturesCompat +import com.google.android.finsky.updateAppIntegrityContent import com.google.android.finsky.updateExpressAuthTokenWrapper import com.google.android.finsky.updateExpressClientKey import com.google.android.finsky.updateExpressSessionTime @@ -74,6 +75,7 @@ import com.google.android.play.core.integrity.protocol.IRequestDialogCallback import com.google.crypto.tink.config.TinkConfig import okio.ByteString.Companion.toByteString import org.microg.gms.profile.ProfileManager +import org.microg.gms.vending.PlayIntegrityData import org.microg.vending.billing.DEFAULT_ACCOUNT_TYPE import org.microg.vending.proto.Timestamp import kotlin.random.Random @@ -98,15 +100,30 @@ class ExpressIntegrityService : LifecycleService() { private class ExpressIntegrityServiceImpl(private val context: Context, override val lifecycle: Lifecycle) : IExpressIntegrityService.Stub(), LifecycleOwner { + private var visitData: PlayIntegrityData? = null + override fun warmUpIntegrityToken(bundle: Bundle, callback: IExpressIntegrityServiceCallback?) { lifecycleScope.launchWhenCreated { runCatching { + val callingPackageName = bundle.getString(KEY_PACKAGE_NAME) + if (callingPackageName == null) { + throw StandardIntegrityException(IntegrityErrorCode.INTERNAL_ERROR, "Null packageName.") + } + visitData = callerAppToIntegrityData(context, callingPackageName) + if (visitData?.allowed != true) { + throw StandardIntegrityException(IntegrityErrorCode.API_NOT_AVAILABLE, "Not allowed visit") + } + val playIntegrityEnabled = VendingPreferences.isDeviceAttestationEnabled(context) + if (!playIntegrityEnabled) { + throw StandardIntegrityException(IntegrityErrorCode.API_NOT_AVAILABLE, "API is disabled") + } + if (!context.isNetworkConnected()) { throw StandardIntegrityException(IntegrityErrorCode.NETWORK_ERROR, "No network is available") } val expressIntegritySession = ExpressIntegritySession( - packageName = bundle.getString(KEY_PACKAGE_NAME) ?: "", + packageName = callingPackageName ?: "", cloudProjectNumber = bundle.getLong(KEY_CLOUD_PROJECT, 0L), sessionId = Random.nextLong(), null, @@ -234,10 +251,12 @@ private class ExpressIntegrityServiceImpl(private val context: Context, override updateLocalExpressFilePB(context, intermediateIntegrityResponseData) + visitData?.updateAppIntegrityContent(context, System.currentTimeMillis(), "$TAG visited success.", true) callback?.onWarmResult(bundleOf(KEY_WARM_UP_SID to expressIntegritySession.sessionId)) }.onFailure { val exception = it as? StandardIntegrityException ?: StandardIntegrityException(it.message) Log.w(TAG, "warm up has failed: code=${exception.code}, message=${exception.message}", exception) + visitData?.updateAppIntegrityContent(context, System.currentTimeMillis(), "$TAG visited failed. ${exception.message}") callback?.onWarmResult(bundleOf(KEY_ERROR to exception.code)) } } @@ -247,8 +266,21 @@ private class ExpressIntegrityServiceImpl(private val context: Context, override Log.d(TAG, "requestExpressIntegrityToken bundle:$bundle") lifecycleScope.launchWhenCreated { runCatching { + val callingPackageName = bundle.getString(KEY_PACKAGE_NAME) + if (callingPackageName == null) { + throw StandardIntegrityException(IntegrityErrorCode.INTERNAL_ERROR, "Null packageName.") + } + visitData = callerAppToIntegrityData(context, callingPackageName) + if (visitData?.allowed != true) { + throw StandardIntegrityException(IntegrityErrorCode.API_NOT_AVAILABLE, "Not allowed visit") + } + val playIntegrityEnabled = VendingPreferences.isDeviceAttestationEnabled(context) + if (!playIntegrityEnabled) { + throw StandardIntegrityException(IntegrityErrorCode.API_NOT_AVAILABLE, "API is disabled") + } + val expressIntegritySession = ExpressIntegritySession( - packageName = bundle.getString(KEY_PACKAGE_NAME) ?: "", + packageName = callingPackageName, cloudProjectNumber = bundle.getLong(KEY_CLOUD_PROJECT, 0L), sessionId = Random.nextLong(), requestHash = bundle.getString(KEY_NONCE), @@ -321,6 +353,7 @@ private class ExpressIntegrityServiceImpl(private val context: Context, override ) Log.d(TAG, "requestExpressIntegrityToken token: $token, sid: ${expressIntegritySession.sessionId}, mode: ${expressIntegritySession.webViewRequestMode}") + visitData?.updateAppIntegrityContent(context, System.currentTimeMillis(), "$TAG visited success.", true) callback?.onRequestResult( bundleOf( KEY_TOKEN to token, @@ -331,6 +364,7 @@ private class ExpressIntegrityServiceImpl(private val context: Context, override }.onFailure { val exception = it as? StandardIntegrityException ?: StandardIntegrityException(it.message) Log.w(TAG, "requesting token has failed: code=${exception.code}, message=${exception.message}", exception) + visitData?.updateAppIntegrityContent(context, System.currentTimeMillis(), "$TAG visited failed. ${exception.message}") callback?.onRequestResult(bundleOf(KEY_ERROR to exception.code)) } } diff --git a/vending-app/src/main/kotlin/com/google/android/finsky/integrityservice/IntegrityService.kt b/vending-app/src/main/kotlin/com/google/android/finsky/integrityservice/IntegrityService.kt index 76478e7ade..d24283b978 100644 --- a/vending-app/src/main/kotlin/com/google/android/finsky/integrityservice/IntegrityService.kt +++ b/vending-app/src/main/kotlin/com/google/android/finsky/integrityservice/IntegrityService.kt @@ -19,6 +19,7 @@ import androidx.lifecycle.LifecycleOwner import androidx.lifecycle.LifecycleService import androidx.lifecycle.lifecycleScope import com.android.vending.AUTH_TOKEN_SCOPE +import com.android.vending.VendingPreferences import com.android.vending.makeTimestamp import com.google.android.finsky.AccessibilityAbuseSignalDataWrapper import com.google.android.finsky.AppAccessRiskDetailsResponse @@ -44,14 +45,17 @@ import com.google.android.finsky.SIGNING_FLAGS import com.google.android.finsky.ScreenCaptureSignalDataWrapper import com.google.android.finsky.ScreenOverlaySignalDataWrapper import com.google.android.finsky.VersionCodeWrapper +import com.google.android.finsky.callerAppToIntegrityData import com.google.android.finsky.getPlayCoreVersion import com.google.android.finsky.encodeBase64 import com.google.android.finsky.getAuthToken import com.google.android.finsky.getPackageInfoCompat import com.google.android.finsky.model.IntegrityErrorCode +import com.google.android.finsky.model.StandardIntegrityException import com.google.android.finsky.requestIntegritySyncData import com.google.android.finsky.sha256 import com.google.android.finsky.signaturesCompat +import com.google.android.finsky.updateAppIntegrityContent import com.google.android.gms.droidguard.DroidGuard import com.google.android.gms.droidguard.internal.DroidGuardResultsRequest import com.google.android.gms.tasks.await @@ -62,6 +66,7 @@ import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext import okio.ByteString.Companion.toByteString import org.microg.gms.profile.ProfileManager +import org.microg.gms.vending.PlayIntegrityData private const val TAG = "IntegrityService" @@ -82,6 +87,8 @@ class IntegrityService : LifecycleService() { private class IntegrityServiceImpl(private val context: Context, override val lifecycle: Lifecycle) : IIntegrityService.Stub(), LifecycleOwner { + private var integrityData: PlayIntegrityData? = null + override fun requestDialog(bundle: Bundle, callback: IRequestDialogCallback) { Log.d(TAG, "Method (requestDialog) called but not implemented ") requestAndShowDialog(bundle, callback) @@ -93,63 +100,66 @@ private class IntegrityServiceImpl(private val context: Context, override val li override fun requestIntegrityToken(request: Bundle, callback: IIntegrityServiceCallback) { Log.d(TAG, "Method (requestIntegrityToken) called") - val packageName = request.getString(KEY_PACKAGE_NAME) - if (packageName == null) { - callback.onError("", IntegrityErrorCode.INTERNAL_ERROR, "Null packageName.") - return - } - val nonceArr = request.getByteArray(KEY_NONCE) - if (nonceArr == null) { - callback.onError(packageName, IntegrityErrorCode.INTERNAL_ERROR, "Nonce missing.") - return - } - if (nonceArr.size < 16) { - callback.onError(packageName, IntegrityErrorCode.NONCE_TOO_SHORT, "Nonce too short.") - return - } - if (nonceArr.size >= 500) { - callback.onError(packageName, IntegrityErrorCode.NONCE_TOO_LONG, "Nonce too long.") - return - } - val cloudProjectNumber = request.getLong(KEY_CLOUD_PROJECT, 0L) - val playCoreVersion = request.getPlayCoreVersion() - Log.d(TAG, "requestIntegrityToken(packageName: $packageName, nonce: ${nonceArr.encodeBase64(false)}, cloudProjectNumber: $cloudProjectNumber, playCoreVersion: $playCoreVersion)") - - val packageInfo = context.packageManager.getPackageInfoCompat(packageName, SIGNING_FLAGS) - val timestamp = makeTimestamp(System.currentTimeMillis()) - val versionCode = packageInfo.versionCode - - val integrityParams = IntegrityParams( - packageName = PackageNameWrapper(packageName), - versionCode = VersionCodeWrapper(versionCode), - nonce = nonceArr.encodeBase64(noPadding = false, noWrap = true, urlSafe = true), - certificateSha256Digests = packageInfo.signaturesCompat.map { - it.toByteArray().sha256().encodeBase64(noPadding = true, noWrap = true, urlSafe = true) - }, - timestampAtRequest = timestamp, - cloudProjectNumber = cloudProjectNumber.takeIf { it > 0L } - ) - - val data = mutableMapOf( - PARAMS_PKG_KEY to packageName, - PARAMS_VC_KEY to versionCode.toString(), - PARAMS_NONCE_SHA256_KEY to nonceArr.sha256().encodeBase64(noPadding = true, noWrap = true, urlSafe = true), - PARAMS_TM_S_KEY to timestamp.seconds.toString(), - PARAMS_BINDING_KEY to integrityParams.encode().encodeBase64(noPadding = false, noWrap = true, urlSafe = true), - ) - if (cloudProjectNumber > 0L) { - data[PARAMS_GCP_N_KEY] = cloudProjectNumber.toString() - } - - var mapSize = 0 - data.entries.forEach { mapSize += it.key.toByteArray().size + it.value.toByteArray().size } - if (mapSize > 65536) { - callback.onError(packageName, IntegrityErrorCode.INTERNAL_ERROR, "Content binding size exceeded maximum allowed size.") - return - } - lifecycleScope.launchWhenCreated { runCatching { + val packageName = request.getString(KEY_PACKAGE_NAME) + if (packageName == null) { + throw StandardIntegrityException(IntegrityErrorCode.INTERNAL_ERROR, "Null packageName.") + } + integrityData = callerAppToIntegrityData(context, packageName) + if (integrityData?.allowed != true) { + throw StandardIntegrityException(IntegrityErrorCode.INTERNAL_ERROR, "Not allowed to request integrity token.") + } + val playIntegrityEnabled = VendingPreferences.isDeviceAttestationEnabled(context) + if (!playIntegrityEnabled) { + throw StandardIntegrityException(IntegrityErrorCode.INTERNAL_ERROR, "API is disabled.") + } + val nonceArr = request.getByteArray(KEY_NONCE) + if (nonceArr == null) { + throw StandardIntegrityException(IntegrityErrorCode.INTERNAL_ERROR, "Nonce missing.") + } + if (nonceArr.size < 16) { + throw StandardIntegrityException(IntegrityErrorCode.NONCE_TOO_SHORT, "Nonce too short.") + } + if (nonceArr.size >= 500) { + throw StandardIntegrityException(IntegrityErrorCode.NONCE_TOO_LONG, "Nonce too long.") + } + val cloudProjectNumber = request.getLong(KEY_CLOUD_PROJECT, 0L) + val playCoreVersion = request.getPlayCoreVersion() + Log.d(TAG, "requestIntegrityToken(packageName: $packageName, nonce: ${nonceArr.encodeBase64(false)}, cloudProjectNumber: $cloudProjectNumber, playCoreVersion: $playCoreVersion)") + + val packageInfo = context.packageManager.getPackageInfoCompat(packageName, SIGNING_FLAGS) + val timestamp = makeTimestamp(System.currentTimeMillis()) + val versionCode = packageInfo.versionCode + + val integrityParams = IntegrityParams( + packageName = PackageNameWrapper(packageName), + versionCode = VersionCodeWrapper(versionCode), + nonce = nonceArr.encodeBase64(noPadding = false, noWrap = true, urlSafe = true), + certificateSha256Digests = packageInfo.signaturesCompat.map { + it.toByteArray().sha256().encodeBase64(noPadding = true, noWrap = true, urlSafe = true) + }, + timestampAtRequest = timestamp, + cloudProjectNumber = cloudProjectNumber.takeIf { it > 0L } + ) + + val data = mutableMapOf( + PARAMS_PKG_KEY to packageName, + PARAMS_VC_KEY to versionCode.toString(), + PARAMS_NONCE_SHA256_KEY to nonceArr.sha256().encodeBase64(noPadding = true, noWrap = true, urlSafe = true), + PARAMS_TM_S_KEY to timestamp.seconds.toString(), + PARAMS_BINDING_KEY to integrityParams.encode().encodeBase64(noPadding = false, noWrap = true, urlSafe = true), + ) + if (cloudProjectNumber > 0L) { + data[PARAMS_GCP_N_KEY] = cloudProjectNumber.toString() + } + + var mapSize = 0 + data.entries.forEach { mapSize += it.key.toByteArray().size + it.value.toByteArray().size } + if (mapSize > 65536) { + throw StandardIntegrityException(IntegrityErrorCode.INTERNAL_ERROR, "Content binding size exceeded maximum allowed size.") + } + val authToken = getAuthToken(context, AUTH_TOKEN_SCOPE) if (TextUtils.isEmpty(authToken)) { Log.w(TAG, "requestIntegrityToken: Got null auth token for type: $AUTH_TOKEN_SCOPE") @@ -167,8 +177,7 @@ private class IntegrityServiceImpl(private val context: Context, override val li if (droidGuardData.utf8().startsWith(INTEGRITY_PREFIX_ERROR)) { Log.w(TAG, "droidGuardData: ${droidGuardData.utf8()}") - callback.onError(packageName, IntegrityErrorCode.NETWORK_ERROR, "DroidGuard failed.") - return@launchWhenCreated + throw StandardIntegrityException(IntegrityErrorCode.NETWORK_ERROR, "DroidGuard failed.") } val integrityRequest = IntegrityRequest( @@ -193,15 +202,19 @@ private class IntegrityServiceImpl(private val context: Context, override val li val integrityToken = integrityResponse.contentWrapper?.content?.token if (integrityToken.isNullOrEmpty()) { - callback.onError(packageName, IntegrityErrorCode.INTERNAL_ERROR, "IntegrityResponse didn't have a token") - return@launchWhenCreated + if (integrityResponse.integrityResponseError?.error != null) { + throw StandardIntegrityException(IntegrityErrorCode.INTERNAL_ERROR, integrityResponse.integrityResponseError.error) + } + throw StandardIntegrityException(IntegrityErrorCode.INTERNAL_ERROR, "No token in response.") } Log.d(TAG, "requestIntegrityToken integrityToken: $integrityToken") + integrityData?.updateAppIntegrityContent(context, System.currentTimeMillis(), "Delivered encrypted integrity token.", true) callback.onSuccess(packageName, integrityToken) }.onFailure { Log.w(TAG, "requestIntegrityToken has exception: ", it) - callback.onError(packageName, IntegrityErrorCode.INTERNAL_ERROR, it.message ?: "Exception") + integrityData?.updateAppIntegrityContent(context, System.currentTimeMillis(), "Integrity check failed: ${it.message}") + callback.onError(integrityData?.packageName, IntegrityErrorCode.INTERNAL_ERROR, it.message ?: "Exception") } } } From 00b8c36372d7eff9326e462e1ad76046108b2715 Mon Sep 17 00:00:00 2001 From: DaVinci9196 <150454414+DaVinci9196@users.noreply.github.com> Date: Thu, 8 Jan 2026 03:01:45 +0800 Subject: [PATCH 12/19] HmsMaps: Optimize UI controls (#3129) --- .../org/microg/gms/maps/hms/GoogleMap.kt | 15 +++--- .../org/microg/gms/maps/hms/Projection.kt | 14 ++++-- .../org/microg/gms/maps/hms/UiSettings.kt | 48 ++++++++++++------- 3 files changed, 48 insertions(+), 29 deletions(-) diff --git a/play-services-maps/core/hms/src/main/kotlin/org/microg/gms/maps/hms/GoogleMap.kt b/play-services-maps/core/hms/src/main/kotlin/org/microg/gms/maps/hms/GoogleMap.kt index 0f239266f2..223fac9992 100644 --- a/play-services-maps/core/hms/src/main/kotlin/org/microg/gms/maps/hms/GoogleMap.kt +++ b/play-services-maps/core/hms/src/main/kotlin/org/microg/gms/maps/hms/GoogleMap.kt @@ -441,7 +441,7 @@ class GoogleMapImpl(private val context: Context, var options: GoogleMapOptions) } override fun getUiSettings(): IUiSettingsDelegate = - map?.uiSettings?.let { UiSettingsImpl(it, view) } ?: UiSettingsCache().also { + map?.uiSettings?.let { UiSettingsImpl(it, view) } ?: UiSettingsCache(view).also { internalOnInitializedCallbackList.add(it.getMapReadyCallback()) } @@ -661,13 +661,7 @@ class GoogleMapImpl(private val context: Context, var options: GoogleMapOptions) if (SDK_INT >= 26) { mapView?.let { it.parent?.onDescendantInvalidated(it, it) } } - map?.let { - val cameraPosition = it.cameraPosition - val tilt = cameraPosition.tilt - val bearing = cameraPosition.bearing - val useFast = tilt < 1f && (bearing % 360f < 1f || bearing % 360f > 359f) - projectionImpl?.updateProjectionState(it.projection, useFast) - } + map?.let { projectionImpl?.updateProjectionState(it.cameraPosition, it.projection) } cameraMoveListener?.onCameraMove() cameraChangeListener?.onCameraChange(map?.cameraPosition?.toGms()) } catch (e: Exception) { @@ -784,7 +778,12 @@ class GoogleMapImpl(private val context: Context, var options: GoogleMapOptions) map.setOnCameraMoveListener { Log.d(TAG, "initMap: onCameraMove: ") try { + if (SDK_INT >= 26) { + mapView?.let { it.parent?.onDescendantInvalidated(it, it) } + } + map.let { projectionImpl?.updateProjectionState(it.cameraPosition, it.projection) } cameraMoveListener?.onCameraMove() + cameraChangeListener?.onCameraChange(map.cameraPosition?.toGms()) } catch (e: Exception) { Log.w(TAG, e) } diff --git a/play-services-maps/core/hms/src/main/kotlin/org/microg/gms/maps/hms/Projection.kt b/play-services-maps/core/hms/src/main/kotlin/org/microg/gms/maps/hms/Projection.kt index b054c95e21..a83370608d 100644 --- a/play-services-maps/core/hms/src/main/kotlin/org/microg/gms/maps/hms/Projection.kt +++ b/play-services-maps/core/hms/src/main/kotlin/org/microg/gms/maps/hms/Projection.kt @@ -15,6 +15,7 @@ import com.google.android.gms.maps.model.LatLng import com.google.android.gms.maps.model.LatLngBounds import com.google.android.gms.maps.model.VisibleRegion import com.huawei.hms.maps.Projection +import com.huawei.hms.maps.model.CameraPosition import org.microg.gms.maps.hms.utils.toGms import org.microg.gms.maps.hms.utils.toHms import kotlin.math.roundToInt @@ -38,11 +39,14 @@ class ProjectionImpl(private var projection: Projection, private var withoutTilt private var farRightX = farRight?.x ?: (farLeftX + 1) private var nearLeftY = nearLeft?.y ?: (farLeftY + 1) - fun updateProjectionState(newProjection: Projection, useFastMode: Boolean) { - Log.d(TAG, "updateProjectionState: useFastMode: $useFastMode") - projection = newProjection - visibleRegion = newProjection.visibleRegion - withoutTiltOrBearing = useFastMode + fun updateProjectionState(cameraPosition: CameraPosition, projection: Projection) { + val tilt = cameraPosition.tilt + val bearing = cameraPosition.bearing + val useFast = tilt < 1f && (bearing % 360f < 1f || bearing % 360f > 359f) + Log.d(TAG, "updateProjectionState: useFastMode: $useFast") + + visibleRegion = projection.visibleRegion + withoutTiltOrBearing = useFast farLeft = visibleRegion.farLeft?.let { projection.toScreenLocation(it) } farRight = visibleRegion.farRight?.let { projection.toScreenLocation(it) } diff --git a/play-services-maps/core/hms/src/main/kotlin/org/microg/gms/maps/hms/UiSettings.kt b/play-services-maps/core/hms/src/main/kotlin/org/microg/gms/maps/hms/UiSettings.kt index cb2f59492b..945b06e539 100644 --- a/play-services-maps/core/hms/src/main/kotlin/org/microg/gms/maps/hms/UiSettings.kt +++ b/play-services-maps/core/hms/src/main/kotlin/org/microg/gms/maps/hms/UiSettings.kt @@ -19,7 +19,20 @@ private const val TAG = "GmsMapsUiSettings" /** * This class "implements" unimplemented methods to avoid duplication in subclasses */ -abstract class AbstractUiSettings : IUiSettingsDelegate.Stub() { +abstract class AbstractUiSettings(rootView: ViewGroup) : IUiSettingsDelegate.Stub() { + + protected val mapUiController = MapUiController(rootView) + + init { + mapUiController.initUiStates( + mapOf( + MapUiElement.MyLocationButton to false, + MapUiElement.ZoomView to false, + MapUiElement.CompassView to false + ) + ) + } + override fun setZoomControlsEnabled(zoom: Boolean) { Log.d(TAG, "unimplemented Method: setZoomControlsEnabled") } @@ -66,22 +79,12 @@ abstract class AbstractUiSettings : IUiSettingsDelegate.Stub() { } } -class UiSettingsImpl(private val uiSettings: UiSettings, rootView: ViewGroup) : IUiSettingsDelegate.Stub() { - - private val mapUiController = MapUiController(rootView) +class UiSettingsImpl(private val uiSettings: UiSettings, rootView: ViewGroup) : AbstractUiSettings(rootView) { init { uiSettings.isZoomControlsEnabled = false uiSettings.isCompassEnabled = false - uiSettings.isMapToolbarEnabled = false uiSettings.isMyLocationButtonEnabled = false - mapUiController.initUiStates( - mapOf( - MapUiElement.MyLocationButton to false, - MapUiElement.ZoomView to false, - MapUiElement.CompassView to false - ) - ) } override fun setZoomControlsEnabled(zoom: Boolean) { @@ -180,7 +183,7 @@ class UiSettingsImpl(private val uiSettings: UiSettings, rootView: ViewGroup) : } } -class UiSettingsCache : AbstractUiSettings() { +class UiSettingsCache(rootView: ViewGroup) : AbstractUiSettings(rootView) { private var compass: Boolean? = null private var scrollGestures: Boolean? = null @@ -300,15 +303,28 @@ class UiSettingsCache : AbstractUiSettings() { fun getMapReadyCallback(): OnMapReadyCallback = OnMapReadyCallback { map -> val uiSettings = map.uiSettings - compass?.let { uiSettings.isCompassEnabled = it } + uiSettings.isZoomControlsEnabled = false + uiSettings.isCompassEnabled = false + uiSettings.isMyLocationButtonEnabled = false + + compass?.let { + uiSettings.isCompassEnabled = it + mapUiController.setUiEnabled(MapUiElement.CompassView, it) + } scrollGestures?.let { uiSettings.isScrollGesturesEnabled = it } zoomGestures?.let { uiSettings.isZoomGesturesEnabled = it } tiltGestures?.let { uiSettings.isTiltGesturesEnabled = it } rotateGestures?.let { uiSettings.isRotateGesturesEnabled = it } isAllGesturesEnabled?.let { uiSettings.setAllGesturesEnabled(it) } - isZoomControlsEnabled?.let { uiSettings.isZoomControlsEnabled = it } - isMyLocationButtonEnabled?.let { uiSettings.isMyLocationButtonEnabled = it } + isZoomControlsEnabled?.let { + uiSettings.isZoomControlsEnabled = it + mapUiController.setUiEnabled(MapUiElement.ZoomView, it) + } + isMyLocationButtonEnabled?.let { + uiSettings.isMyLocationButtonEnabled = it + mapUiController.setUiEnabled(MapUiElement.MyLocationButton, it) + } isIndoorLevelPickerEnabled?.let { uiSettings.isIndoorLevelPickerEnabled = it } isMapToolbarEnabled?.let { uiSettings.isMapToolbarEnabled = it } isScrollGesturesEnabledDuringRotateOrZoom?.let { uiSettings.isScrollGesturesEnabledDuringRotateOrZoom = it } From f9d155617846e30a0eab7c532b3477c5751093df Mon Sep 17 00:00:00 2001 From: DaVinci9196 <150454414+DaVinci9196@users.noreply.github.com> Date: Thu, 8 Jan 2026 04:05:47 +0800 Subject: [PATCH 13/19] Vending: Added return value (update.availability) (#3145) --- .../android/finsky/installservice/DevTriggeredUpdateService.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/vending-app/src/main/kotlin/com/google/android/finsky/installservice/DevTriggeredUpdateService.kt b/vending-app/src/main/kotlin/com/google/android/finsky/installservice/DevTriggeredUpdateService.kt index 193ba46662..cab4f9047d 100644 --- a/vending-app/src/main/kotlin/com/google/android/finsky/installservice/DevTriggeredUpdateService.kt +++ b/vending-app/src/main/kotlin/com/google/android/finsky/installservice/DevTriggeredUpdateService.kt @@ -40,7 +40,7 @@ class DevTriggeredUpdateServiceImpl(private val context: Context, override val l override fun requestUpdateInfo(packageName: String?, bundle: Bundle?, callback: IAppUpdateServiceCallback?) { bundle?.keySet() Log.d(TAG, "requestUpdateInfo: packageName: $packageName bundle: $bundle") - callback?.onUpdateResult(bundleOf("error.code" to 0)) + callback?.onUpdateResult(bundleOf("error.code" to 0, "update.availability" to 1)) } override fun completeUpdate(packageName: String?, bundle: Bundle?, callback: IAppUpdateServiceCallback?) { From 207ef8352a282d1725ef7a7ad942ac11e660b680 Mon Sep 17 00:00:00 2001 From: DaVinci9196 <150454414+DaVinci9196@users.noreply.github.com> Date: Thu, 8 Jan 2026 04:06:54 +0800 Subject: [PATCH 14/19] Asset: Fixed game resource download issues (#3124) --- .../assetmoduleservice/AssetModuleService.kt | 89 ++++++++++++------- 1 file changed, 57 insertions(+), 32 deletions(-) diff --git a/vending-app/src/main/kotlin/com/google/android/finsky/assetmoduleservice/AssetModuleService.kt b/vending-app/src/main/kotlin/com/google/android/finsky/assetmoduleservice/AssetModuleService.kt index efc6cb7a54..d8bb72eb2c 100644 --- a/vending-app/src/main/kotlin/com/google/android/finsky/assetmoduleservice/AssetModuleService.kt +++ b/vending-app/src/main/kotlin/com/google/android/finsky/assetmoduleservice/AssetModuleService.kt @@ -56,22 +56,32 @@ class AssetModuleServiceImpl( private val packageDownloadData: MutableMap ) : AbstractAssetModuleServiceImpl(context, lifecycle) { private val fileDescriptorMap = mutableMapOf() + private val lock = Any() private fun checkSessionValid(packageName: String, sessionId: Int) { + Log.d(TAG, "checkSessionValid: $packageName $sessionId ${packageDownloadData[packageName]?.sessionIds}") if (packageDownloadData[packageName]?.sessionIds?.values?.contains(sessionId) != true) { Log.w(TAG, "No active session with id $sessionId in $packageName") throw AssetPackException(AssetPackErrorCode.ACCESS_DENIED) } } - override fun getDefaultSessionId(packageName: String, moduleName: String): Int = + override fun getDefaultSessionId(packageName: String, moduleName: String): Int = synchronized(lock) { packageDownloadData[packageName]?.sessionIds?.get(moduleName) ?: 0 + } override suspend fun startDownload(params: StartDownloadParameters, packageName: String, callback: IAssetModuleServiceCallback?) { - if (packageDownloadData[packageName] == null || - packageDownloadData[packageName]?.packageName != packageName || - packageDownloadData[packageName]?.moduleNames?.intersect(params.moduleNames.toSet())?.isEmpty() == true) { - packageDownloadData[packageName] = httpClient.initAssetModuleData(context, packageName, accountManager, params.moduleNames, params.options) + val needInit = synchronized(lock) { + packageDownloadData[packageName] == null || + packageDownloadData[packageName]?.packageName != packageName || + packageDownloadData[packageName]?.moduleNames?.intersect(params.moduleNames.toSet())?.isEmpty() == true + } + + if (needInit) { + val newData = httpClient.initAssetModuleData(context, packageName, accountManager, params.moduleNames, params.options) + synchronized(lock) { + packageDownloadData[packageName] = packageDownloadData[packageName].merge(newData) + } if (packageDownloadData[packageName] == null) { throw AssetPackException(AssetPackErrorCode.API_NOT_AVAILABLE) } @@ -114,24 +124,26 @@ class AssetModuleServiceImpl( override suspend fun getSessionStates(params: GetSessionStatesParameters, packageName: String, callback: IAssetModuleServiceCallback?) { val listBundleData: MutableList = mutableListOf() - if (packageDownloadData[packageName] != null && packageDownloadData[packageName]?.moduleNames?.all { - packageDownloadData[packageName]?.getModuleData(it)?.status == AssetPackStatus.COMPLETED - } == true && params.installedAssetModules.isEmpty()) { - Log.d(TAG, "getSessionStates: resetAllModuleStatus: $listBundleData") - packageDownloadData[packageName]?.resetAllModuleStatus() - callback?.onGetSessionStates(listBundleData) - return - } + synchronized(lock) { + if (packageDownloadData[packageName] != null && packageDownloadData[packageName]?.moduleNames?.all { + packageDownloadData[packageName]?.getModuleData(it)?.status == AssetPackStatus.COMPLETED + } == true && params.installedAssetModules.isEmpty()) { + Log.d(TAG, "getSessionStates: resetAllModuleStatus: $listBundleData") + packageDownloadData[packageName]?.resetAllModuleStatus() + callback?.onGetSessionStates(listBundleData) + return + } - packageDownloadData[packageName]?.moduleNames?.forEach { moduleName -> - if (moduleName in params.installedAssetModules) return@forEach + packageDownloadData[packageName]?.moduleNames?.forEach { moduleName -> + if (moduleName in params.installedAssetModules) return@forEach - listBundleData.add(sendBroadcastForExistingFile(context, packageDownloadData[packageName]!!, moduleName, null, null)) + listBundleData.add(sendBroadcastForExistingFile(context, packageDownloadData[packageName]!!, moduleName, null, null)) - packageDownloadData[packageName]?.getModuleData(moduleName)?.chunks?.forEach { chunkData -> - val destination = chunkData.getChunkFile(context) - if (destination.exists() && destination.length() == chunkData.chunkBytesToDownload) { - sendBroadcastForExistingFile(context, packageDownloadData[packageName]!!, moduleName, chunkData, destination) + packageDownloadData[packageName]?.getModuleData(moduleName)?.chunks?.forEach { chunkData -> + val destination = chunkData.getChunkFile(context) + if (destination.exists() && destination.length() == chunkData.chunkBytesToDownload) { + sendBroadcastForExistingFile(context, packageDownloadData[packageName]!!, moduleName, chunkData, destination) + } } } } @@ -143,9 +155,11 @@ class AssetModuleServiceImpl( override suspend fun notifyChunkTransferred(params: NotifyChunkTransferredParameters, packageName: String, callback: IAssetModuleServiceCallback?) { checkSessionValid(packageName, params.sessionId) - val downLoadFile = context.getChunkFile(params.sessionId, params.moduleName, params.sliceId, params.chunkNumber) - fileDescriptorMap[downLoadFile]?.close() - fileDescriptorMap.remove(downLoadFile) + synchronized(lock) { + val downLoadFile = context.getChunkFile(params.sessionId, params.moduleName, params.sliceId, params.chunkNumber) + fileDescriptorMap[downLoadFile]?.close() + fileDescriptorMap.remove(downLoadFile) + } // TODO: Remove chunk after successful transfer of chunk or only with module? callback?.onNotifyChunkTransferred( bundleOf(BundleKeys.MODULE_NAME to params.moduleName) + @@ -159,8 +173,10 @@ class AssetModuleServiceImpl( override suspend fun notifyModuleCompleted(params: NotifyModuleCompletedParameters, packageName: String, callback: IAssetModuleServiceCallback?) { checkSessionValid(packageName, params.sessionId) - packageDownloadData[packageName]?.updateDownloadStatus(params.moduleName, AssetPackStatus.COMPLETED) - sendBroadcastForExistingFile(context, packageDownloadData[packageName]!!, params.moduleName, null, null) + synchronized(lock) { + packageDownloadData[packageName]?.updateDownloadStatus(params.moduleName, AssetPackStatus.COMPLETED) + sendBroadcastForExistingFile(context, packageDownloadData[packageName]!!, params.moduleName, null, null) + } val directory = context.getModuleDir(params.sessionId, params.moduleName) if (directory.exists()) { @@ -192,9 +208,11 @@ class AssetModuleServiceImpl( override suspend fun getChunkFileDescriptor(params: GetChunkFileDescriptorParameters, packageName: String, callback: IAssetModuleServiceCallback?) { checkSessionValid(packageName, params.sessionId) - val downLoadFile = context.getChunkFile(params.sessionId, params.moduleName, params.sliceId, params.chunkNumber) - val parcelFileDescriptor = ParcelFileDescriptor.open(downLoadFile, ParcelFileDescriptor.MODE_READ_ONLY).also { - fileDescriptorMap[downLoadFile] = it + val parcelFileDescriptor = synchronized(lock) { + val downLoadFile = context.getChunkFile(params.sessionId, params.moduleName, params.sliceId, params.chunkNumber) + ParcelFileDescriptor.open(downLoadFile, ParcelFileDescriptor.MODE_READ_ONLY).also { + fileDescriptorMap[downLoadFile] = it + } } Log.d(TAG, "getChunkFileDescriptor -> $parcelFileDescriptor") @@ -205,10 +223,17 @@ class AssetModuleServiceImpl( } override suspend fun requestDownloadInfo(params: RequestDownloadInfoParameters, packageName: String, callback: IAssetModuleServiceCallback?) { - if (packageDownloadData[packageName] == null || - packageDownloadData[packageName]?.packageName != packageName || - packageDownloadData[packageName]?.moduleNames?.intersect(params.moduleNames.toSet())?.isEmpty() == true) { - packageDownloadData[packageName] = httpClient.initAssetModuleData(context, packageName, accountManager, params.moduleNames, params.options) + val needInit = synchronized(lock) { + packageDownloadData[packageName] == null || + packageDownloadData[packageName]?.packageName != packageName || + packageDownloadData[packageName]?.moduleNames?.intersect(params.moduleNames.toSet())?.isEmpty() == true + } + + if (needInit) { + val newData = httpClient.initAssetModuleData(context, packageName, accountManager, params.moduleNames, params.options) + synchronized(lock) { + packageDownloadData[packageName] = packageDownloadData[packageName].merge(newData) + } if (packageDownloadData[packageName] == null) { throw AssetPackException(AssetPackErrorCode.API_NOT_AVAILABLE) } From 527a0cd758fb6b39e91f13dc4157a96e93650c45 Mon Sep 17 00:00:00 2001 From: DaVinci9196 <150454414+DaVinci9196@users.noreply.github.com> Date: Thu, 8 Jan 2026 04:08:19 +0800 Subject: [PATCH 15/19] Phenotype: resolve Gmail failing to send emails with attachments (#3189) --- .../main/kotlin/org/microg/gms/phenotype/PhenotypeService.kt | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/play-services-core/src/main/kotlin/org/microg/gms/phenotype/PhenotypeService.kt b/play-services-core/src/main/kotlin/org/microg/gms/phenotype/PhenotypeService.kt index a767ba19fd..fab3e4638c 100644 --- a/play-services-core/src/main/kotlin/org/microg/gms/phenotype/PhenotypeService.kt +++ b/play-services-core/src/main/kotlin/org/microg/gms/phenotype/PhenotypeService.kt @@ -70,6 +70,11 @@ private val CONFIGURATION_OPTIONS = mapOf( Flag("45661535", encodeSupportedLanguageList(), 0), Flag("45700179", encodeSupportedLanguageList(), 0) ), + "gmail_android.user#com.google.android.gm" to arrayOf( + Flag("45624002", true, 0), + Flag("45668769", true, 0), + Flag("45633067", true, 0), + ), ) class PhenotypeServiceImpl(val packageName: String?) : IPhenotypeService.Stub() { From bcd639d55dc45247c95bd8ffcd605507cfcbc292 Mon Sep 17 00:00:00 2001 From: DaVinci9196 <150454414+DaVinci9196@users.noreply.github.com> Date: Thu, 8 Jan 2026 18:02:46 +0800 Subject: [PATCH 16/19] Google 2FA: prevent infinite request loop if registration fails (#3222) Co-authored-by: Marvin W --- play-services-core/src/main/AndroidManifest.xml | 2 +- .../src/main/java/org/microg/gms/gcm/McsService.java | 8 ++++---- .../main/kotlin/org/microg/gms/gcm/GcmInGmsService.kt | 9 +++++++-- .../src/main/kotlin/org/microg/gms/gcm/extensions.kt | 5 +++-- .../main/kotlin/org/microg/gms/ui/AccountsFragment.kt | 5 +++-- 5 files changed, 18 insertions(+), 11 deletions(-) diff --git a/play-services-core/src/main/AndroidManifest.xml b/play-services-core/src/main/AndroidManifest.xml index 6f593efdf8..8e4affa4f4 100644 --- a/play-services-core/src/main/AndroidManifest.xml +++ b/play-services-core/src/main/AndroidManifest.xml @@ -368,7 +368,7 @@ android:exported="false" android:process=":persistent"> - + diff --git a/play-services-core/src/main/java/org/microg/gms/gcm/McsService.java b/play-services-core/src/main/java/org/microg/gms/gcm/McsService.java index 5c298ae418..7bce08dbd3 100644 --- a/play-services-core/src/main/java/org/microg/gms/gcm/McsService.java +++ b/play-services-core/src/main/java/org/microg/gms/gcm/McsService.java @@ -81,8 +81,8 @@ import static android.app.AlarmManager.ELAPSED_REALTIME_WAKEUP; import static android.os.Build.VERSION.SDK_INT; import static org.microg.gms.common.PackageUtils.warnIfNotPersistentProcess; +import static org.microg.gms.gcm.ExtensionsKt.ACTION_GCM_CONNECTED; import static org.microg.gms.gcm.GcmConstants.*; -import static org.microg.gms.gcm.ExtensionsKt.ACTION_GCM_REGISTERED; import static org.microg.gms.gcm.McsConstants.*; @ForegroundServiceInfo(value = "Cloud messaging", resName = "service_name_mcs", resPackage = "com.google.android.gms") @@ -497,15 +497,15 @@ private void handleLoginResponse(LoginResponse loginResponse) { if (loginResponse.error == null) { GcmPrefs.clearLastPersistedId(this); logd(this, "Logged in"); - notifyGcmRegistered(); + notifyGcmConnected(); wakeLock.release(); } else { throw new RuntimeException("Could not login: " + loginResponse.error); } } - private void notifyGcmRegistered() { - Intent intent = new Intent(ACTION_GCM_REGISTERED); + private void notifyGcmConnected() { + Intent intent = new Intent(ACTION_GCM_CONNECTED); intent.setPackage(Constants.GMS_PACKAGE_NAME); sendBroadcast(intent); } diff --git a/play-services-core/src/main/kotlin/org/microg/gms/gcm/GcmInGmsService.kt b/play-services-core/src/main/kotlin/org/microg/gms/gcm/GcmInGmsService.kt index 8304c24463..36b0c36994 100644 --- a/play-services-core/src/main/kotlin/org/microg/gms/gcm/GcmInGmsService.kt +++ b/play-services-core/src/main/kotlin/org/microg/gms/gcm/GcmInGmsService.kt @@ -155,7 +155,8 @@ class GcmInGmsService : LifecycleService() { Log.d(TAG, "start handle gcm message") intent.extras?.let { notifyVerificationInfo(it) } } - ACTION_GCM_REGISTERED -> { + ACTION_GCM_REGISTER_ALL_ACCOUNTS, + ACTION_GCM_CONNECTED -> { updateLocalAccountGroups() } ACTION_GCM_REGISTER_ACCOUNT -> { @@ -370,6 +371,10 @@ class GcmInGmsService : LifecycleService() { completeRegisterRequest(context, gcmDatabase, request).getString(GcmConstants.EXTRA_REGISTRATION_ID) } Log.d(TAG, "GCM IN GMS regId: $regId") + if (regId == null) { + Log.w(TAG, "registerGcmInGms reg id is null") + return + } val sharedPreferencesEditor = sp?.edit() sharedPreferencesEditor?.putLong(KEY_GCM_ANDROID_ID, LastCheckinInfo.read(context).androidId) sharedPreferencesEditor?.putString(KEY_GCM_REG_ID, regId) @@ -510,4 +515,4 @@ class GcmRegistrationReceiver : WakefulBroadcastReceiver() { } ForegroundServiceContext(context).startService(callIntent) } -} \ No newline at end of file +} diff --git a/play-services-core/src/main/kotlin/org/microg/gms/gcm/extensions.kt b/play-services-core/src/main/kotlin/org/microg/gms/gcm/extensions.kt index 428a9bdd7a..8491604554 100644 --- a/play-services-core/src/main/kotlin/org/microg/gms/gcm/extensions.kt +++ b/play-services-core/src/main/kotlin/org/microg/gms/gcm/extensions.kt @@ -12,8 +12,9 @@ import okhttp3.OkHttpClient import okhttp3.Response const val ACTION_GCM_RECONNECT = "org.microg.gms.gcm.RECONNECT" -const val ACTION_GCM_REGISTERED = "org.microg.gms.gcm.REGISTERED" +const val ACTION_GCM_CONNECTED = "org.microg.gms.gcm.CONNECTED" const val ACTION_GCM_REGISTER_ACCOUNT = "org.microg.gms.gcm.REGISTER_ACCOUNT" +const val ACTION_GCM_REGISTER_ALL_ACCOUNTS = "org.microg.gms.gcm.REGISTER_ALL_ACCOUNTS" const val ACTION_GCM_NOTIFY_COMPLETE = "org.microg.gms.gcm.NOTIFY_COMPLETE" const val KEY_GCM_REGISTER_ACCOUNT_NAME = "register_account_name" const val EXTRA_NOTIFICATION_ACCOUNT = "notification_account" @@ -44,4 +45,4 @@ inline fun createGrpcClient( .minMessageToCompress(minMessageToCompress) .build() return grpcClient.create(S::class) -} \ No newline at end of file +} diff --git a/play-services-core/src/main/kotlin/org/microg/gms/ui/AccountsFragment.kt b/play-services-core/src/main/kotlin/org/microg/gms/ui/AccountsFragment.kt index 4102e2c1c7..2be044bd99 100644 --- a/play-services-core/src/main/kotlin/org/microg/gms/ui/AccountsFragment.kt +++ b/play-services-core/src/main/kotlin/org/microg/gms/ui/AccountsFragment.kt @@ -26,7 +26,8 @@ import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext import org.microg.gms.auth.AuthConstants import org.microg.gms.common.Constants -import org.microg.gms.gcm.ACTION_GCM_REGISTERED +import org.microg.gms.gcm.ACTION_GCM_CONNECTED +import org.microg.gms.gcm.ACTION_GCM_REGISTER_ALL_ACCOUNTS import org.microg.gms.people.DatabaseHelper import org.microg.gms.people.PeopleManager import org.microg.gms.settings.SettingsContract @@ -65,7 +66,7 @@ class AccountsFragment : PreferenceFragmentCompat() { }).also { it.isCircular = true } else null private fun registerGcmInGms() { - Intent(ACTION_GCM_REGISTERED).apply { + Intent(ACTION_GCM_REGISTER_ALL_ACCOUNTS).apply { `package` = Constants.GMS_PACKAGE_NAME }.let { requireContext().sendBroadcast(it) } } From 748964bf8046e79cad22a571f72f3b56d93ad3c8 Mon Sep 17 00:00:00 2001 From: DaVinci9196 <150454414+DaVinci9196@users.noreply.github.com> Date: Thu, 8 Jan 2026 18:53:15 +0800 Subject: [PATCH 17/19] Vending: Added InAppReviewActivity (#3163) Co-authored-by: Marvin W --- vending-app/src/main/AndroidManifest.xml | 7 +++++++ .../protocol/IInAppReviewServiceCallback.aidl | 2 +- .../inappreviewdialog/InAppReviewActivity.kt | 20 ++++++++++++++++++ .../inappreviewservice/InAppReviewService.kt | 21 ++++++++++++++++--- 4 files changed, 46 insertions(+), 4 deletions(-) create mode 100644 vending-app/src/main/kotlin/com/google/android/finsky/inappreviewdialog/InAppReviewActivity.kt diff --git a/vending-app/src/main/AndroidManifest.xml b/vending-app/src/main/AndroidManifest.xml index 6cb46bed4e..3bb7d80b28 100644 --- a/vending-app/src/main/AndroidManifest.xml +++ b/vending-app/src/main/AndroidManifest.xml @@ -299,6 +299,13 @@ + + Date: Fri, 9 Jan 2026 04:57:46 +0800 Subject: [PATCH 18/19] Auth: Fixed some login failure issues (#3143) Co-authored-by: Marvin W --- build.gradle | 1 + .../auth/api/identity/ClearTokenRequest.aidl | 8 ++ .../api/identity/RevokeAccessRequest.aidl | 8 ++ .../internal/IAuthorizationService.aidl | 5 + .../api/identity/RevokeAccessRequest.java | 9 ++ .../org/microg/gms/common/AccountUtils.kt | 50 ++++++++++ play-services-core/build.gradle | 2 + .../src/main/AndroidManifest.xml | 3 +- .../microg/gms/gcm/UnregisterReceiver.java | 50 ---------- .../identity/AuthorizationService.kt | 98 +++++++++++++++---- .../identity/IdentitySignInService.kt | 26 +++-- .../gms/auth/signin/AssistedSignInFragment.kt | 38 ++++--- .../gms/auth/signin/AuthSignInActivity.kt | 2 +- .../gms/auth/signin/AuthSignInService.kt | 15 +-- .../auth/signin/SignInConfigurationService.kt | 41 +++++--- .../org/microg/gms/auth/signin/extensions.kt | 11 ++- .../gms/common/PackageIntentOpWorker.kt | 70 +++++++++++++ .../gms/common/PersistentTrustedReceiver.kt | 44 +++++++++ 18 files changed, 360 insertions(+), 121 deletions(-) create mode 100644 play-services-auth/src/main/aidl/com/google/android/gms/auth/api/identity/ClearTokenRequest.aidl create mode 100644 play-services-auth/src/main/aidl/com/google/android/gms/auth/api/identity/RevokeAccessRequest.aidl create mode 100644 play-services-base/core/src/main/kotlin/org/microg/gms/common/AccountUtils.kt delete mode 100644 play-services-core/src/main/java/org/microg/gms/gcm/UnregisterReceiver.java create mode 100644 play-services-core/src/main/kotlin/org/microg/gms/common/PackageIntentOpWorker.kt create mode 100644 play-services-core/src/main/kotlin/org/microg/gms/common/PersistentTrustedReceiver.kt diff --git a/build.gradle b/build.gradle index ae1cd09be2..c05a2b956f 100644 --- a/build.gradle +++ b/build.gradle @@ -24,6 +24,7 @@ buildscript { ext.preferenceVersion = '1.2.0' ext.recyclerviewVersion = '1.3.2' ext.webkitVersion = '1.10.0' + ext.workVersion = '2.7.0' ext.slf4jVersion = '1.7.36' ext.volleyVersion = '1.2.1' diff --git a/play-services-auth/src/main/aidl/com/google/android/gms/auth/api/identity/ClearTokenRequest.aidl b/play-services-auth/src/main/aidl/com/google/android/gms/auth/api/identity/ClearTokenRequest.aidl new file mode 100644 index 0000000000..a2b736fb20 --- /dev/null +++ b/play-services-auth/src/main/aidl/com/google/android/gms/auth/api/identity/ClearTokenRequest.aidl @@ -0,0 +1,8 @@ +/* + * SPDX-FileCopyrightText: 2025 microG Project Team + * SPDX-License-Identifier: Apache-2.0 + */ + +package com.google.android.gms.auth.api.identity; + +parcelable ClearTokenRequest; \ No newline at end of file diff --git a/play-services-auth/src/main/aidl/com/google/android/gms/auth/api/identity/RevokeAccessRequest.aidl b/play-services-auth/src/main/aidl/com/google/android/gms/auth/api/identity/RevokeAccessRequest.aidl new file mode 100644 index 0000000000..25526d9a51 --- /dev/null +++ b/play-services-auth/src/main/aidl/com/google/android/gms/auth/api/identity/RevokeAccessRequest.aidl @@ -0,0 +1,8 @@ +/* + * SPDX-FileCopyrightText: 2025 microG Project Team + * SPDX-License-Identifier: Apache-2.0 + */ + +package com.google.android.gms.auth.api.identity; + +parcelable RevokeAccessRequest; \ No newline at end of file diff --git a/play-services-auth/src/main/aidl/com/google/android/gms/auth/api/identity/internal/IAuthorizationService.aidl b/play-services-auth/src/main/aidl/com/google/android/gms/auth/api/identity/internal/IAuthorizationService.aidl index db1fc73f2a..656f0c0f2d 100644 --- a/play-services-auth/src/main/aidl/com/google/android/gms/auth/api/identity/internal/IAuthorizationService.aidl +++ b/play-services-auth/src/main/aidl/com/google/android/gms/auth/api/identity/internal/IAuthorizationService.aidl @@ -9,8 +9,13 @@ import com.google.android.gms.auth.api.identity.internal.IAuthorizationCallback; import com.google.android.gms.auth.api.identity.internal.IVerifyWithGoogleCallback; import com.google.android.gms.auth.api.identity.AuthorizationRequest; import com.google.android.gms.auth.api.identity.VerifyWithGoogleRequest; +import com.google.android.gms.auth.api.identity.RevokeAccessRequest; +import com.google.android.gms.auth.api.identity.ClearTokenRequest; +import com.google.android.gms.common.api.internal.IStatusCallback; interface IAuthorizationService { void authorize(in IAuthorizationCallback callback, in AuthorizationRequest request) = 0; void verifyWithGoogle(in IVerifyWithGoogleCallback callback, in VerifyWithGoogleRequest request) = 1; + void revokeAccess(in IStatusCallback callback, in RevokeAccessRequest request) = 2; + void clearToken(in IStatusCallback callback, in ClearTokenRequest request) = 3; } \ No newline at end of file diff --git a/play-services-auth/src/main/java/com/google/android/gms/auth/api/identity/RevokeAccessRequest.java b/play-services-auth/src/main/java/com/google/android/gms/auth/api/identity/RevokeAccessRequest.java index 71c3140f9b..0019e5cf0a 100644 --- a/play-services-auth/src/main/java/com/google/android/gms/auth/api/identity/RevokeAccessRequest.java +++ b/play-services-auth/src/main/java/com/google/android/gms/auth/api/identity/RevokeAccessRequest.java @@ -143,4 +143,13 @@ public static abstract class Builder { public void writeToParcel(@NonNull Parcel parcel, int flags) { CREATOR.writeToParcel(this, parcel, flags); } + + @Override + public String toString() { + return "RevokeAccessRequest{" + + "scopes=" + scopes + + ", account=" + account + + ", sessionId='" + sessionId + '\'' + + '}'; + } } diff --git a/play-services-base/core/src/main/kotlin/org/microg/gms/common/AccountUtils.kt b/play-services-base/core/src/main/kotlin/org/microg/gms/common/AccountUtils.kt new file mode 100644 index 0000000000..51a6637a30 --- /dev/null +++ b/play-services-base/core/src/main/kotlin/org/microg/gms/common/AccountUtils.kt @@ -0,0 +1,50 @@ +/** + * SPDX-FileCopyrightText: 2025 microG Project Team + * SPDX-License-Identifier: Apache-2.0 + */ + +package org.microg.gms.common + +import android.accounts.Account +import android.annotation.SuppressLint +import android.content.Context +import android.content.Context.MODE_PRIVATE +import androidx.core.content.edit +import org.microg.gms.auth.AuthConstants + +class AccountUtils(val context: Context) { + + private val prefs = context.getSharedPreferences("common.selected_account_prefs", MODE_PRIVATE) + + companion object { + private const val TYPE = "selected_account_type:" + @SuppressLint("StaticFieldLeak") + @Volatile + private var instance: AccountUtils? = null + fun get(context: Context): AccountUtils = instance ?: synchronized(this) { + instance ?: AccountUtils(context.applicationContext).also { instance = it } + } + } + + fun saveSelectedAccount(packageName: String, account: Account?) { + if (account != null) { + prefs.edit(true) { + putString(packageName, account.name) + putString(TYPE.plus(packageName), account.type) + } + } + } + + fun getSelectedAccount(packageName: String): Account? { + val name = prefs.getString(packageName, null) ?: return null + val type = prefs.getString(TYPE.plus(packageName), AuthConstants.DEFAULT_ACCOUNT_TYPE) ?: return null + return Account(name, type) + } + + fun removeSelectedAccount(packageName: String) { + prefs.edit { + remove(packageName) + remove(TYPE.plus(packageName)) + } + } +} \ No newline at end of file diff --git a/play-services-core/build.gradle b/play-services-core/build.gradle index 5648333872..8fc2896bbc 100644 --- a/play-services-core/build.gradle +++ b/play-services-core/build.gradle @@ -102,6 +102,8 @@ dependencies { implementation "androidx.lifecycle:lifecycle-service:$lifecycleVersion" implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlinVersion" + + implementation "androidx.work:work-runtime-ktx:$workVersion" } android { diff --git a/play-services-core/src/main/AndroidManifest.xml b/play-services-core/src/main/AndroidManifest.xml index 8e4affa4f4..e25e14c433 100644 --- a/play-services-core/src/main/AndroidManifest.xml +++ b/play-services-core/src/main/AndroidManifest.xml @@ -412,8 +412,7 @@ + android:name="org.microg.gms.common.PersistentTrustedReceiver"> diff --git a/play-services-core/src/main/java/org/microg/gms/gcm/UnregisterReceiver.java b/play-services-core/src/main/java/org/microg/gms/gcm/UnregisterReceiver.java deleted file mode 100644 index bba2e414c4..0000000000 --- a/play-services-core/src/main/java/org/microg/gms/gcm/UnregisterReceiver.java +++ /dev/null @@ -1,50 +0,0 @@ -package org.microg.gms.gcm; - -import android.content.BroadcastReceiver; -import android.content.Context; -import android.content.Intent; -import android.util.Log; - -import java.util.List; - -import static android.content.Intent.ACTION_PACKAGE_REMOVED; -import static android.content.Intent.ACTION_PACKAGE_DATA_CLEARED; -import static android.content.Intent.ACTION_PACKAGE_FULLY_REMOVED; -import static android.content.Intent.EXTRA_DATA_REMOVED; -import static android.content.Intent.EXTRA_REPLACING; - -public class UnregisterReceiver extends BroadcastReceiver { - private static final String TAG = "GmsGcmUnregisterRcvr"; - - @Override - public void onReceive(final Context context, Intent intent) { - Log.d(TAG, "Package changed: " + intent); - if ((ACTION_PACKAGE_REMOVED.contains(intent.getAction()) && intent.getBooleanExtra(EXTRA_DATA_REMOVED, false) && - !intent.getBooleanExtra(EXTRA_REPLACING, false)) || - ACTION_PACKAGE_FULLY_REMOVED.contains(intent.getAction()) || - ACTION_PACKAGE_DATA_CLEARED.contains(intent.getAction())) { - final GcmDatabase database = new GcmDatabase(context); - final String packageName = intent.getData().getSchemeSpecificPart(); - Log.d(TAG, "Package removed or data cleared: " + packageName); - final GcmDatabase.App app = database.getApp(packageName); - if (app != null) { - new Thread(new Runnable() { - @Override - public void run() { - List registrations = database.getRegistrationsByApp(packageName); - boolean deletedAll = true; - for (GcmDatabase.Registration registration : registrations) { - deletedAll &= PushRegisterManager.unregister(context, registration.packageName, registration.signature, null, null).deleted != null; - } - if (deletedAll) { - database.removeApp(packageName); - } - database.close(); - } - }).start(); - } else { - database.close(); - } - } - } -} diff --git a/play-services-core/src/main/kotlin/org/microg/gms/auth/credentials/identity/AuthorizationService.kt b/play-services-core/src/main/kotlin/org/microg/gms/auth/credentials/identity/AuthorizationService.kt index 2b96ddc12d..286b543485 100644 --- a/play-services-core/src/main/kotlin/org/microg/gms/auth/credentials/identity/AuthorizationService.kt +++ b/play-services-core/src/main/kotlin/org/microg/gms/auth/credentials/identity/AuthorizationService.kt @@ -5,6 +5,7 @@ package org.microg.gms.auth.credentials.identity +import android.accounts.AccountManager import android.app.PendingIntent import android.app.PendingIntent.FLAG_IMMUTABLE import android.app.PendingIntent.FLAG_UPDATE_CURRENT @@ -16,6 +17,8 @@ import androidx.lifecycle.LifecycleOwner import androidx.lifecycle.lifecycleScope import com.google.android.gms.auth.api.identity.AuthorizationRequest import com.google.android.gms.auth.api.identity.AuthorizationResult +import com.google.android.gms.auth.api.identity.ClearTokenRequest +import com.google.android.gms.auth.api.identity.RevokeAccessRequest import com.google.android.gms.auth.api.identity.VerifyWithGoogleRequest import com.google.android.gms.auth.api.identity.VerifyWithGoogleResult import com.google.android.gms.auth.api.identity.internal.IAuthorizationCallback @@ -26,21 +29,26 @@ import com.google.android.gms.auth.api.signin.internal.SignInConfiguration import com.google.android.gms.common.ConnectionResult import com.google.android.gms.common.api.Scope import com.google.android.gms.common.api.Status +import com.google.android.gms.common.api.internal.IStatusCallback import com.google.android.gms.common.internal.ConnectionInfo import com.google.android.gms.common.internal.GetServiceRequest import com.google.android.gms.common.internal.IGmsCallbacks import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext import org.microg.gms.BaseService +import org.microg.gms.auth.AuthConstants import org.microg.gms.auth.credentials.FEATURES import org.microg.gms.auth.signin.AuthSignInActivity import org.microg.gms.auth.signin.SignInConfigurationService +import org.microg.gms.auth.signin.getOAuthManager import org.microg.gms.auth.signin.getServerAuthTokenManager import org.microg.gms.auth.signin.performSignIn import org.microg.gms.auth.signin.scopeUris +import org.microg.gms.common.AccountUtils import org.microg.gms.common.Constants import org.microg.gms.common.GmsService import org.microg.gms.common.PackageUtils +import java.util.concurrent.atomic.AtomicInteger private const val TAG = "AuthorizationService" @@ -60,42 +68,67 @@ class AuthorizationService : BaseService(TAG, GmsService.AUTH_API_IDENTITY_AUTHO class AuthorizationServiceImpl(val context: Context, val packageName: String, override val lifecycle: Lifecycle) : IAuthorizationService.Stub(), LifecycleOwner { + companion object{ + private val nextRequestCode = AtomicInteger(0) + } + override fun authorize(callback: IAuthorizationCallback?, request: AuthorizationRequest?) { - Log.d(TAG, "Method: authorize called, request:$request") + Log.d(TAG, "Method: authorize called, packageName:$packageName request:$request") lifecycleScope.launchWhenStarted { - val account = request?.account ?: SignInConfigurationService.getDefaultAccount(context, packageName) - if (account == null) { - Log.d(TAG, "Method: authorize called, but account is null") - callback?.onAuthorized(Status.CANCELED, null) - return@launchWhenStarted - } + val requestAccount = request?.account + val account = requestAccount ?: AccountUtils.get(context).getSelectedAccount(packageName) val googleSignInOptions = GoogleSignInOptions.Builder().apply { - setAccountName(account.name) request?.requestedScopes?.forEach { requestScopes(it) } - if (request?.idTokenRequested == true && request.serverClientId != null) requestIdToken(request.serverClientId) + if (request?.idTokenRequested == true && request.serverClientId != null) { + if (account?.name != requestAccount?.name) { + requestEmail().requestProfile() + } + requestIdToken(request.serverClientId) + } if (request?.serverAuthCodeRequested == true && request.serverClientId != null) requestServerAuthCode(request.serverClientId, request.forceCodeForRefreshToken) }.build() - val intent = Intent(context, AuthSignInActivity::class.java).apply { - `package` = Constants.GMS_PACKAGE_NAME - putExtra("config", SignInConfiguration(packageName, googleSignInOptions)) - } - val signInAccount = performSignIn(context, packageName, googleSignInOptions, account, false) - callback?.onAuthorized(Status.SUCCESS, + Log.d(TAG, "authorize: account: ${account?.name}") + val result = if (account != null) { + val (accessToken, signInAccount) = performSignIn(context, packageName, googleSignInOptions, account, false) + if (requestAccount != null) { + AccountUtils.get(context).saveSelectedAccount(packageName, requestAccount) + } AuthorizationResult( signInAccount?.serverAuthCode, - signInAccount?.idToken, + accessToken, signInAccount?.idToken, signInAccount?.grantedScopes?.toList().orEmpty().map { it.scopeUri }, signInAccount, - PendingIntent.getActivity(context, account.hashCode(), intent, FLAG_UPDATE_CURRENT or FLAG_IMMUTABLE) - ).also { Log.d(TAG, "authorize: result:$it") }) + null + ) + } else { + val options = GoogleSignInOptions.Builder(googleSignInOptions).apply { + val defaultAccount = SignInConfigurationService.getDefaultAccount(context, packageName) + defaultAccount?.name?.let { setAccountName(it) } + }.build() + val intent = Intent(context, AuthSignInActivity::class.java).apply { + `package` = Constants.GMS_PACKAGE_NAME + putExtra("config", SignInConfiguration(packageName, options)) + } + AuthorizationResult( + null, + null, + null, + request?.requestedScopes.orEmpty().map { it.scopeUri }, + null, + PendingIntent.getActivity(context, nextRequestCode.incrementAndGet(), intent, FLAG_UPDATE_CURRENT or FLAG_IMMUTABLE) + ) + } + runCatching { + callback?.onAuthorized(Status.SUCCESS, result.also { Log.d(TAG, "authorize: result:$it") }) + } } } override fun verifyWithGoogle(callback: IVerifyWithGoogleCallback?, request: VerifyWithGoogleRequest?) { Log.d(TAG, "unimplemented Method: verifyWithGoogle: request:$request") lifecycleScope.launchWhenStarted { - val account = SignInConfigurationService.getDefaultAccount(context, packageName) + val account = AccountUtils.get(context).getSelectedAccount(packageName) ?: SignInConfigurationService.getDefaultAccount(context, packageName) if (account == null) { Log.d(TAG, "Method: authorize called, but account is null") callback?.onVerifed(Status.CANCELED, null) @@ -119,4 +152,31 @@ class AuthorizationServiceImpl(val context: Context, val packageName: String, ov } } + override fun revokeAccess(callback: IStatusCallback?, request: RevokeAccessRequest?) { + Log.d(TAG, "Method: revokeAccess called, request:$request") + lifecycleScope.launchWhenStarted { + val authOptions = SignInConfigurationService.getAuthOptions(context, packageName) + val authAccount = request?.account + if (authOptions.isNotEmpty() && authAccount != null) { + val authManager = getOAuthManager(context, packageName, authOptions.first(), authAccount) + val token = authManager.peekAuthToken() + if (token != null) { + // todo "https://oauth2.googleapis.com/revoke" + authManager.invalidateAuthToken(token) + authManager.isPermitted = false + } + } + AccountUtils.get(context).removeSelectedAccount(packageName) + runCatching { callback?.onResult(Status.SUCCESS) } + } + } + + override fun clearToken(callback: IStatusCallback?, request: ClearTokenRequest?) { + Log.d(TAG, "Method: clearToken called, request:$request") + request?.token?.let { + AccountManager.get(context).invalidateAuthToken(AuthConstants.DEFAULT_ACCOUNT_TYPE, it) + } + runCatching { callback?.onResult(Status.SUCCESS) } + } + } \ No newline at end of file diff --git a/play-services-core/src/main/kotlin/org/microg/gms/auth/credentials/identity/IdentitySignInService.kt b/play-services-core/src/main/kotlin/org/microg/gms/auth/credentials/identity/IdentitySignInService.kt index 4ed9bcdc02..815c3c4b60 100644 --- a/play-services-core/src/main/kotlin/org/microg/gms/auth/credentials/identity/IdentitySignInService.kt +++ b/play-services-core/src/main/kotlin/org/microg/gms/auth/credentials/identity/IdentitySignInService.kt @@ -5,7 +5,6 @@ package org.microg.gms.auth.credentials.identity -import android.accounts.AccountManager import android.app.PendingIntent import android.content.Context import android.content.Intent @@ -14,6 +13,9 @@ import android.util.Base64 import android.util.Log import androidx.core.app.PendingIntentCompat import androidx.core.os.bundleOf +import androidx.lifecycle.Lifecycle +import androidx.lifecycle.LifecycleOwner +import androidx.lifecycle.lifecycleScope import com.google.android.gms.auth.api.identity.BeginSignInRequest import com.google.android.gms.auth.api.identity.BeginSignInResult import com.google.android.gms.auth.api.identity.GetPhoneNumberHintIntentRequest @@ -34,17 +36,19 @@ import com.google.android.gms.fido.common.Transport import com.google.android.gms.fido.fido2.api.common.PublicKeyCredentialDescriptor import com.google.android.gms.fido.fido2.api.common.PublicKeyCredentialRequestOptions import com.google.android.gms.fido.fido2.api.common.UserVerificationRequirement +import kotlinx.coroutines.launch import org.json.JSONArray import org.json.JSONObject import org.microg.gms.BaseService -import org.microg.gms.auth.AuthConstants import org.microg.gms.auth.signin.ACTION_ASSISTED_SIGN_IN import org.microg.gms.auth.signin.BEGIN_SIGN_IN_REQUEST import org.microg.gms.auth.signin.GET_SIGN_IN_INTENT_REQUEST import org.microg.gms.auth.credentials.FEATURES import org.microg.gms.auth.signin.CLIENT_PACKAGE_NAME import org.microg.gms.auth.signin.GOOGLE_SIGN_IN_OPTIONS +import org.microg.gms.auth.signin.SignInConfigurationService import org.microg.gms.auth.signin.performSignOut +import org.microg.gms.common.AccountUtils import org.microg.gms.common.GmsService import org.microg.gms.fido.core.Database import org.microg.gms.fido.core.ui.AuthenticatorActivity.Companion.KEY_OPTIONS @@ -61,13 +65,13 @@ class IdentitySignInService : BaseService(TAG, GmsService.AUTH_API_IDENTITY_SIGN val connectionInfo = ConnectionInfo() connectionInfo.features = FEATURES callback.onPostInitCompleteWithConnectionInfo( - ConnectionResult.SUCCESS, IdentitySignInServiceImpl(this, request.packageName).asBinder(), connectionInfo + ConnectionResult.SUCCESS, IdentitySignInServiceImpl(this, request.packageName, lifecycle).asBinder(), connectionInfo ) } } -class IdentitySignInServiceImpl(private val context: Context, private val clientPackageName: String) : - ISignInService.Stub() { +class IdentitySignInServiceImpl(private val context: Context, private val clientPackageName: String, override val lifecycle: Lifecycle) : + ISignInService.Stub(), LifecycleOwner { private val requestMap = mutableMapOf() @@ -130,11 +134,15 @@ class IdentitySignInServiceImpl(private val context: Context, private val client override fun signOut(callback: IStatusCallback, requestTag: String) { Log.d(TAG, "method signOut called, requestTag=$requestTag") - if (requestMap.containsKey(requestTag)) { - val accounts = AccountManager.get(context).getAccountsByType(AuthConstants.DEFAULT_ACCOUNT_TYPE) - if (accounts.isNotEmpty()) { - accounts.forEach { performSignOut(context, clientPackageName, requestMap[requestTag], it) } + lifecycleScope.launch { + val signInAccount = SignInConfigurationService.getDefaultAccount(context, clientPackageName) + val authOptions = SignInConfigurationService.getAuthOptions(context, clientPackageName).plus(requestMap[requestTag]) + if (signInAccount != null && authOptions.isNotEmpty()) { + authOptions.forEach { + performSignOut(context, clientPackageName, it, signInAccount) + } } + AccountUtils.get(context).removeSelectedAccount(clientPackageName) } callback.onResult(Status.SUCCESS) } diff --git a/play-services-core/src/main/kotlin/org/microg/gms/auth/signin/AssistedSignInFragment.kt b/play-services-core/src/main/kotlin/org/microg/gms/auth/signin/AssistedSignInFragment.kt index 75711452af..72ba905ce0 100644 --- a/play-services-core/src/main/kotlin/org/microg/gms/auth/signin/AssistedSignInFragment.kt +++ b/play-services-core/src/main/kotlin/org/microg/gms/auth/signin/AssistedSignInFragment.kt @@ -42,6 +42,7 @@ import kotlinx.coroutines.launch import kotlinx.coroutines.withContext import org.microg.gms.auth.AuthConstants import org.microg.gms.auth.login.LoginActivity +import org.microg.gms.common.AccountUtils import org.microg.gms.people.PeopleManager import org.microg.gms.utils.getApplicationLabel @@ -75,6 +76,7 @@ class AssistedSignInFragment : BottomSheetDialogFragment() { private var container: FrameLayout? = null private var loginJob: Job? = null private var isSigningIn = false + private var signInBack = false private val authStatusList = arraySetOf>() private var lastChooseAccount: Account? = null @@ -91,14 +93,18 @@ class AssistedSignInFragment : BottomSheetDialogFragment() { fun initView() { accounts = accountManager.getAccountsByType(AuthConstants.DEFAULT_ACCOUNT_TYPE) lifecycleScope.launch { - if (accounts.isEmpty()) { - addGoogleAccount() - } else { - filterAccountsLogin({ - prepareMultiSignIn(it) - }, { accountName, permitted -> - autoSingleSignIn(accountName, permitted) - }) + runCatching { + if (accounts.isEmpty()) { + addGoogleAccount() + } else { + filterAccountsLogin({ + prepareMultiSignIn(it) + }, { accountName, permitted -> + autoSingleSignIn(accountName, permitted) + }) + } + }.onFailure { + errorResult() } } } @@ -292,8 +298,10 @@ class AssistedSignInFragment : BottomSheetDialogFragment() { } override fun onDismiss(dialog: DialogInterface) { - cancelLogin() - errorResult(Status.CANCELED) + if (!signInBack) { + cancelLogin() + errorResult(Status.CANCELED) + } super.onDismiss(dialog) } @@ -315,7 +323,7 @@ class AssistedSignInFragment : BottomSheetDialogFragment() { isSigningIn = true delay(3000) runCatching { - val googleSignInAccount = withContext(Dispatchers.IO) { + val (_, googleSignInAccount) = withContext(Dispatchers.IO) { performSignIn(requireContext(), clientPackageName, options, lastChooseAccount!!, true, beginSignInRequest.googleIdTokenRequestOptions.nonce) } loginResult(googleSignInAccount) @@ -345,8 +353,12 @@ class AssistedSignInFragment : BottomSheetDialogFragment() { private fun loginResult(googleSignInAccount: GoogleSignInAccount?) { if (activity != null && activity is AssistedSignInActivity) { - val assistedSignInActivity = activity as AssistedSignInActivity - assistedSignInActivity.loginResult(googleSignInAccount) + signInBack = true + runCatching { + val assistedSignInActivity = activity as AssistedSignInActivity + AccountUtils.get(requireContext()).saveSelectedAccount(clientPackageName, googleSignInAccount?.account) + assistedSignInActivity.loginResult(googleSignInAccount) + } } activity?.finish() } diff --git a/play-services-core/src/main/kotlin/org/microg/gms/auth/signin/AuthSignInActivity.kt b/play-services-core/src/main/kotlin/org/microg/gms/auth/signin/AuthSignInActivity.kt index 3f4a5f5bc7..dd7044d1dc 100644 --- a/play-services-core/src/main/kotlin/org/microg/gms/auth/signin/AuthSignInActivity.kt +++ b/play-services-core/src/main/kotlin/org/microg/gms/auth/signin/AuthSignInActivity.kt @@ -179,7 +179,7 @@ class AuthSignInActivity : AppCompatActivity() { } private suspend fun signIn(account: Account) { - val googleSignInAccount = performSignIn(this, config?.packageName!!, config?.options, account, true, idNonce) + val (_, googleSignInAccount) = performSignIn(this, config?.packageName!!, config?.options, account, true, idNonce) if (googleSignInAccount != null) { finishResult(CommonStatusCodes.SUCCESS, account = account, googleSignInAccount = googleSignInAccount) } else { diff --git a/play-services-core/src/main/kotlin/org/microg/gms/auth/signin/AuthSignInService.kt b/play-services-core/src/main/kotlin/org/microg/gms/auth/signin/AuthSignInService.kt index 672bb27e40..104a6b658f 100644 --- a/play-services-core/src/main/kotlin/org/microg/gms/auth/signin/AuthSignInService.kt +++ b/play-services-core/src/main/kotlin/org/microg/gms/auth/signin/AuthSignInService.kt @@ -39,6 +39,7 @@ import com.google.android.gms.common.internal.GetServiceRequest import com.google.android.gms.common.internal.IGmsCallbacks import org.microg.gms.BaseService import org.microg.gms.auth.AuthPrefs +import org.microg.gms.common.AccountUtils import org.microg.gms.common.GmsService import org.microg.gms.common.PackageUtils import org.microg.gms.games.GAMES_PACKAGE_NAME @@ -95,7 +96,7 @@ class AuthSignInServiceImpl( Log.d(TAG, "silentSignIn: account -> ${account?.name}") if (account != null && options?.isForceCodeForRefreshToken != true) { if (getOAuthManager(context, packageName, options, account).isPermitted || AuthPrefs.isTrustGooglePermitted(context)) { - val googleSignInAccount = performSignIn(context, packageName, options, account) + val (_, googleSignInAccount) = performSignIn(context, packageName, options, account) if (googleSignInAccount != null) { sendResult(googleSignInAccount, Status(CommonStatusCodes.SUCCESS)) } else { @@ -120,14 +121,16 @@ class AuthSignInServiceImpl( try { val account = account ?: options?.account ?: SignInConfigurationService.getDefaultAccount(context, packageName) if (account != null) { - val defaultOptions = SignInConfigurationService.getDefaultOptions(context, packageName) - Log.d(TAG, "$packageName:signOut defaultOptions:($defaultOptions)") - performSignOut(context, packageName, defaultOptions ?: options, account) + SignInConfigurationService.getAuthOptions(context, packageName).forEach { + Log.d(TAG, "$packageName:signOut authOption:($it)") + performSignOut(context, packageName, it, account) + } } if (options?.scopes?.any { it.scopeUri.contains(Scopes.GAMES) } == true) { GamesConfigurationService.setDefaultAccount(context, packageName, null) } - SignInConfigurationService.setDefaultSignInInfo(context, packageName, null, null) + AccountUtils.get(context).removeSelectedAccount(packageName) + SignInConfigurationService.setAuthInfo(context, packageName, null, null) runCatching { callbacks.onSignOut(Status.SUCCESS) } } catch (e: Exception) { Log.w(TAG, e) @@ -160,7 +163,7 @@ class AuthSignInServiceImpl( authManager.invalidateAuthToken(token) authManager.isPermitted = false } - SignInConfigurationService.setDefaultSignInInfo(context, packageName, account, options?.toJson()) + SignInConfigurationService.setAuthInfo(context, packageName, account, options?.toJson()) runCatching { callbacks.onRevokeAccess(Status.SUCCESS) } } catch (e: Exception) { Log.w(TAG, e) diff --git a/play-services-core/src/main/kotlin/org/microg/gms/auth/signin/SignInConfigurationService.kt b/play-services-core/src/main/kotlin/org/microg/gms/auth/signin/SignInConfigurationService.kt index 5cc24f7994..8035f323fc 100644 --- a/play-services-core/src/main/kotlin/org/microg/gms/auth/signin/SignInConfigurationService.kt +++ b/play-services-core/src/main/kotlin/org/microg/gms/auth/signin/SignInConfigurationService.kt @@ -54,7 +54,7 @@ class SignInConfigurationService : Service() { val packageName = msg.data?.getString(MSG_DATA_PACKAGE_NAME) val account = msg.data?.getParcelable(MSG_DATA_ACCOUNT) val googleSignInOptions = msg.data?.getString(MSG_DATA_SIGN_IN_OPTIONS) - packageName?.let { setDefaultSignInInfo(it, account, googleSignInOptions) } + packageName?.let { setAuthInfo(it, account, googleSignInOptions) } bundleOf( MSG_DATA_PACKAGE_NAME to packageName, MSG_DATA_ACCOUNT to account, @@ -64,10 +64,10 @@ class SignInConfigurationService : Service() { MSG_GET_DEFAULT_OPTIONS -> { val packageName = msg.data?.getString(MSG_DATA_PACKAGE_NAME) - val googleSignInOptions = packageName?.let { getDefaultOptions(it) } + val googleSignInOptions = packageName?.let { getAuthOptions(it) } bundleOf( MSG_DATA_PACKAGE_NAME to packageName, - MSG_DATA_SIGN_IN_OPTIONS to googleSignInOptions + MSG_DATA_SIGN_IN_OPTIONS to googleSignInOptions?.toTypedArray() ) } @@ -95,23 +95,32 @@ class SignInConfigurationService : Service() { return null } - private fun getDefaultOptions(packageName: String): String? { - val data = preferences.getString(DEFAULT_SIGN_IN_OPTIONS_PREFIX + getPackageNameSuffix(packageName), null) - if (data.isNullOrBlank()) return null + private fun getAuthOptions(packageName: String): Set? { + val data = preferences.getStringSet(DEFAULT_SIGN_IN_OPTIONS_PREFIX + getPackageNameSuffix(packageName), null) + if (data.isNullOrEmpty()) return null return data } - private fun setDefaultSignInInfo(packageName: String, account: Account?, optionsJson: String?) { + private fun setAuthInfo(packageName: String, account: Account?, optionsJson: String?) { val editor: SharedPreferences.Editor = preferences.edit() + val accountPrefix = DEFAULT_ACCOUNT_PREFIX + getPackageNameSuffix(packageName) + val optionsPrefix = DEFAULT_SIGN_IN_OPTIONS_PREFIX + getPackageNameSuffix(packageName) if (account == null || account.name == AuthConstants.DEFAULT_ACCOUNT) { - editor.remove(DEFAULT_ACCOUNT_PREFIX + getPackageNameSuffix(packageName)) + editor.remove(accountPrefix) + editor.remove(optionsPrefix) } else { - editor.putString(DEFAULT_ACCOUNT_PREFIX + getPackageNameSuffix(packageName), account.name) + editor.putString(accountPrefix, account.name) } - if (optionsJson == null) { - editor.remove(DEFAULT_SIGN_IN_OPTIONS_PREFIX + getPackageNameSuffix(packageName)) - } else { - editor.putString(DEFAULT_SIGN_IN_OPTIONS_PREFIX + getPackageNameSuffix(packageName), optionsJson) + if (optionsJson != null) { + val oldOptions = runCatching { preferences.getString(optionsPrefix, null) }.getOrNull() + if (oldOptions != null) { + editor.putStringSet(optionsPrefix, setOf(oldOptions, optionsJson)) + } else { + val savedOptions = preferences.getStringSet(optionsPrefix, emptySet()) ?: emptySet() + val newSet = HashSet(savedOptions) + newSet.add(optionsJson) + editor.putStringSet(optionsPrefix, newSet) + } } editor.apply() } @@ -156,16 +165,16 @@ class SignInConfigurationService : Service() { }).data?.getParcelable(MSG_DATA_ACCOUNT) } - suspend fun getDefaultOptions(context: Context, packageName: String): GoogleSignInOptions? { + suspend fun getAuthOptions(context: Context, packageName: String): Set { return singleRequest(context, Message.obtain().apply { what = MSG_GET_DEFAULT_OPTIONS data = bundleOf( MSG_DATA_PACKAGE_NAME to packageName ) - }).data?.getString(MSG_DATA_SIGN_IN_OPTIONS)?.let { GoogleSignInOptions.fromJson(it) } + }).data?.getStringArray(MSG_DATA_SIGN_IN_OPTIONS)?.map { GoogleSignInOptions.fromJson(it) }?.toSet() ?: emptySet() } - suspend fun setDefaultSignInInfo(context: Context, packageName: String, account: Account?, optionsJson: String?) { + suspend fun setAuthInfo(context: Context, packageName: String, account: Account?, optionsJson: String?) { singleRequest(context, Message.obtain().apply { what = MSG_SET_DEFAULT_SIGN_IN_INFO data = bundleOf( diff --git a/play-services-core/src/main/kotlin/org/microg/gms/auth/signin/extensions.kt b/play-services-core/src/main/kotlin/org/microg/gms/auth/signin/extensions.kt index a3a2ae23a0..76de77e71f 100644 --- a/play-services-core/src/main/kotlin/org/microg/gms/auth/signin/extensions.kt +++ b/play-services-core/src/main/kotlin/org/microg/gms/auth/signin/extensions.kt @@ -107,7 +107,7 @@ suspend fun checkAccountAuthStatus(context: Context, packageName: String, scopeL return withContext(Dispatchers.IO) { authManager.requestAuth(true) }.auth != null } -suspend fun performSignIn(context: Context, packageName: String, options: GoogleSignInOptions?, account: Account, permitted: Boolean = false, idNonce: String? = null): GoogleSignInAccount? { +suspend fun performSignIn(context: Context, packageName: String, options: GoogleSignInOptions?, account: Account, permitted: Boolean = false, idNonce: String? = null): Pair { val authManager = getOAuthManager(context, packageName, options, account) val authResponse = withContext(Dispatchers.IO) { if (options?.includeUnacceptableScope == true || !permitted) { @@ -119,9 +119,9 @@ suspend fun performSignIn(context: Context, packageName: String, options: Google var consentResult:String ?= null if ("remote_consent" == authResponse.issueAdvice && authResponse.resolutionDataBase64 != null){ consentResult = performConsentView(context, packageName, account, authResponse.resolutionDataBase64) - if (consentResult == null) return null + if (consentResult == null) return Pair(null, null) } else { - if (authResponse.auth == null) return null + if (authResponse.auth == null) return Pair(null, null) } Log.d(TAG, "id token requested: ${options?.isIdTokenRequested == true}, serverClientId = ${options?.serverClientId}, permitted = ${authManager.isPermitted}") val idTokenResponse = getIdTokenManager(context, packageName, options, account)?.let { @@ -169,8 +169,8 @@ suspend fun performSignIn(context: Context, packageName: String, options: Google if (options?.includeGame == true) { GamesConfigurationService.setDefaultAccount(context, packageName, account) } - SignInConfigurationService.setDefaultSignInInfo(context, packageName, account, options?.toJson()) - return GoogleSignInAccount( + SignInConfigurationService.setAuthInfo(context, packageName, account, options?.toJson()) + val googleSignInAccount = GoogleSignInAccount( id, tokenId, account.name, @@ -183,6 +183,7 @@ suspend fun performSignIn(context: Context, packageName: String, options: Google givenName, familyName ) + return Pair(authResponse.auth, googleSignInAccount) } suspend fun performConsentView(context: Context, packageName: String, account: Account, dataBase64: String): String? { diff --git a/play-services-core/src/main/kotlin/org/microg/gms/common/PackageIntentOpWorker.kt b/play-services-core/src/main/kotlin/org/microg/gms/common/PackageIntentOpWorker.kt new file mode 100644 index 0000000000..56679a40bb --- /dev/null +++ b/play-services-core/src/main/kotlin/org/microg/gms/common/PackageIntentOpWorker.kt @@ -0,0 +1,70 @@ +/** + * SPDX-FileCopyrightText: 2025 microG Project Team + * SPDX-License-Identifier: Apache-2.0 + */ + +package org.microg.gms.common + +import android.content.Context +import android.util.Log +import androidx.work.CoroutineWorker +import androidx.work.WorkerParameters +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext +import org.microg.gms.auth.signin.SignInConfigurationService +import org.microg.gms.auth.signin.performSignOut +import org.microg.gms.gcm.GcmDatabase +import org.microg.gms.gcm.PushRegisterManager + +class PackageIntentOpWorker( + val appContext: Context, + params: WorkerParameters +) : CoroutineWorker(appContext, params) { + + companion object { + private const val TAG = "PackageIntentOpWorker" + const val PACKAGE_NAME = "packageName" + } + + override suspend fun doWork(): Result { + val packageName = inputData.getString(PACKAGE_NAME) ?: return Result.failure() + Log.d(TAG, "doWork: $packageName clearing.") + + clearGcmData(packageName) + clearAuthInfo(packageName) + + Log.d(TAG, "doWork: $packageName cleared.") + return Result.success() + } + + private suspend fun clearGcmData(packageName: String) = withContext(Dispatchers.IO) { + val database = GcmDatabase(appContext) + val app = database.getApp(packageName) + if (app != null) { + val registrations = database.getRegistrationsByApp(packageName) + var deletedAll = true + for (registration in registrations) { + deletedAll = deletedAll and (PushRegisterManager.unregister(appContext, registration.packageName, registration.signature, null, null).deleted != null) + } + if (deletedAll) { + database.removeApp(packageName) + } + database.close() + } else { + database.close() + } + } + + private suspend fun clearAuthInfo(packageName: String) = withContext(Dispatchers.IO) { + val authOptions = SignInConfigurationService.getAuthOptions(appContext, packageName) + val authAccount = SignInConfigurationService.getDefaultAccount(appContext, packageName) + if (authOptions.isNotEmpty() && authAccount != null) { + authOptions.forEach { + Log.d(TAG, "$packageName:clear authAccount: ${authAccount.name} authOption:($it)") + performSignOut(appContext, packageName, it, authAccount) + } + } + SignInConfigurationService.setAuthInfo(appContext, packageName, null, null) + AccountUtils.get(appContext).removeSelectedAccount(packageName) + } +} \ No newline at end of file diff --git a/play-services-core/src/main/kotlin/org/microg/gms/common/PersistentTrustedReceiver.kt b/play-services-core/src/main/kotlin/org/microg/gms/common/PersistentTrustedReceiver.kt new file mode 100644 index 0000000000..62631912ba --- /dev/null +++ b/play-services-core/src/main/kotlin/org/microg/gms/common/PersistentTrustedReceiver.kt @@ -0,0 +1,44 @@ +/** + * SPDX-FileCopyrightText: 2025 microG Project Team + * SPDX-License-Identifier: Apache-2.0 + */ + +package org.microg.gms.common + +import android.content.BroadcastReceiver +import android.content.Context +import android.content.Intent +import android.util.Log +import androidx.work.Data +import androidx.work.OneTimeWorkRequestBuilder +import androidx.work.WorkManager + +class PersistentTrustedReceiver : BroadcastReceiver() { + + companion object { + private const val TAG = "TrustedReceiver" + } + + override fun onReceive(context: Context, intent: Intent?) { + Log.d(TAG, "Package changed: $intent") + val action = intent?.action ?: return + val pkg = intent.data?.schemeSpecificPart ?: return + + if ((Intent.ACTION_PACKAGE_REMOVED.contains(action) + && intent.getBooleanExtra(Intent.EXTRA_DATA_REMOVED, false) + && !intent.getBooleanExtra(Intent.EXTRA_REPLACING, false)) + || Intent.ACTION_PACKAGE_FULLY_REMOVED.contains(action) + || Intent.ACTION_PACKAGE_DATA_CLEARED.contains(action) + ) { + Log.d(TAG, "Package removed or data cleared: $pkg") + val data = Data.Builder() + .putString(PackageIntentOpWorker.PACKAGE_NAME, pkg) + .build() + val request = OneTimeWorkRequestBuilder() + .setInputData(data) + .build() + WorkManager.getInstance(context).enqueue(request) + } + } + +} \ No newline at end of file From 41d01cbd53133ff6c6eaa28b82e4606e51e515ab Mon Sep 17 00:00:00 2001 From: Marvin W Date: Fri, 9 Jan 2026 21:17:10 +0700 Subject: [PATCH 19/19] Add missing action to manifest Fixes bug introduced with #3222 --- play-services-core/src/main/AndroidManifest.xml | 1 + 1 file changed, 1 insertion(+) diff --git a/play-services-core/src/main/AndroidManifest.xml b/play-services-core/src/main/AndroidManifest.xml index e25e14c433..66038da7e2 100644 --- a/play-services-core/src/main/AndroidManifest.xml +++ b/play-services-core/src/main/AndroidManifest.xml @@ -370,6 +370,7 @@ +