Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
104 commits
Select commit Hold shift + click to select a range
e5668ac
Added bare layout for wearable TOS
deadYokai Dec 25, 2025
5ae5054
Added founded callbacks to IWearableCallbacks.aidl
deadYokai Dec 27, 2025
30a2cad
Added missing classes (stubs) + aidls
deadYokai Dec 27, 2025
75029a8
Added some aidl files and consent stuff
deadYokai Dec 28, 2025
54f8fc7
capability, connectionConfiguration rework
deadYokai Dec 29, 2025
f9c59c3
added services `getNodeId`, `sendRequest` and option variants of `sen…
deadYokai Dec 30, 2025
465f5f0
finaly successful connection
deadYokai Dec 31, 2025
af855ec
Some channel manager things
deadYokai Jan 4, 2026
63a77a0
Wearable lib relocation
deadYokai Jan 5, 2026
4e22cf9
updated NodeDatabaseHelper
deadYokai Jan 9, 2026
ab5c920
fix lint
deadYokai Jan 9, 2026
f9244e2
updated scary Bluetooth things
deadYokai Jan 12, 2026
a261776
Node & Assets db update
deadYokai Jan 13, 2026
8544d12
reworked Bluetooth
deadYokai Jan 14, 2026
1eacce1
update code
deadYokai Feb 6, 2026
5012986
implement few unimplemented methods
deadYokai Feb 7, 2026
6813117
Make use of packageName in ConnectionConfiguration
teccheck Feb 28, 2026
f8c8d69
Add WearOS config UI
teccheck Feb 28, 2026
b828883
Fix channel messages
teccheck Mar 1, 2026
a724559
code update
deadYokai Mar 1, 2026
80792b4
node migration update
deadYokai Mar 1, 2026
2d54f91
feat(wear): add runtimeType to ConnectionConfiguration (field 20)
teccheck Mar 4, 2026
3b2adf8
fix(wear): fix Connect message proto
teccheck Mar 4, 2026
7dd589e
feat(wear): add missing proto definitions
teccheck Mar 4, 2026
6d3fc68
feat(wear): add Wearable Reader and Writer
deadYokai Mar 4, 2026
048aa39
feat(wear): add missing messages to MessageHandler and MessageListener
deadYokai Mar 4, 2026
9d96a77
feat(wear): add NetworkConnectionManager
deadYokai Mar 4, 2026
05f76ca
rough ble implementation
deadYokai Mar 4, 2026
c51ede6
fix Type enum in wearable.proto
deadYokai Mar 4, 2026
25f975d
account matching stub
deadYokai Mar 4, 2026
cae6a36
minor changes
deadYokai Mar 9, 2026
5a7ac41
added TransportConnectionHandler class
deadYokai Mar 9, 2026
44c5464
integrate TransportConnectionHandler class
deadYokai Mar 9, 2026
d159b7b
added DataTransport class
deadYokai Mar 10, 2026
4190b7b
integration of DataTransport class
deadYokai Mar 10, 2026
d78eba4
added of AssetManager class
deadYokai Mar 15, 2026
ae92ac9
integration of AssetManager class
deadYokai Mar 15, 2026
61a6ed2
a2a encryption stub draft
deadYokai Mar 16, 2026
06957ca
move Connect handshake to separate class
deadYokai Mar 16, 2026
37ed493
Connect handshake rework
deadYokai Mar 16, 2026
495dce0
updated NodeMigration classes
deadYokai Mar 16, 2026
2010432
updated NodeMigration
deadYokai Mar 16, 2026
9bca31d
added missing peerNodeId to MessageHandler
deadYokai Mar 16, 2026
0e08ac6
update and fixes
deadYokai Mar 28, 2026
c25f308
Merge remote-tracking branch 'upstream/master' into wearable_tos
deadYokai May 9, 2026
cad179e
updated BleConnectionManager
deadYokai May 12, 2026
720e7a2
Ble wiring
deadYokai May 12, 2026
12ea32c
updated NodeDatabaseHelper
deadYokai May 12, 2026
47e42a7
bug fixes
deadYokai May 12, 2026
02bb252
bug fixes & added peernodeid to db
deadYokai May 13, 2026
edb38f7
fixes nodeid and peernodeid assigments
deadYokai May 13, 2026
0c23010
set watch nodeid to uuid instead of 8 hex
deadYokai May 13, 2026
9295b32
update
deadYokai May 13, 2026
9fb5076
enable connection in putConfig
deadYokai May 13, 2026
472e846
deduplicate config if node name is address
deadYokai May 13, 2026
a20f725
bug fixes
deadYokai May 13, 2026
7ca5f4a
intent updates
deadYokai May 14, 2026
8e8fb4d
fix fd leak, cached streams per fd
deadYokai May 14, 2026
182c741
send close request to channels before bt drop
deadYokai May 14, 2026
27feccd
[ui] added drop connectionconfiguration table button
deadYokai May 14, 2026
fd5de70
added forgotten peernodeid to onCreate in ConfigurationDatabaseHelper
deadYokai May 15, 2026
e140c7f
fixes
deadYokai May 15, 2026
79721ca
update
deadYokai May 15, 2026
7a74bd4
update
deadYokai May 16, 2026
83144db
update
deadYokai May 17, 2026
1fc26b1
update
deadYokai May 20, 2026
77e48cb
update
deadYokai May 21, 2026
fef3df7
update
deadYokai May 21, 2026
5e6d977
update
deadYokai May 22, 2026
401edf1
updateConnectionStrategy.aidl stub
deadYokai May 22, 2026
cd977b6
update
deadYokai May 22, 2026
20148b3
implementation of updateConnectionStrategy
deadYokai May 22, 2026
63c97c5
implementation of backup/restore parcelables
deadYokai May 22, 2026
63056f0
implementation of privacyRecordOptinRequest
deadYokai May 22, 2026
885546d
implementation of dataSynchronizationProgressTracking
deadYokai May 22, 2026
ddd3d48
update
deadYokai May 26, 2026
409222a
update & bug fixes
deadYokai May 28, 2026
0df01a3
update
deadYokai May 29, 2026
84af52d
DataTransport update
deadYokai May 30, 2026
fe92e3c
DataTransport exclude ghost nodes
deadYokai May 30, 2026
8d1c052
remove custom heartbeat that can cause connection issues
deadYokai May 30, 2026
38880eb
update
deadYokai May 30, 2026
e442d40
update
deadYokai Jun 20, 2026
fc64a8a
aidl update
deadYokai Jun 20, 2026
52dfc49
upd
deadYokai Jul 21, 2026
020add3
upd
deadYokai Sep 15, 2026
7dc49aa
working pairing
deadYokai Sep 30, 2026
eaf3b07
fix some bluetooth leaks
deadYokai Oct 1, 2026
7e63384
ToS and consents
deadYokai Oct 3, 2026
3a916d2
remove setConnectionAlive
deadYokai Oct 7, 2026
8a75329
Merge branch 'master' into wearable_tos
deadYokai Oct 7, 2026
8ffe96f
fix github resolve FEATURES duplicate
deadYokai Oct 7, 2026
283d34b
update features
deadYokai Oct 7, 2026
d4b34a8
added WearableWifiService class
deadYokai Oct 7, 2026
3027747
onDestroy needs `BLUETOOTH_SCAN` permission
deadYokai Oct 7, 2026
3bb9bb1
syncRecordToAll public for WearableWifiService
deadYokai Oct 7, 2026
76b021c
wifiSync
deadYokai Oct 8, 2026
6451d72
AppRecommendations & AppTheme
deadYokai Oct 8, 2026
a0c84b7
RPC and Assets update
deadYokai Oct 8, 2026
548c59d
update some parcels
deadYokai Oct 8, 2026
cac506c
some EAP and FastPair (`not tested`)
deadYokai Oct 8, 2026
29d82b5
some logs
deadYokai Oct 8, 2026
fb23044
getFdForAsset update
deadYokai Oct 8, 2026
ef2c957
add warning log for DummyService
deadYokai Oct 8, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
package org.microg.gms;

import android.os.RemoteException;
import android.util.Log;

import com.google.android.gms.common.ConnectionResult;
import com.google.android.gms.common.api.CommonStatusCodes;
Expand All @@ -32,6 +33,7 @@ public DummyService() {

@Override
public void handleServiceRequest(IGmsCallbacks callback, GetServiceRequest request, GmsService service) throws RemoteException {
Log.w("GmsDummySvc", "Service not implemented: " + service + " requested by " + request.packageName);
callback.onPostInitComplete(ConnectionResult.API_DISABLED, null, null);
}
}
1 change: 1 addition & 0 deletions play-services-core/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,7 @@ dependencies {
implementation project(':play-services-safetynet')
implementation project(':play-services-tasks-ktx')
implementation project(':play-services-fitness')
implementation project(':play-services-wearable')

mapboxRuntimeOnly project(':play-services-maps-core-mapbox')
vtmRuntimeOnly project(':play-services-maps-core-vtm')
Expand Down
1 change: 1 addition & 0 deletions play-services-core/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -460,6 +460,7 @@
<activity
android:name="com.google.android.gms.wearable.consent.TermsOfServiceActivity"
android:process=":ui"
android:exported="true"
android:configChanges="screenSize|orientation|keyboardHidden">
<intent-filter>
<action android:name="com.google.android.gms.wearable.TOS"/>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,14 +5,293 @@

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

import android.graphics.Typeface
import android.os.Bundle
import android.util.Log
import android.util.TypedValue
import android.view.Gravity
import android.view.View
import android.view.ViewGroup
import android.widget.ImageView
import android.widget.LinearLayout
import android.widget.TextView
import androidx.appcompat.app.AppCompatActivity
import androidx.appcompat.widget.SwitchCompat
import androidx.core.widget.NestedScrollView
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import com.google.android.gms.R
import com.google.android.gms.common.api.CommonStatusCodes
import com.google.android.gms.common.api.Status
import com.google.android.gms.common.api.internal.ConnectionCallbacks
import com.google.android.gms.common.api.internal.OnConnectionFailedListener
import com.google.android.gms.wearable.internal.AcceptTermsRequest
import com.google.android.material.button.MaterialButton
import org.microg.gms.wearable.BaseWearableCallbacks
import org.microg.gms.wearable.WearableClientImpl
import org.microg.gms.wearable.WearableTerms
import java.util.concurrent.CountDownLatch
import java.util.concurrent.TimeUnit
import java.util.concurrent.atomic.AtomicInteger



class TermsOfServiceActivity : AppCompatActivity() {

private lateinit var terms: List<WearableTerms.TermDef>
private val checked = HashSet<Int>()

private var termsContext = WearableTerms.CONTEXT_UNSUPERVISED
private var watchNodeId: String? = null
private var accountName: String? = null
private var perWatchConsents = false
private var showBackup = false
private var isLeDevice = false

private lateinit var scroll: NestedScrollView
private lateinit var buttonBar: View
private var acceptButton: MaterialButton? = null
private var declineButton: MaterialButton? = null

private var atBottom = false
private var submitting = false


override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setResult(RESULT_CANCELED)
finish()

termsContext = intent.getIntExtra(EXTRA_TERMS_CONTEXT, WearableTerms.CONTEXT_UNSUPERVISED)
watchNodeId = intent.getStringExtra(EXTRA_WATCH_PEER_ID)
accountName = intent.getStringExtra(EXTRA_ACCOUNT_NAME)
perWatchConsents = intent.getBooleanExtra(EXTRA_USE_CONSENT_PER_WATCH, false)
showBackup = intent.getBooleanExtra(EXTRA_SHOW_BACKUP_CONSENT, false)
isLeDevice = intent.getBooleanExtra(EXTRA_IS_LE_DEVICE, false)

if (termsContext != WearableTerms.CONTEXT_UNSUPERVISED) {
Log.w(TAG, "Unsupported terms context $termsContext")
finish()
return
}

terms = WearableTerms.forContext(termsContext)
?.filter { it.termType != WearableTerms.CLOUDSYNC && (it.termType != WearableTerms.BACKUP || showBackup) }
?: run { finish(); return }

savedInstanceState?.getIntArray(STATE_CHECKED)?.let { checked.addAll(it.toList()) }

setContentView(R.layout.activity_wearable_tos)

acceptButton = findViewById(R.id.terms_of_service_accept_button)
declineButton = findViewById(R.id.terms_of_service_decline_button)
scroll = findViewById(R.id.terms_of_service_scroll_container)
buttonBar = acceptButton?.parent as View

bindHeader()

findViewById<RecyclerView>(R.id.terms_of_service_list).apply {
layoutManager = LinearLayoutManager(this@TermsOfServiceActivity)
isNestedScrollingEnabled = false
adapter = TermsAdapter(terms.filter { it.termType != WearableTerms.TOS })
}

buttonBar.setBackgroundColor(themeColor(android.R.attr.colorBackground))
scroll.clipToPadding = true
scroll.viewTreeObserver.addOnGlobalLayoutListener {
if (scroll.paddingBottom != buttonBar.height) {
scroll.setPadding(0, 0, 0, buttonBar.height)
}
updateAcceptButton()
}
scroll.setOnScrollChangeListener(NestedScrollView.OnScrollChangeListener { _, _, _, _, _ -> updateAcceptButton() })
acceptButton?.setOnClickListener {
if (!atBottom) scroll.fullScroll(View.FOCUS_DOWN) else submit()
}
declineButton?.setOnClickListener {
setResult(RESULT_CANCELED)
finish()
}

}

override fun onSaveInstanceState(outState: Bundle) {
super.onSaveInstanceState(outState)
outState.putIntArray(STATE_CHECKED, checked.toIntArray())
}

private fun bindHeader() {
val pad = dp(24)
findViewById<ImageView>(R.id.header_image).visibility = View.GONE
findViewById<View>(R.id.header).setPadding(pad, pad, pad, 0)
val tos = terms.firstOrNull { it.termType == WearableTerms.TOS }
findViewById<TextView>(R.id.terms_of_service_title).apply {
tos?.getTitle(this@TermsOfServiceActivity)?.let { text = it }
setTextSize(TypedValue.COMPLEX_UNIT_SP, 24f)
}
findViewById<TextView>(R.id.terms_of_service_subtitle).apply {
setPadding(pad, dp(8), pad, dp(8))
text = tos?.getDescription(this@TermsOfServiceActivity)
visibility = if (text.isNullOrEmpty()) View.GONE else View.VISIBLE
}
}

private fun updateAcceptButton() {
atBottom = !scroll.canScrollVertically(1)
acceptButton?.setText(if (atBottom) R.string.wearable_tos_accept else R.string.wearable_tos_more)
}

private fun buildRequest(): AcceptTermsRequest {
val accepted = ArrayList<Int>()
for (term in terms) {
if (!term.explicit || term.termType in checked) accepted.add(term.termType)
}
if (isLeDevice) accepted.add(WearableTerms.CLOUDSYNC)
val skipped = if (showBackup && WearableTerms.BACKUP !in checked) listOf(WearableTerms.BACKUP) else null
return AcceptTermsRequest(
termsContext, accepted, null, null,
watchNodeId, accountName, skipped, perWatchConsents
)
}

private fun submit() {
if (submitting) return
submitting = true
acceptButton?.isEnabled = false
declineButton?.isEnabled = false
val request = buildRequest()
Thread {
val status = try {
acceptTermsBlocking(request)
} catch (e: Exception) {
Log.w(TAG, "acceptTerms failed", e)
CommonStatusCodes.ERROR
}
runOnUiThread {
if (isFinishing || isDestroyed) return@runOnUiThread
if (status == CommonStatusCodes.SUCCESS) {
setResult(RESULT_OK)
finish()
} else {
submitting = false
acceptButton?.isEnabled = true
declineButton?.isEnabled = true
}
}
}.start()
}

private fun acceptTermsBlocking(request: AcceptTermsRequest): Int {
val connected = CountDownLatch(1)
val client = WearableClientImpl(
this, null,
object : ConnectionCallbacks {
override fun onConnected(connectionHint: Bundle?) = connected.countDown()
override fun onConnectionSuspended(cause: Int) = Unit
},
OnConnectionFailedListener {
Log.w(TAG, "Connection failed: $it")
connected.countDown()
}
)
try {
client.connect()
if (!connected.await(10, TimeUnit.SECONDS) || !client.isConnected) {
Log.w(TAG, "Could not connect to the wearable service")
return CommonStatusCodes.ERROR
}
val code = AtomicInteger(CommonStatusCodes.ERROR)
val latch = CountDownLatch(1)
client.serviceInterface.acceptTerms(object : BaseWearableCallbacks() {
override fun onStatus(status: Status) {
code.set(status.statusCode)
latch.countDown()
}
}, request)

if (!latch.await(30, TimeUnit.SECONDS))
return CommonStatusCodes.ERROR

return code.get()
} finally {
client.disconnect()
}
}

private fun dp(value: Int) = (value * resources.displayMetrics.density).toInt()

private fun themeColor(attr: Int): Int {
val value = TypedValue()
theme.resolveAttribute(attr, value, true)
return value.data
}

private inner class TermsAdapter(private val items: List<WearableTerms.TermDef>) :
RecyclerView.Adapter<TermsAdapter.Holder>() {

inner class Holder(
row: View,
val title: TextView,
val description: TextView,
val toggle: SwitchCompat
) : RecyclerView.ViewHolder(row)

override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): Holder {
val ctx = parent.context
val title = TextView(ctx).apply {
setTextSize(TypedValue.COMPLEX_UNIT_SP, 16f)
setTypeface(typeface, Typeface.BOLD)
}
val description = TextView(ctx).apply { setTextSize(TypedValue.COMPLEX_UNIT_SP, 14f) }
val texts = LinearLayout(ctx).apply {
orientation = LinearLayout.VERTICAL
layoutParams = LinearLayout.LayoutParams(0, ViewGroup.LayoutParams.WRAP_CONTENT, 1f)
addView(title)
addView(description)
}
val toggle = SwitchCompat(ctx).apply {
layoutParams = LinearLayout.LayoutParams(
ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT
).apply { marginStart = dp(16) }
}
val row = LinearLayout(ctx).apply {
orientation = LinearLayout.HORIZONTAL
gravity = Gravity.CENTER_VERTICAL
setPadding(dp(24), dp(12), dp(24), dp(12))
layoutParams = RecyclerView.LayoutParams(
ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT
)
addView(texts)
addView(toggle)
}
return Holder(row, title, description, toggle)
}

override fun getItemCount() = items.size

override fun onBindViewHolder(holder: Holder, position: Int) {
val term = items[position]
val title = term.getTitle(this@TermsOfServiceActivity)
holder.title.text = title
holder.title.visibility = if (title.isNullOrEmpty()) View.GONE else View.VISIBLE
holder.description.text = term.getDescription(this@TermsOfServiceActivity)
holder.toggle.setOnCheckedChangeListener(null)
holder.toggle.visibility = if (term.explicit) View.VISIBLE else View.GONE
holder.toggle.isChecked = term.termType in checked
holder.toggle.setOnCheckedChangeListener { _, isChecked ->
if (isChecked) checked.add(term.termType) else checked.remove(term.termType)
}
}
}


companion object {
private const val TAG = "TermsOfService"
private const val STATE_CHECKED = "checked_terms"
private const val EXTRA_TERMS_CONTEXT = "terms_context"
private const val EXTRA_WATCH_PEER_ID = "watch_peer_id"
private const val EXTRA_USE_CONSENT_PER_WATCH = "use_consent_per_watch"
private const val EXTRA_ACCOUNT_NAME = "account_name"
private const val EXTRA_SHOW_BACKUP_CONSENT = "show_backup_consent"
private const val EXTRA_IS_LE_DEVICE = "is_le_device"
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,10 @@ class SettingsFragment : ResourceSettingsFragment() {
findNavController().navigate(requireContext(), R.id.openWorkProfileSettings)
true
}
findPreference<Preference>(PREF_WEAR)!!.onPreferenceClickListener = Preference.OnPreferenceClickListener {
findNavController().navigate(requireContext(), R.id.openWearSettings)
true
}

findPreference<Preference>(PREF_ABOUT)!!.apply {
onPreferenceClickListener = Preference.OnPreferenceClickListener {
Expand Down Expand Up @@ -138,6 +142,7 @@ class SettingsFragment : ResourceSettingsFragment() {
const val PREF_CHECKIN = "pref_checkin"
const val PREF_VENDING = "pref_vending"
const val PREF_WORK_PROFILE = "pref_work_profile"
const val PREF_WEAR = "pref_wear"
const val PREF_ACCOUNTS = "pref_accounts"
}

Expand Down
Loading
Loading