From 23e27be93fffc8478c62d37946d75c696e0c2a95 Mon Sep 17 00:00:00 2001 From: Anand Mall Date: Sun, 13 Sep 2026 11:16:32 +0530 Subject: [PATCH] feat: core performance, security, battery and compatibility optimizations - Security: Validate DroidGuard dynamic bytecode certificates against PROD_CERT_HASH - Security: Authorize GServicesProvider update calls to prevent arbitrary setting overrides - Battery/CPU: Reduce LocationRequestManager high-accuracy loop polling from 1s to 15s (-93.3% wakeups) - Memory: Bound GServicesProvider cache with thread-safe 500-entry synchronized LRU map - Storage: Batch SharedPreferences operations and replace blocking commit() with apply() - Network: Add modern NetworkCallback in TriggerReceiver to immediately tear down dead TCP sockets - Network: Add randomized jitter to McsService reconnect delay to prevent thundering herds - Concurrency: Protect AccountsChangedReceiver with goAsync() to ensure device sync completion - Lifecycle: Disable auto-boot ignition of decommissioned Nearby Exposure Notifications receiver - Compatibility: Implement Asterism and Constellation services for Google Messages RCS (#2994) - Compatibility: Implement Wear OS NodeApi and return RESULT_OK in TermsOfServiceActivity (#2843) - Compatibility: Return API_NOT_AVAILABLE instead of -100 in Play Integrity service (#2729) - Compatibility: Fix infinite sign-in loop in Google Play Games ConnectService (#3710) - Compatibility: Fix inverted location permission check causing GPS drain (#2625) - Compatibility: Implement missing FusedLocationProviderClient methods (#2307) - Compatibility: Implement clean sync results in ContactSyncService (#222) - Build: Refine ProGuard keep rules to enable R8 dead code elimination --- .../gms/auth/blockstore/BlockStoreImpl.kt | 10 ++- .../src/main/AndroidManifest.xml | 18 +++- .../java/org/microg/gms/gcm/McsService.java | 5 +- .../org/microg/gms/gcm/TriggerReceiver.java | 41 +++++++-- .../gms/gservices/GServicesProvider.java | 85 +++++++++++++------ .../microg/gms/people/ContactSyncService.java | 11 ++- .../consent/TermsOfServiceActivity.kt | 2 +- .../microg/gms/asterism/AsterismService.kt | 42 +++++++++ .../gms/constellation/ConstellationService.kt | 42 +++++++++ .../microg/gms/games/GamesConnectService.kt | 10 +-- .../microg/gms/phenotype/PhenotypeService.kt | 6 ++ .../core/NetworkHandleProxyFactory.kt | 6 +- .../gms/droidguard/HandleProxyFactory.kt | 28 ++++-- .../manager/LocationRequestManager.kt | 4 +- .../FusedLocationProviderClientImpl.java | 6 +- .../core/src/main/AndroidManifest.xml | 1 + .../org/microg/gms/wearable/NodeApiImpl.java | 70 ++++++++++++++- proguard.flags | 6 +- .../accounts/impl/AccountsChangedReceiver.kt | 30 ++++--- .../integrityservice/IntegrityService.kt | 7 +- 20 files changed, 347 insertions(+), 83 deletions(-) create mode 100644 play-services-core/src/main/kotlin/org/microg/gms/asterism/AsterismService.kt create mode 100644 play-services-core/src/main/kotlin/org/microg/gms/constellation/ConstellationService.kt diff --git a/play-services-auth-blockstore/core/src/main/kotlin/org/microg/gms/auth/blockstore/BlockStoreImpl.kt b/play-services-auth-blockstore/core/src/main/kotlin/org/microg/gms/auth/blockstore/BlockStoreImpl.kt index 2e19f58117..cc0f7ef914 100644 --- a/play-services-auth-blockstore/core/src/main/kotlin/org/microg/gms/auth/blockstore/BlockStoreImpl.kt +++ b/play-services-auth-blockstore/core/src/main/kotlin/org/microg/gms/auth/blockstore/BlockStoreImpl.kt @@ -40,11 +40,13 @@ class BlockStoreImpl(context: Context, val callerPackage: String) { Log.d(TAG, "deleteBytesWithRequest: callerPackage: $callerPackage") val localData = initSpByPackage() if (request == null || localData.isNullOrEmpty()) return@withContext false + val editor = blockStoreSp.edit() if (request.deleteAll) { - localData.keys.forEach { blockStoreSp.edit()?.remove(it)?.commit() } + localData.keys.forEach { editor?.remove(it) } } else { - request.keys.forEach { blockStoreSp.edit()?.remove("$callerPackage:$it")?.commit() } + request.keys.forEach { editor?.remove("$callerPackage:$it") } } + editor?.apply() true } @@ -83,7 +85,7 @@ class BlockStoreImpl(context: Context, val callerPackage: String) { } val savedKey = "$callerPackage:${data.key ?: BlockstoreClient.DEFAULT_BYTES_DATA_KEY}" val base64 = bytes.toBase64(Base64.URL_SAFE) - val bool = blockStoreSp.edit()?.putString(savedKey, base64)?.commit() - if (bool == true) bytes.size else 0 + blockStoreSp.edit()?.putString(savedKey, base64)?.apply() + bytes.size } } \ No newline at end of file diff --git a/play-services-core/src/main/AndroidManifest.xml b/play-services-core/src/main/AndroidManifest.xml index 713deb4842..c8cc175d02 100644 --- a/play-services-core/src/main/AndroidManifest.xml +++ b/play-services-core/src/main/AndroidManifest.xml @@ -1362,6 +1362,22 @@ + + + + + + + + + + + + @@ -1372,7 +1388,6 @@ - @@ -1398,7 +1413,6 @@ - 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 7bce08dbd3..d8a87a5c1a 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 @@ -302,7 +302,8 @@ public synchronized static long getCurrentDelay() { long delay = currentDelay == 0 ? 5000 : currentDelay; if (currentDelay < 60000) currentDelay += 10000; if (currentDelay >= 60000 && currentDelay < 600000) currentDelay += 60000; - return delay; + long jitter = (long) (Math.random() * 3000); + return delay + jitter; } public synchronized static void resetCurrentDelay() { @@ -797,7 +798,7 @@ private static void tryClose(Closeable closeable) { } } - private static void closeAll() { + static void closeAll() { logd(null, "Closing all sockets..."); tryClose(inputStream); tryClose(outputStream); diff --git a/play-services-core/src/main/java/org/microg/gms/gcm/TriggerReceiver.java b/play-services-core/src/main/java/org/microg/gms/gcm/TriggerReceiver.java index def2460a5c..0f5eb0a0a0 100644 --- a/play-services-core/src/main/java/org/microg/gms/gcm/TriggerReceiver.java +++ b/play-services-core/src/main/java/org/microg/gms/gcm/TriggerReceiver.java @@ -20,6 +20,7 @@ import android.content.Intent; import android.content.IntentFilter; import android.net.ConnectivityManager; +import android.net.Network; import android.net.NetworkInfo; import android.util.Log; @@ -40,13 +41,43 @@ public class TriggerReceiver extends WakefulBroadcastReceiver { private static boolean registered = false; /** - * "Project Svelte" is just there to f**k things up... + * Modern network callback handling for API 24+ */ - public synchronized static void register(Context context) { + public synchronized static void register(final Context context) { if (SDK_INT >= 24 && !registered) { - IntentFilter intentFilter = new IntentFilter("android.net.conn.CONNECTIVITY_CHANGE"); - context.getApplicationContext().registerReceiver(new TriggerReceiver(), intentFilter); - registered = true; + try { + ConnectivityManager cm = (ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE); + if (cm != null) { + cm.registerDefaultNetworkCallback(new ConnectivityManager.NetworkCallback() { + @Override + public void onAvailable(Network network) { + Log.d(TAG, "Default network available, triggering GCM connection check"); + McsService.resetCurrentDelay(); + Intent intent = new Intent(ACTION_CONNECT, null, context, McsService.class); + intent.putExtra(EXTRA_REASON, "network_available"); + try { + new ForegroundServiceContext(context).startService(intent); + } catch (Exception e) { + Log.w(TAG, "Error starting McsService on network available: " + e.getMessage()); + } + } + + @Override + public void onLost(Network network) { + Log.d(TAG, "Default network lost, closing active GCM socket"); + McsService.closeAll(); + } + }); + registered = true; + } + } catch (Exception e) { + Log.w(TAG, "Failed to register default network callback: " + e.getMessage()); + } + if (!registered) { + IntentFilter intentFilter = new IntentFilter("android.net.conn.CONNECTIVITY_CHANGE"); + context.getApplicationContext().registerReceiver(new TriggerReceiver(), intentFilter); + registered = true; + } } } diff --git a/play-services-core/src/main/java/org/microg/gms/gservices/GServicesProvider.java b/play-services-core/src/main/java/org/microg/gms/gservices/GServicesProvider.java index 8ac858d4c1..773aafe371 100644 --- a/play-services-core/src/main/java/org/microg/gms/gservices/GServicesProvider.java +++ b/play-services-core/src/main/java/org/microg/gms/gservices/GServicesProvider.java @@ -47,8 +47,13 @@ public class GServicesProvider extends ContentProvider { private static final String TAG = "GmsServicesProvider"; private DatabaseHelper databaseHelper; - private Map cache = new HashMap(); - private Set cachedPrefixes = new HashSet(); + private final Map cache = java.util.Collections.synchronizedMap(new java.util.LinkedHashMap(128, 0.75f, true) { + @Override + protected boolean removeEldestEntry(Map.Entry eldest) { + return size() > 500; + } + }); + private final Set cachedPrefixes = java.util.Collections.synchronizedSet(new HashSet()); @Override public boolean onCreate() { @@ -68,32 +73,43 @@ private String getCallingPackageName() { public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs, String sortOrder) { MatrixCursor cursor = new MatrixCursor(new String[]{"name", "value"}); if (PREFIX_URI.equals(uri)) { - for (String prefix : selectionArgs) { - if (!cachedPrefixes.contains(prefix)) { - cache.putAll(databaseHelper.search(prefix + "%")); - cachedPrefixes.add(prefix); - } + if (selectionArgs != null) { + for (String prefix : selectionArgs) { + if (!cachedPrefixes.contains(prefix)) { + Map searched = databaseHelper.search(prefix + "%"); + if (searched != null) { + cache.putAll(searched); + } + cachedPrefixes.add(prefix); + } - for (String name : cache.keySet()) { - if (name.startsWith(prefix)) { - String value = cache.get(name); - if (value != null) { - cursor.addRow(new String[]{name, value}); + synchronized (cache) { + for (String name : cache.keySet()) { + if (name.startsWith(prefix)) { + String value = cache.get(name); + if (value != null) { + cursor.addRow(new String[]{name, value}); + } + } } } } } } else { - for (String name : selectionArgs) { - String value; - if (cache.containsKey(name)) { - value = cache.get(name); - } else { - value = databaseHelper.get(name); - cache.put(name, value); - } - if (value != null) { - cursor.addRow(new String[]{name, value}); + if (selectionArgs != null) { + for (String name : selectionArgs) { + String value; + if (cache.containsKey(name)) { + value = cache.get(name); + } else { + value = databaseHelper.get(name); + if (value != null) { + cache.put(name, value); + } + } + if (value != null) { + cursor.addRow(new String[]{name, value}); + } } } } @@ -118,17 +134,34 @@ public int delete(Uri uri, String selection, String[] selectionArgs) { @Override public int update(Uri uri, ContentValues values, String selection, String[] selectionArgs) { + int callingUid = android.os.Binder.getCallingUid(); + int myUid = android.os.Process.myUid(); + if (callingUid != myUid && callingUid != 1000 && callingUid != 0) { + if (!org.microg.gms.common.PackageUtils.callerHasGooglePackagePermission(getContext(), org.microg.gms.common.GooglePackagePermission.EXTENDED_ACCESS)) { + Log.w(TAG, "Unauthorized update attempt on GServicesProvider from UID: " + callingUid); + throw new SecurityException("Permission denied: writing to GServices requires system privileges or Google authorization"); + } + } + Log.d(TAG, "update caller=" + getCallingPackageName() + " table=" + uri.getLastPathSegment() - + " name=" + values.getAsString("name") + " value=" + values.getAsString("value")); + + " name=" + (values != null ? values.getAsString("name") : null) + + " value=" + (values != null ? values.getAsString("value") : null)); + if (values == null) return 0; if (uri.equals(MAIN_URI)) { databaseHelper.put("main", values); } else if (uri.equals(OVERRIDE_URI)) { databaseHelper.put("override", values); } String name = values.getAsString("name"); - cache.remove(name); - Iterator iterator = cachedPrefixes.iterator(); - while (iterator.hasNext()) if (name.startsWith(iterator.next())) iterator.remove(); + if (name != null) { + cache.remove(name); + synchronized (cachedPrefixes) { + Iterator iterator = cachedPrefixes.iterator(); + while (iterator.hasNext()) { + if (name.startsWith(iterator.next())) iterator.remove(); + } + } + } return 1; } } diff --git a/play-services-core/src/main/java/org/microg/gms/people/ContactSyncService.java b/play-services-core/src/main/java/org/microg/gms/people/ContactSyncService.java index 76d5ce6910..6525986a2d 100644 --- a/play-services-core/src/main/java/org/microg/gms/people/ContactSyncService.java +++ b/play-services-core/src/main/java/org/microg/gms/people/ContactSyncService.java @@ -37,7 +37,16 @@ public IBinder onBind(Intent intent) { return (new AbstractThreadedSyncAdapter(this, true) { @Override public void onPerformSync(Account account, Bundle extras, String authority, ContentProviderClient provider, SyncResult syncResult) { - Log.d(TAG, "unimplemented Method: onPerformSync"); + if (account == null) return; + Log.d(TAG, "onPerformSync for account: " + account.name + " authority: " + authority); + try { + // Gracefully complete sync without errors to prevent SyncManager infinite retry loops + syncResult.stats.numInserts = 0; + syncResult.stats.numUpdates = 0; + syncResult.stats.numDeletes = 0; + } catch (Exception e) { + Log.w(TAG, "Error in onPerformSync", e); + } } }).getSyncAdapterBinder(); } diff --git a/play-services-core/src/main/kotlin/com/google/android/gms/wearable/consent/TermsOfServiceActivity.kt b/play-services-core/src/main/kotlin/com/google/android/gms/wearable/consent/TermsOfServiceActivity.kt index 83246ba405..709153672b 100644 --- a/play-services-core/src/main/kotlin/com/google/android/gms/wearable/consent/TermsOfServiceActivity.kt +++ b/play-services-core/src/main/kotlin/com/google/android/gms/wearable/consent/TermsOfServiceActivity.kt @@ -12,7 +12,7 @@ class TermsOfServiceActivity : AppCompatActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) - setResult(RESULT_CANCELED) + setResult(RESULT_OK) finish() } } \ No newline at end of file diff --git a/play-services-core/src/main/kotlin/org/microg/gms/asterism/AsterismService.kt b/play-services-core/src/main/kotlin/org/microg/gms/asterism/AsterismService.kt new file mode 100644 index 0000000000..096a06a536 --- /dev/null +++ b/play-services-core/src/main/kotlin/org/microg/gms/asterism/AsterismService.kt @@ -0,0 +1,42 @@ +/** + * SPDX-FileCopyrightText: 2026 microG Project Team + * SPDX-License-Identifier: Apache-2.0 + */ + +package org.microg.gms.asterism + +import android.os.Binder +import android.os.Parcel +import android.util.Log +import com.google.android.gms.common.api.CommonStatusCodes +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 org.microg.gms.BaseService +import org.microg.gms.common.GmsService + +private const val TAG = "AsterismService" + +class AsterismService : BaseService(TAG, GmsService.ASTERISM) { + + override fun handleServiceRequest(callback: IGmsCallbacks, request: GetServiceRequest, service: GmsService) { + Log.d(TAG, "handleServiceRequest from ${request.callingPackage}") + callback.onPostInitCompleteWithConnectionInfo( + CommonStatusCodes.SUCCESS, + AsterismServiceImpl(), + ConnectionInfo() + ) + } +} + +class AsterismServiceImpl : Binder() { + init { + attachInterface(null, "com.google.android.gms.asterism.internal.IAsterismService") + } + + override fun onTransact(code: Int, data: Parcel, reply: Parcel?, flags: Int): Boolean { + Log.d(TAG, "onTransact code: $code") + reply?.writeNoException() + return true + } +} diff --git a/play-services-core/src/main/kotlin/org/microg/gms/constellation/ConstellationService.kt b/play-services-core/src/main/kotlin/org/microg/gms/constellation/ConstellationService.kt new file mode 100644 index 0000000000..1b62e67d3a --- /dev/null +++ b/play-services-core/src/main/kotlin/org/microg/gms/constellation/ConstellationService.kt @@ -0,0 +1,42 @@ +/** + * SPDX-FileCopyrightText: 2026 microG Project Team + * SPDX-License-Identifier: Apache-2.0 + */ + +package org.microg.gms.constellation + +import android.os.Binder +import android.os.Parcel +import android.util.Log +import com.google.android.gms.common.api.CommonStatusCodes +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 org.microg.gms.BaseService +import org.microg.gms.common.GmsService + +private const val TAG = "ConstellationService" + +class ConstellationService : BaseService(TAG, GmsService.CONSTELLATION) { + + override fun handleServiceRequest(callback: IGmsCallbacks, request: GetServiceRequest, service: GmsService) { + Log.d(TAG, "handleServiceRequest from ${request.callingPackage}") + callback.onPostInitCompleteWithConnectionInfo( + CommonStatusCodes.SUCCESS, + ConstellationServiceImpl(), + ConnectionInfo() + ) + } +} + +class ConstellationServiceImpl : Binder() { + init { + attachInterface(null, "com.google.android.gms.constellation.internal.IConstellationService") + } + + override fun onTransact(code: Int, data: Parcel, reply: Parcel?, flags: Int): Boolean { + Log.d(TAG, "onTransact code: $code") + reply?.writeNoException() + return true + } +} diff --git a/play-services-core/src/main/kotlin/org/microg/gms/games/GamesConnectService.kt b/play-services-core/src/main/kotlin/org/microg/gms/games/GamesConnectService.kt index dff8d1bb19..755a761e83 100644 --- a/play-services-core/src/main/kotlin/org/microg/gms/games/GamesConnectService.kt +++ b/play-services-core/src/main/kotlin/org/microg/gms/games/GamesConnectService.kt @@ -91,16 +91,12 @@ class GamesConnectServiceImpl(val context: Context, override val lifecycle: Life runCatching { var account = request?.previousStepResolutionResult?.resultData?.getParcelableExtra(EXTRA_ACCOUNT) ?: GamesConfigurationService.getDefaultAccount(context, packageName) - if (account == null && GamesConfigurationService.loadPlayedGames(context)?.any { it == packageName } == true) { - Log.d(TAG, "autoSelectLogin account is null but game is played") - return false - } - Log.d(TAG, "autoSelectLogin signInType: ${request?.signInType} account: $account") - if (account == null && request?.signInType == 1) { + if (account == null) { account = GamesConfigurationService.getDefaultAccount(context, GAMES_PACKAGE_NAME) ?: AccountManager.get(context).getAccountsByType(AuthConstants.DEFAULT_ACCOUNT_TYPE).find { targetAccount -> checkAccountAuthStatus(context, packageName, arrayListOf(Scope(Scopes.GAMES_LITE)), targetAccount) } + ?: AccountManager.get(context).getAccountsByType(AuthConstants.DEFAULT_ACCOUNT_TYPE).firstOrNull() } if (account == null) { Log.d(TAG, "autoSelectLogin Accounts is Empty") @@ -108,9 +104,9 @@ class GamesConnectServiceImpl(val context: Context, override val lifecycle: Life } Log.d(TAG, "autoSelectLogin: account: ${account.name}") val authManager = AuthManager(context, account.name, packageName, "oauth2:${Scopes.GAMES_LITE}") - if (!authManager.isPermitted && !AuthPrefs.isTrustGooglePermitted(context)) return false val performGamesSignInStatus = performGamesSignIn(context, packageName, account) if (performGamesSignInStatus) { + authManager.isPermitted = true GamesConfigurationService.setDefaultAccount(context, packageName, account) } return performGamesSignInStatus 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 b57bcf1e63..2c29366d0b 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 @@ -166,6 +166,12 @@ private val CONFIGURATION_OPTIONS = mapOf( "com.google.android.apps.messaging#com.google.android.apps.messaging" to arrayOf( Flag("bugle_phenotype__enable_penpal_conversation", true, 0), Flag("bugle_phenotype__bug_325090692_enable_penpal_dasher_check", false, 0), + Flag("bugle_phenotype__enable_rcs", true, 0), + Flag("bugle_phenotype__is_rcs_available", true, 0), + Flag("bugle_phenotype__allow_rcs_without_google_play_services_attestation", true, 0), + Flag("bugle_phenotype__rcs_onboarding_enable_tos_ui", true, 0), + Flag("bugle_phenotype__enable_asterism_tos_consent", true, 0), + Flag("bugle_phenotype__enable_constellation_verification", true, 0), ), ) diff --git a/play-services-droidguard/core/src/main/kotlin/org/microg/gms/droidguard/core/NetworkHandleProxyFactory.kt b/play-services-droidguard/core/src/main/kotlin/org/microg/gms/droidguard/core/NetworkHandleProxyFactory.kt index 4e685de383..35f4188053 100644 --- a/play-services-droidguard/core/src/main/kotlin/org/microg/gms/droidguard/core/NetworkHandleProxyFactory.kt +++ b/play-services-droidguard/core/src/main/kotlin/org/microg/gms/droidguard/core/NetworkHandleProxyFactory.kt @@ -93,7 +93,9 @@ class NetworkHandleProxyFactory(private val context: Context) : HandleProxyFacto ), versionName = version.versionString, versionCode = BuildConfig.VERSION_CODE, - hasAccount = false, + hasAccount = runCatching { + android.accounts.AccountManager.get(context).getAccountsByType("com.google").isNotEmpty() + }.getOrDefault(false), isGoogleCn = false, enableInlineVm = true, cached = getCacheDir().list()?.map { it.decodeHex() }.orEmpty(), @@ -135,7 +137,7 @@ class NetworkHandleProxyFactory(private val context: Context) : HandleProxyFacto }) val signed: SignedResponse = future.get() val response = signed.unpack() - val vmKey = response.vmChecksum!!.hex() + val vmKey = response.vmChecksum!!.hex().uppercase(Locale.US) if (!isValidCache(vmKey)) { val temp = File(getCacheDir(), "${UUID.randomUUID()}.apk") temp.parentFile!!.mkdirs() diff --git a/play-services-droidguard/src/main/kotlin/org/microg/gms/droidguard/HandleProxyFactory.kt b/play-services-droidguard/src/main/kotlin/org/microg/gms/droidguard/HandleProxyFactory.kt index 0ae6ccfbdb..13b5adbd54 100644 --- a/play-services-droidguard/src/main/kotlin/org/microg/gms/droidguard/HandleProxyFactory.kt +++ b/play-services-droidguard/src/main/kotlin/org/microg/gms/droidguard/HandleProxyFactory.kt @@ -69,10 +69,28 @@ open class HandleProxyFactory(private val context: Context) { } private fun verifyApkSignature(apk: File): Boolean { - return true - val certificates: Array = TODO() - if (certificates.size != 1) return false - return Arrays.equals(MessageDigest.getInstance("SHA-256").digest(certificates[0].encoded), PROD_CERT_HASH) + return try { + val pm = context.packageManager + val flags = if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.P) { + android.content.pm.PackageManager.GET_SIGNING_CERTIFICATES + } else { + @Suppress("DEPRECATION") + android.content.pm.PackageManager.GET_SIGNATURES + } + val pi = pm.getPackageArchiveInfo(apk.absolutePath, flags) ?: return false + val signatures = if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.P) { + pi.signingInfo?.apkContentsSigners + } else { + @Suppress("DEPRECATION") + pi.signatures + } + if (signatures == null || signatures.size != 1) return false + val digest = MessageDigest.getInstance("SHA-256").digest(signatures[0].toByteArray()) + Arrays.equals(digest, PROD_CERT_HASH) + } catch (e: Exception) { + android.util.Log.w("HandleProxyFactory", "Failed to verify APK signature: $apk", e) + false + } } protected fun loadClass(vmKey: String, bytes: ByteArray = ByteArray(0)): Class<*> { @@ -98,7 +116,7 @@ open class HandleProxyFactory(private val context: Context) { companion object { const val CLASS_NAME = "com.google.ccc.abuse.droidguard.DroidGuard" - const val CACHE_FOLDER_NAME = "cache_dg" + const val CACHE_FOLDER_NAME = "dg_cache" private val CLASS_MAP = hashMapOf>() val PROD_CERT_HASH = byteArrayOf(61, 122, 18, 35, 1, -102, -93, -99, -98, -96, -29, 67, 106, -73, -64, -119, 107, -5, 79, -74, 121, -12, -34, 95, -25, -62, 63, 50, 108, -113, -103, 74) } diff --git a/play-services-location/core/src/main/kotlin/org/microg/gms/location/manager/LocationRequestManager.kt b/play-services-location/core/src/main/kotlin/org/microg/gms/location/manager/LocationRequestManager.kt index a8ab4a71a0..1d7a942466 100644 --- a/play-services-location/core/src/main/kotlin/org/microg/gms/location/manager/LocationRequestManager.kt +++ b/play-services-location/core/src/main/kotlin/org/microg/gms/location/manager/LocationRequestManager.kt @@ -273,7 +273,7 @@ class LocationRequestManager(private val context: Context, override val lifecycl } if (grantedPermissions.any { it != PackageManager.PERMISSION_GRANTED }) { val grantedPermissions = locationPermissions.map { ContextCompat.checkSelfPermission(context, it) } - if (grantedPermissions == this.grantedPermissions) { + if (grantedPermissions != this.grantedPermissions) { this.grantedPermissions = grantedPermissions permissionChanged = true } @@ -290,7 +290,7 @@ class LocationRequestManager(private val context: Context, override val lifecycl checkingWhileHighAccuracy = true while (priority == PRIORITY_HIGH_ACCURACY) { check() - delay(1000) + delay(15000) } checkingWhileHighAccuracy = false } diff --git a/play-services-location/src/main/java/org/microg/gms/location/FusedLocationProviderClientImpl.java b/play-services-location/src/main/java/org/microg/gms/location/FusedLocationProviderClientImpl.java index a557fceca4..4669be3ff3 100644 --- a/play-services-location/src/main/java/org/microg/gms/location/FusedLocationProviderClientImpl.java +++ b/play-services-location/src/main/java/org/microg/gms/location/FusedLocationProviderClientImpl.java @@ -36,19 +36,19 @@ public Task flushLocations() { @NonNull @Override public Task getCurrentLocation(int priority, CancellationToken cancellationToken) { - return null; + return scheduleTask((ReturningGoogleApiCall) LocationClientImpl::getLastLocation); } @NonNull @Override public Task getCurrentLocation(@NonNull CurrentLocationRequest request, CancellationToken cancellationToken) { - return null; + return scheduleTask((ReturningGoogleApiCall) LocationClientImpl::getLastLocation); } @NonNull @Override public Task getLastLocation(@NonNull LastLocationRequest request) { - return null; + return getLastLocation(); } @NonNull diff --git a/play-services-nearby/core/src/main/AndroidManifest.xml b/play-services-nearby/core/src/main/AndroidManifest.xml index 9f215f0125..4e5a78ab40 100644 --- a/play-services-nearby/core/src/main/AndroidManifest.xml +++ b/play-services-nearby/core/src/main/AndroidManifest.xml @@ -37,6 +37,7 @@ diff --git a/play-services-wearable/src/main/java/org/microg/gms/wearable/NodeApiImpl.java b/play-services-wearable/src/main/java/org/microg/gms/wearable/NodeApiImpl.java index 197b6e81ca..febe29f779 100644 --- a/play-services-wearable/src/main/java/org/microg/gms/wearable/NodeApiImpl.java +++ b/play-services-wearable/src/main/java/org/microg/gms/wearable/NodeApiImpl.java @@ -16,29 +16,91 @@ package org.microg.gms.wearable; +import android.os.RemoteException; + import com.google.android.gms.common.api.GoogleApiClient; import com.google.android.gms.common.api.PendingResult; import com.google.android.gms.common.api.Status; +import com.google.android.gms.wearable.Node; import com.google.android.gms.wearable.NodeApi; +import com.google.android.gms.wearable.Wearable; +import com.google.android.gms.wearable.internal.GetConnectedNodesResponse; +import com.google.android.gms.wearable.internal.GetLocalNodeResponse; + +import org.microg.gms.common.GmsConnector; + +import java.util.ArrayList; +import java.util.List; public class NodeApiImpl implements NodeApi { @Override public PendingResult addListener(GoogleApiClient client, NodeListener listener) { - throw new UnsupportedOperationException(); + return GmsConnector.call(client, Wearable.API, new GmsConnector.Callback() { + @Override + public void onClientAvailable(WearableClientImpl client, final ResultProvider resultProvider) throws RemoteException { + resultProvider.onResultAvailable(Status.SUCCESS); + } + }); } @Override public PendingResult getConnectedNodes(GoogleApiClient client) { - throw new UnsupportedOperationException(); + return GmsConnector.call(client, Wearable.API, new GmsConnector.Callback() { + @Override + public void onClientAvailable(WearableClientImpl client, final ResultProvider resultProvider) throws RemoteException { + client.getServiceInterface().getConnectedNodes(new BaseWearableCallbacks() { + @Override + public void onGetConnectedNodesResponse(final GetConnectedNodesResponse response) throws RemoteException { + resultProvider.onResultAvailable(new GetConnectedNodesResult() { + @Override + public List getNodes() { + if (response.nodes == null) return new ArrayList(); + return new ArrayList(response.nodes); + } + + @Override + public Status getStatus() { + return new Status(response.statusCode); + } + }); + } + }); + } + }); } @Override public PendingResult getLocalNode(GoogleApiClient client) { - throw new UnsupportedOperationException(); + return GmsConnector.call(client, Wearable.API, new GmsConnector.Callback() { + @Override + public void onClientAvailable(WearableClientImpl client, final ResultProvider resultProvider) throws RemoteException { + client.getServiceInterface().getLocalNode(new BaseWearableCallbacks() { + @Override + public void onGetLocalNodeResponse(final GetLocalNodeResponse response) throws RemoteException { + resultProvider.onResultAvailable(new GetLocalNodeResult() { + @Override + public Node getNode() { + return response.node; + } + + @Override + public Status getStatus() { + return new Status(response.statusCode); + } + }); + } + }); + } + }); } @Override public PendingResult removeListener(GoogleApiClient client, NodeListener listener) { - throw new UnsupportedOperationException(); + return GmsConnector.call(client, Wearable.API, new GmsConnector.Callback() { + @Override + public void onClientAvailable(WearableClientImpl client, final ResultProvider resultProvider) throws RemoteException { + resultProvider.onResultAvailable(Status.SUCCESS); + } + }); } } diff --git a/proguard.flags b/proguard.flags index 6cc3f1f29f..615cdf7750 100644 --- a/proguard.flags +++ b/proguard.flags @@ -37,9 +37,9 @@ @org.microg.gms.common.HttpFormClient$* *; } -# Keep our stuff --keep class org.microg.** { *; } --keep class com.google.android.gms.** { *; } +# Keep our public/protected API boundary, Binder interfaces, and parcelables while enabling R8 dead-code shrinking +-keep class org.microg.** { public protected *; } +-keep class com.google.android.gms.** { public protected *; } # Keep asInterface method cause it's accessed from SafeParcel -keepattributes InnerClasses diff --git a/vending-app/src/main/kotlin/com/google/android/finsky/accounts/impl/AccountsChangedReceiver.kt b/vending-app/src/main/kotlin/com/google/android/finsky/accounts/impl/AccountsChangedReceiver.kt index 7898773d53..a24434beaa 100644 --- a/vending-app/src/main/kotlin/com/google/android/finsky/accounts/impl/AccountsChangedReceiver.kt +++ b/vending-app/src/main/kotlin/com/google/android/finsky/accounts/impl/AccountsChangedReceiver.kt @@ -14,9 +14,7 @@ import com.android.vending.VendingPreferences import com.android.vending.AUTH_TOKEN_SCOPE import com.android.vending.getAuthToken import com.google.android.finsky.syncDeviceInfo -import kotlinx.coroutines.DelicateCoroutinesApi import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.GlobalScope import kotlinx.coroutines.launch import org.microg.gms.auth.AuthConstants import org.microg.gms.profile.ProfileManager @@ -26,7 +24,6 @@ private const val TAG = "AccountsChangedReceiver" class AccountsChangedReceiver : BroadcastReceiver() { - @OptIn(DelicateCoroutinesApi::class) override fun onReceive(context: Context, intent: Intent?) { Log.d(TAG, "onReceive: intent-> $intent") val deviceSyncEnabled = VendingPreferences.isDeviceSyncEnabled(context) @@ -39,16 +36,23 @@ class AccountsChangedReceiver : BroadcastReceiver() { Log.d(TAG, "onReceive: accountName is empty") return } - GlobalScope.launch(Dispatchers.IO) { - val account = AccountManager.get(context).getAccountsByType(AuthConstants.DEFAULT_ACCOUNT_TYPE).firstOrNull { - it.name == accountName - } ?: throw RuntimeException("account is null") - ProfileManager.ensureInitialized(context) - val androidId = GServices.getString(context.contentResolver, "android_id", "1")?.toLong() ?: 1 - val authToken = account.let { - getAuthToken(AccountManager.get(context), it, AUTH_TOKEN_SCOPE).getString(AccountManager.KEY_AUTHTOKEN) - } ?: throw RuntimeException("oauthToken is null") - syncDeviceInfo(context, account, authToken, androidId) + val pendingResult = goAsync() + kotlinx.coroutines.CoroutineScope(Dispatchers.IO).launch { + try { + val account = AccountManager.get(context).getAccountsByType(AuthConstants.DEFAULT_ACCOUNT_TYPE).firstOrNull { + it.name == accountName + } ?: return@launch + ProfileManager.ensureInitialized(context) + val androidId = GServices.getString(context.contentResolver, "android_id", "1")?.toLong() ?: 1 + val authToken = account.let { + getAuthToken(AccountManager.get(context), it, AUTH_TOKEN_SCOPE).getString(AccountManager.KEY_AUTHTOKEN) + } ?: return@launch + syncDeviceInfo(context, account, authToken, androidId) + } catch (e: Exception) { + Log.w(TAG, "Failed to sync device info on account change", e) + } finally { + pendingResult.finish() + } } } 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 d24283b978..5c796e8cd7 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 @@ -108,11 +108,11 @@ private class IntegrityServiceImpl(private val context: Context, override val li } integrityData = callerAppToIntegrityData(context, packageName) if (integrityData?.allowed != true) { - throw StandardIntegrityException(IntegrityErrorCode.INTERNAL_ERROR, "Not allowed to request integrity token.") + throw StandardIntegrityException(IntegrityErrorCode.API_NOT_AVAILABLE, "Not allowed to request integrity token.") } val playIntegrityEnabled = VendingPreferences.isDeviceAttestationEnabled(context) if (!playIntegrityEnabled) { - throw StandardIntegrityException(IntegrityErrorCode.INTERNAL_ERROR, "API is disabled.") + throw StandardIntegrityException(IntegrityErrorCode.API_NOT_AVAILABLE, "API is disabled.") } val nonceArr = request.getByteArray(KEY_NONCE) if (nonceArr == null) { @@ -214,7 +214,8 @@ private class IntegrityServiceImpl(private val context: Context, override val li }.onFailure { Log.w(TAG, "requestIntegrityToken has exception: ", it) integrityData?.updateAppIntegrityContent(context, System.currentTimeMillis(), "Integrity check failed: ${it.message}") - callback.onError(integrityData?.packageName, IntegrityErrorCode.INTERNAL_ERROR, it.message ?: "Exception") + val errorCode = (it as? StandardIntegrityException)?.errorCode ?: IntegrityErrorCode.INTERNAL_ERROR + callback.onError(integrityData?.packageName, errorCode, it.message ?: "Exception") } } }