Skip to content

Conversation

@SBALAVIGNESH123
Copy link
Owner

@SBALAVIGNESH123 SBALAVIGNESH123 commented Dec 11, 2025

This pull request adds full Bluetooth transport support for WearOS devices in MicroG, enabling basic wearable functionality without relying on Google Play Services. It introduces a new BluetoothWearableConnection class that implements a Bluetooth Classic RFCOMM transport using the standard SPP UUID and handles length-prefixed protobuf messages. The WearableImpl has been updated with a background ConnectionThread that automatically scans bonded devices at startup, attempts to connect using the new Bluetooth transport, and includes a 10-second reconnection loop while preserving existing TCP/IP handling. The Android manifest now includes the required Bluetooth permissions, including the newer Android 12+ BLUETOOTH_SCAN and BLUETOOTH_CONNECT. In testing, the connection successfully paired with a Pixel Watch and logged stable Bluetooth sessions, working alongside the TCP mechanism. Current limitations include broad connection attempts across all bonded devices and reliance on the general SPP UUID, though this foundation can be expanded with device-specific UUID detection, improved state management, and UI integration. This work closes issue microg#2843, relates to the long-standing issue microg#4, and fulfills the bounty request for foundational WearOS functionality such as notification sync, app communication, and media control within the existing MicroG wearable framework.


Note

  • WearOS Bluetooth transport: New BluetoothWearableConnection (RFCOMM SPP) and connection thread in WearableImpl; manifest gains BLUETOOTH_CONNECT; settings UI lists bonded devices; robust deserialization and permission checks added.
  • Identity API enhancements: IAuthorizationService adds revokeAccess/clearToken; implementation persists per-app account via new AccountUtils, returns access/id tokens appropriately, and supports token invalidation.
  • Package cleanup + GCM updates: Replace UnregisterReceiver with trusted receiver + PackageIntentOpWorker (WorkManager) to clear GCM/auth data on package changes; GCM broadcast action renamed to CONNECTED and flow adjusted; UI triggers updated.
  • Play Integrity controls: New PLAY_INTEGRITY_APP_LIST setting, PlayIntegrityData model, and UI in SafetyNet (renamed to Device Attestation) to view/toggle per-app access; vending-side services track/update visit status; integrity/express services enforce enablement and per-app allow list.
  • Vending services: Add in-app review service/activity returning a confirmation intent; asset module service gains thread-safety and session merging; dev-triggered update reports availability.
  • Location/Maps fixes: Safer Wi‑Fi info handling, telephony-aware Ichnaea requests, lifecycle/parcelable and projection/UI settings refinements.
  • Build/infra: Actions checkout v6; add WorkManager (work-runtime-ktx) dependency and version; minor string/localization updates.

Written by Cursor Bugbot for commit 7f8a5d0. This will update automatically on new commits. Configure here.

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](actions/checkout@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] <support@github.com>
Copy link

@cursor cursor bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This PR is being reviewed by Cursor Bugbot

Details

You are on the Bugbot Free tier. On this plan, Bugbot will review limited PRs each billing cycle.

To receive Bugbot reviews on all of your PRs, visit the Cursor dashboard to activate Pro and start your 14-day free trial.

@SBALAVIGNESH123 SBALAVIGNESH123 force-pushed the wearos-bluetooth-support branch from cbe08fb to ef58386 Compare December 11, 2025 01:03
}
}
}
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Bug: Race condition with unsynchronized activeConnections map access

The new ConnectionThread synchronizes on activeConnections when checking for existing connections, but the onConnectReceived method (called via the onMessage callback at line 711) modifies activeConnections without synchronization. Since HashMap is not thread-safe, concurrent access from the Bluetooth connection thread and the message callback thread can cause ConcurrentModificationException or data corruption.

Additional Locations (1)

Fix in Cursor Fix in Web

@Override
public void onDisconnected() {
// Cleanup handled by existing logic
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Bug: Missing disconnect handler leaves Bluetooth connections orphaned

The onDisconnected callback is empty with only a comment claiming "Cleanup handled by existing logic." However, the TCP equivalent in MessageHandler.onDisconnected() explicitly calls wearable.onDisconnectReceived() to remove the connection from activeConnections and update state. Without this call, disconnected Bluetooth connections remain in activeConnections, preventing reconnection attempts and causing stale state.

Fix in Cursor Fix in Web

byte[] bytes = piece.toByteArray();
os.writeInt(bytes.length);
os.write(bytes);
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Bug: Missing flush after writing to DataOutputStream

The writeMessagePiece method writes to a DataOutputStream but never calls flush(). Data written to DataOutputStream may be buffered internally, so without an explicit flush, messages may not be sent immediately over the Bluetooth socket. This can cause delayed or missing protocol messages, particularly problematic for the initial handshake.

Fix in Cursor Fix in Web

Copy link

@cursor cursor bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Bug: Null pointer exception when closing Bluetooth connections

The closeConnection method accesses sct.getWearableConnection() without a null check on sct. The sct variable is only initialized when enableConnection("server") is called. With the new Bluetooth implementation, connections can be created and closed via onConnectReceived and closeConnection without sct ever being set, causing a NullPointerException when closing any Bluetooth-based connection.

play-services-wearable/core/src/main/java/org/microg/gms/wearable/WearableImpl.java#L597-L598

}
if (connection == sct.getWearableConnection()) {

Fix in Cursor Fix in Web


@SBALAVIGNESH123 SBALAVIGNESH123 force-pushed the wearos-bluetooth-support branch from 33d5ff3 to 7b4047c Compare December 11, 2025 02:01
@SBALAVIGNESH123 SBALAVIGNESH123 force-pushed the wearos-bluetooth-support branch from 76640d3 to ece85ec Compare December 11, 2025 02:11
super("GmsWearSvc", GmsService.WEAR);
}

