Skip to content

Commit 51a7697

Browse files
feat(wearable): add Wear OS RFCOMM transport, bridges, and Channel API
Implement Bluetooth RFCOMM transport alongside the existing TCP path, with notification, call, and media bridges, Channel stream/file plumbing, and a settings entry for Wearable TOS auto-accept. Notes: - WEAR_BT_UUID is a microG experimental RFCOMM service id for lab use; it is not documented as a stock Wear companion SDP UUID. - ChannelManager hands AIDL callers duped pipe ends and keeps canonical FDs. - closeChannel does not swallow RuntimeException; readOutputFromFd fails closed on short skip/EOF before offset. - Output forwarder no-ops without WearableImpl; pipe close tolerates Robolectric double-close NPEs. - activeConnections and Bluetooth transport threads use concurrent maps. Signed-off-by: namdamdoi68-oss <namdamdoi68@gmail.com>
1 parent 9a206ae commit 51a7697

36 files changed

Lines changed: 3103 additions & 48 deletions

play-services-base/core/src/main/kotlin/org/microg/gms/settings/SettingsContract.kt

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -326,6 +326,18 @@ object SettingsContract {
326326
)
327327
}
328328

329+
object Wearable {
330+
const val ID = "wearable"
331+
fun getContentUri(context: Context) = Uri.withAppendedPath(getAuthorityUri(context), ID)
332+
fun getContentType(context: Context) = "vnd.android.cursor.item/vnd.${getAuthority(context)}.$ID"
333+
334+
const val AUTO_ACCEPT_TOS = "wearable_auto_accept_tos"
335+
336+
val PROJECTION = arrayOf(
337+
AUTO_ACCEPT_TOS
338+
)
339+
}
340+
329341
private fun <T> withoutCallingIdentity(f: () -> T): T {
330342
val identity = Binder.clearCallingIdentity()
331343
try {

play-services-base/core/src/main/kotlin/org/microg/gms/settings/SettingsProvider.kt

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ import org.microg.gms.settings.SettingsContract.Location
2626
import org.microg.gms.settings.SettingsContract.Profile
2727
import org.microg.gms.settings.SettingsContract.SafetyNet
2828
import org.microg.gms.settings.SettingsContract.Vending
29+
import org.microg.gms.settings.SettingsContract.Wearable
2930
import org.microg.gms.settings.SettingsContract.WorkProfile
3031
import org.microg.gms.settings.SettingsContract.getAuthority
3132
import java.io.File
@@ -85,6 +86,7 @@ class SettingsProvider : ContentProvider() {
8586
Vending.ID -> queryVending(projection ?: Vending.PROJECTION)
8687
WorkProfile.ID -> queryWorkProfile(projection ?: WorkProfile.PROJECTION)
8788
GameProfile.ID -> queryGameProfile(projection ?: GameProfile.PROJECTION)
89+
Wearable.ID -> queryWearable(projection ?: Wearable.PROJECTION)
8890
else -> null
8991
}
9092

@@ -108,6 +110,7 @@ class SettingsProvider : ContentProvider() {
108110
Vending.ID -> updateVending(values)
109111
WorkProfile.ID -> updateWorkProfile(values)
110112
GameProfile.ID -> updateGameProfile(values)
113+
Wearable.ID -> updateWearable(values)
111114
else -> return 0
112115
}
113116
return 1
@@ -440,6 +443,25 @@ class SettingsProvider : ContentProvider() {
440443
editor.apply()
441444
}
442445

446+
private fun queryWearable(p: Array<out String>): Cursor = MatrixCursor(p).addRow(p) { key ->
447+
when (key) {
448+
Wearable.AUTO_ACCEPT_TOS -> getSettingsBoolean(key, false)
449+
else -> throw IllegalArgumentException("Unknown key: $key")
450+
}
451+
}
452+
453+
private fun updateWearable(values: ContentValues) {
454+
if (values.size() == 0) return
455+
val editor = preferences.edit()
456+
values.valueSet().forEach { (key, value) ->
457+
when (key) {
458+
Wearable.AUTO_ACCEPT_TOS -> editor.putBoolean(key, value as Boolean)
459+
else -> throw IllegalArgumentException("Unknown key: $key")
460+
}
461+
}
462+
editor.apply()
463+
}
464+
443465
private fun MatrixCursor.addRow(
444466
p: Array<out String>,
445467
valueGetter: (String) -> Any?

play-services-core/src/main/AndroidManifest.xml

Lines changed: 23 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -138,10 +138,18 @@
138138

139139
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
140140
<uses-permission android:name="android.permission.INTERNET" />
141+
<uses-permission android:name="android.permission.BLUETOOTH" android:maxSdkVersion="30" />
142+
<uses-permission android:name="android.permission.BLUETOOTH_ADMIN" android:maxSdkVersion="30" />
143+
<uses-permission android:name="android.permission.BLUETOOTH_CONNECT" />
144+
<uses-permission android:name="android.permission.BLUETOOTH_SCAN" />
141145
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
142146
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
143147
<uses-permission android:name="android.permission.ACCESS_BACKGROUND_LOCATION" />
144148
<uses-permission android:name="android.permission.READ_PHONE_STATE" />
149+
<uses-permission android:name="android.permission.ANSWER_PHONE_CALLS" />
150+
<uses-permission
151+
android:name="android.permission.MEDIA_CONTENT_CONTROL"
152+
tools:ignore="ProtectedPermissions" />
145153

146154
<uses-permission android:name="android.permission.AUTHENTICATE_ACCOUNTS" />
147155
<uses-permission android:name="android.permission.GET_ACCOUNTS" />
@@ -261,7 +269,9 @@
261269
</intent-filter>
262270
</service>
263271

264-
<service android:name="org.microg.gms.wearable.location.WearableLocationService">
272+
<service
273+
android:name="org.microg.gms.wearable.location.WearableLocationService"
274+
android:exported="true">
265275
<intent-filter>
266276
<action android:name="com.google.android.gms.wearable.MESSAGE_RECEIVED" />
267277
<data
@@ -451,12 +461,23 @@
451461

452462
<!-- Wearable -->
453463

454-
<service android:name="org.microg.gms.wearable.WearableService">
464+
<service
465+
android:name="org.microg.gms.wearable.WearableService"
466+
android:exported="true">
455467
<intent-filter>
456468
<action android:name="com.google.android.gms.wearable.BIND" />
457469
</intent-filter>
458470
</service>
459471

472+
<service
473+
android:name="org.microg.gms.wearable.notification.WearableNotificationService"
474+
android:exported="true"
475+
android:permission="android.permission.BIND_NOTIFICATION_LISTENER_SERVICE">
476+
<intent-filter>
477+
<action android:name="android.service.notification.NotificationListenerService" />
478+
</intent-filter>
479+
</service>
480+
460481
<activity
461482
android:name="com.google.android.gms.wearable.consent.TermsOfServiceActivity"
462483
android:process=":ui"

play-services-core/src/main/kotlin/com/google/android/gms/wearable/consent/TermsOfServiceActivity.kt

Lines changed: 28 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,14 +5,40 @@
55

66
package com.google.android.gms.wearable.consent
77

8+
import android.app.AlertDialog
89
import android.os.Bundle
910
import androidx.appcompat.app.AppCompatActivity
11+
import com.google.android.gms.R
12+
import org.microg.gms.wearable.WearablePreferences
1013

1114
class TermsOfServiceActivity : AppCompatActivity() {
1215

1316
override fun onCreate(savedInstanceState: Bundle?) {
1417
super.onCreate(savedInstanceState)
15-
setResult(RESULT_CANCELED)
16-
finish()
18+
19+
if (WearablePreferences.isAutoAcceptTosEnabled(this)) {
20+
// User has opted-in to auto-accept; proceed immediately.
21+
setResult(RESULT_OK)
22+
finish()
23+
return
24+
}
25+
26+
// Show an explicit dialog so the user can make an informed choice.
27+
AlertDialog.Builder(this)
28+
.setTitle(R.string.wearable_tos_dialog_title)
29+
.setMessage(R.string.wearable_tos_dialog_message)
30+
.setPositiveButton(R.string.wearable_tos_accept) { _, _ ->
31+
setResult(RESULT_OK)
32+
finish()
33+
}
34+
.setNegativeButton(R.string.wearable_tos_decline) { _, _ ->
35+
setResult(RESULT_CANCELED)
36+
finish()
37+
}
38+
.setOnCancelListener {
39+
setResult(RESULT_CANCELED)
40+
finish()
41+
}
42+
.show()
1743
}
1844
}

play-services-core/src/main/kotlin/org/microg/gms/ui/SettingsFragment.kt

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,10 @@ class SettingsFragment : ResourceSettingsFragment() {
5454
findNavController().navigate(requireContext(), R.id.openWorkProfileSettings)
5555
true
5656
}
57+
findPreference<Preference>(PREF_WEARABLE)!!.onPreferenceClickListener = Preference.OnPreferenceClickListener {
58+
findNavController().navigate(requireContext(), R.id.openWearableSettings)
59+
true
60+
}
5761

5862
findPreference<Preference>(PREF_ABOUT)!!.apply {
5963
onPreferenceClickListener = Preference.OnPreferenceClickListener {
@@ -139,6 +143,7 @@ class SettingsFragment : ResourceSettingsFragment() {
139143
const val PREF_VENDING = "pref_vending"
140144
const val PREF_WORK_PROFILE = "pref_work_profile"
141145
const val PREF_ACCOUNTS = "pref_accounts"
146+
const val PREF_WEARABLE = "pref_wearable"
142147
}
143148

144149
init {
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
/*
2+
* SPDX-FileCopyrightText: 2026 microG Project Team
3+
* SPDX-License-Identifier: Apache-2.0
4+
*/
5+
6+
package org.microg.gms.ui
7+
8+
import android.os.Bundle
9+
import androidx.lifecycle.lifecycleScope
10+
import androidx.preference.Preference
11+
import androidx.preference.PreferenceFragmentCompat
12+
import androidx.preference.TwoStatePreference
13+
import com.google.android.gms.R
14+
import kotlinx.coroutines.launch
15+
import org.microg.gms.wearable.WearablePreferences
16+
17+
class WearableFragment : PreferenceFragmentCompat() {
18+
private lateinit var autoAcceptTos: TwoStatePreference
19+
20+
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
21+
addPreferencesFromResource(R.xml.preferences_wearable)
22+
23+
autoAcceptTos = preferenceScreen.findPreference(PREF_AUTO_ACCEPT_TOS)!!
24+
autoAcceptTos.onPreferenceChangeListener = Preference.OnPreferenceChangeListener { _, newValue ->
25+
val appContext = requireContext().applicationContext
26+
if (newValue is Boolean) {
27+
lifecycleScope.launch {
28+
WearablePreferences.setAutoAcceptTosEnabled(appContext, newValue)
29+
updateContent()
30+
}
31+
}
32+
true
33+
}
34+
}
35+
36+
override fun onResume() {
37+
super.onResume()
38+
updateContent()
39+
}
40+
41+
private fun updateContent() {
42+
val appContext = requireContext().applicationContext
43+
lifecycleScope.launch {
44+
autoAcceptTos.isChecked = WearablePreferences.isAutoAcceptTosEnabled(appContext)
45+
}
46+
}
47+
48+
companion object {
49+
const val PREF_AUTO_ACCEPT_TOS = "wearable_auto_accept_tos"
50+
}
51+
}
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
/*
2+
* SPDX-FileCopyrightText: 2025 microG Project Team
3+
* SPDX-License-Identifier: Apache-2.0
4+
*/
5+
6+
package org.microg.gms.wearable
7+
8+
import android.content.Context
9+
import org.microg.gms.settings.SettingsContract
10+
11+
object WearablePreferences {
12+
@JvmStatic
13+
fun isAutoAcceptTosEnabled(context: Context): Boolean {
14+
val projection = arrayOf(SettingsContract.Wearable.AUTO_ACCEPT_TOS)
15+
return SettingsContract.getSettings(context, SettingsContract.Wearable.getContentUri(context), projection) { c ->
16+
c.getInt(0) != 0
17+
}
18+
}
19+
20+
@JvmStatic
21+
fun setAutoAcceptTosEnabled(context: Context, enabled: Boolean) {
22+
SettingsContract.setSettings(context, SettingsContract.Wearable.getContentUri(context)) {
23+
put(SettingsContract.Wearable.AUTO_ACCEPT_TOS, enabled)
24+
}
25+
}
26+
}

0 commit comments

Comments
 (0)