public static WearableImpl impl;
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Bug: Static impl field creates thread-safety issues

The static impl field in WearableService is accessed by WearableSettingsActivity without synchronization. Since the service's onCreate and onDestroy can be called on different threads than UI access from the activity, this creates a race condition. The activity could read a non-null reference that becomes null immediately after, or use a stale WearableImpl instance that has been stopped.

Additional Locations (1)

Fix in Cursor Fix in Web

@SBALAVIGNESH123 SBALAVIGNESH123 force-pushed the wearos-bluetooth-support branch from c57d56c to ff4c1a9 Compare December 11, 2025 02:33
@SBALAVIGNESH123 SBALAVIGNESH123 force-pushed the wearos-bluetooth-support branch 2 times, most recently from 7851adb to 1c4933f Compare December 11, 2025 02:46
@SBALAVIGNESH123 SBALAVIGNESH123 force-pushed the wearos-bluetooth-support branch from a19d816 to 882fa2f Compare December 11, 2025 17:38
@SBALAVIGNESH123 SBALAVIGNESH123 force-pushed the wearos-bluetooth-support branch 2 times, most recently from f35a68b to 70caf33 Compare December 13, 2025 13:29
SBALAVIGNESH123 and others added 7 commits December 15, 2025 17:51
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.
…GNESH123/GmsCore into wearos-bluetooth-support

# Please enter a commit message to explain why this merge is necessary,
# especially if it merges an updated upstream into a topic branch.
#
# Lines starting with '#' will be ignored, and an empty message aborts
# the commit.
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.
@deadYokai
Copy link

deadYokai commented Dec 22, 2025

adopted Wearable lib to Wire 4.9.9, but i unable to test it, cuz i dont have compatable device

https://github.com/deadYokai/Wearable/tree/wire_upd

@SBALAVIGNESH123
Copy link
Owner Author

Thanks @deadYokai for the Wire 4.9.9 update! 🙏

I really appreciate you taking the time to adapt the Wearable library. This will be important for keeping everything compatible with the latest protobuf changes.

I don't have a physical WearOS device for testing either at the moment. Would you like me to integrate your changes from your 'wire_upd' branch into this PR? Or would you prefer to submit them separately?

Happy to collaborate on getting this working together!

@deadYokai
Copy link

deadYokai commented Dec 23, 2025

@SBALAVIGNESH123 i already submited PR to microg/Wearable#3 , i'm waiting for approving

You can intergrate changes, but as far as PR not merged, can't be compatable with current version of GmsCore / Wearable

And also, i'm trying to implement BluetoothGatt protocol, as far i know it's used by WearOS 3+ devices

@deadYokai
Copy link

found UUID in google's gms

bluetoothSocketCreateRfcommSocketToServiceRecord = this.h.createRfcommSocketToServiceRecord(UUID.fromString("5e8945b0-9525-11e3-a5e2-0800200c9a66"));

maybe i make own implementation of bluetooth stuff?

@SBALAVIGNESH123
Copy link
Owner Author

Hey @deadYokai, yes that would be great!
Go ahead with your own implementation of the BluetoothGatt protocol. The UUID you found (5e8945b0-9525-11e3-a5e2-0800200c9a66) is the right one for the RFCOMM socket.

Copy link

@cursor cursor bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is the final PR Bugbot will review for you during this billing cycle

Your free Bugbot reviews will reset on February 19

Details

You are on the Bugbot Free tier. On this plan, Bugbot will review limited PRs each billing cycle.

To receive Bugbot reviews on all of your PRs, visit the Cursor dashboard to activate Pro and start your 14-day free trial.

if (SDK_INT >= 31 && connectionInfo != null) {
onWifiDetailsAvailable(listOf(connectionInfo.toWifiDetails()))
if (SDK_INT >= 31 && connectionInfo != null && connectionInfo.toWifiDetails() != null) {
onWifiDetailsAvailable(listOfNotNull(connectionInfo.toWifiDetails()))
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Double call to nullable function creates race condition

The toWifiDetails() function is called twice on the same connectionInfo object - once in the condition check and again in the body. Since WifiInfo.toWifiDetails() can return null when bssid is null, and WiFi state can change between calls, there's a time-of-check to time-of-use issue. If the bssid becomes null between the two calls, the second call returns null, resulting in listOfNotNull producing an empty list and onWifiDetailsAvailable silently doing nothing. The result should be captured in a variable to ensure atomic evaluation.

Fix in Cursor Fix in Web

.name("Phone")
.networkId(localId)
.peerAndroidId(localId)
.peerAndroidId(0L)
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Bluetooth handshake sends invalid Android ID

The manual Connect message sent during Bluetooth handshake uses peerAndroidId(0L) instead of the actual device Android ID. This contradicts how MessageHandler is configured, which retrieves the real Android ID from device settings via SettingsContract.CheckIn.ANDROID_ID. Sending a hardcoded zero for the Android ID could cause WearOS devices to reject the connection or fail to properly identify the phone during pairing. The handshake should use the same Android ID that MessageHandler uses for consistency.

Fix in Cursor Fix in Web

@eepymeowers
Copy link

Is this buildable yet? If it is I'll try it out! I have a GW5 I've been wanting to use.

performSignOut(context, clientPackageName, it, signInAccount)
}
}
AccountUtils.get(context).removeSelectedAccount(clientPackageName)
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sign-out callback invoked before async operations complete

High Severity

The signOut method calls callback.onResult(Status.SUCCESS) at line 147 immediately after launching the coroutine, rather than inside it. This means the callback signals success before the actual sign-out operations (performSignOut, removeSelectedAccount) have completed. Comparing with AuthSignInService.kt which correctly places the callback inside the lifecycleScope.launchWhenStarted block, this callback should be moved inside the lifecycleScope.launch block after the sign-out operations finish. Calling apps will incorrectly believe sign-out is complete when it hasn't actually happened yet.

🔬 Verification Test

Why verification test was not possible: This is an Android service implementation that requires the full Android runtime environment, lifecycle management, and IPC mechanisms to test. The bug is clearly visible from static code analysis - comparing line 147 (callback outside the coroutine) with the similar implementation in AuthSignInService.kt lines 134 (callback inside the coroutine).

Fix in Cursor Fix in Web

Log.d(TAG, "updateProjectionState: useFastMode: $useFast")

visibleRegion = projection.visibleRegion
withoutTiltOrBearing = useFast
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Projection field never updated causing stale fallback data

Medium Severity

The updateProjectionState method accepts a projection parameter that shadows the class field this.projection. While the method correctly uses the parameter to update cached values like visibleRegion, farLeft, etc., it never updates this.projection itself. The previous implementation included projection = newProjection to update the class field. When the fallback paths are taken in fromScreenLocation (lines 74, 87) and toScreenLocation (lines 100, 113)—which happens when isInvalid() returns true or withoutTiltOrBearing is false—the code uses the stale this.projection from initialization rather than the current projection state, leading to incorrect map coordinates.

🔬 Verification Test

Why verification test was not possible: This requires the Huawei HMS Maps SDK runtime environment and actual map interactions to test. The bug is evident from static analysis: the old code updated this.projection = newProjection (visible in the diff's removed line), but the new code removes this assignment while the fallback paths at lines 74, 87, 100, and 113 still reference this.projection.

Fix in Cursor Fix in Web

if (data.isNullOrBlank()) return null
private fun getAuthOptions(packageName: String): Set<String>? {
val data = preferences.getStringSet(DEFAULT_SIGN_IN_OPTIONS_PREFIX + getPackageNameSuffix(packageName), null)
if (data.isNullOrEmpty()) return null
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

SharedPreferences type mismatch causes ClassCastException on upgrade

High Severity

The getAuthOptions method calls preferences.getStringSet() directly on a key that was previously stored as a String by the old setDefaultSignInInfo method (using putString). When Android's SharedPreferencesImpl loads a String value and the code attempts to read it as a StringSet, it throws a ClassCastException because the internal map contains a String object that cannot be cast to Set<String>. While setAuthInfo uses runCatching to safely attempt reading old String data during migration, getAuthOptions lacks this protection. Users upgrading from older versions who have existing sign-in options stored will crash when calling sign-out or revoke-access operations that invoke getAuthOptions before any new sign-in triggers the migration path in setAuthInfo.

🔬 Verification Test

Why verification test was not possible: This requires testing against a real Android SharedPreferences implementation with pre-existing String data from an older app version. The bug is evident from comparing the old code (which used putString/getString for options) with the new code (which uses getStringSet without type-safe reading). Android's SharedPreferencesImpl.getStringSet performs an unchecked cast (Set<String>) mMap.get(key) that will fail if the stored value is a String.

Fix in Cursor Fix in Web

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

9 participants