Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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 @@ -222,6 +222,24 @@ object SettingsContract {
)
}

object Wearable {
const val ID = "wearable"
fun getContentUri(context: Context) = Uri.withAppendedPath(getAuthorityUri(context), ID)
fun getContentType(context: Context) = "vnd.android.cursor.item/vnd.${getAuthority(context)}.$ID"

const val TOS_ACCEPTED = "wearable_tos_accepted"
const val NOTIFICATIONS_ENABLED = "wearable_notifications_enabled"
const val MEDIA_CONTROL_ENABLED = "wearable_media_control_enabled"
const val CALL_CONTROL_ENABLED = "wearable_call_control_enabled"

val PROJECTION = arrayOf(
TOS_ACCEPTED,
NOTIFICATIONS_ENABLED,
MEDIA_CONTROL_ENABLED,
CALL_CONTROL_ENABLED,
)
}

object Profile {
const val ID = "profile"
fun getContentUri(context: Context) = Uri.withAppendedPath(getAuthorityUri(context), ID)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ import org.microg.gms.settings.SettingsContract.Location
import org.microg.gms.settings.SettingsContract.Profile
import org.microg.gms.settings.SettingsContract.SafetyNet
import org.microg.gms.settings.SettingsContract.Vending
import org.microg.gms.settings.SettingsContract.Wearable
import org.microg.gms.settings.SettingsContract.WorkProfile
import org.microg.gms.settings.SettingsContract.getAuthority
import java.io.File
Expand Down Expand Up @@ -85,6 +86,7 @@ class SettingsProvider : ContentProvider() {
Vending.ID -> queryVending(projection ?: Vending.PROJECTION)
WorkProfile.ID -> queryWorkProfile(projection ?: WorkProfile.PROJECTION)
GameProfile.ID -> queryGameProfile(projection ?: GameProfile.PROJECTION)
Wearable.ID -> queryWearable(projection ?: Wearable.PROJECTION)
else -> null
}

Expand All @@ -108,6 +110,7 @@ class SettingsProvider : ContentProvider() {
Vending.ID -> updateVending(values)
WorkProfile.ID -> updateWorkProfile(values)
GameProfile.ID -> updateGameProfile(values)
Wearable.ID -> updateWearable(values)
else -> return 0
}
return 1
Expand Down Expand Up @@ -440,6 +443,31 @@ class SettingsProvider : ContentProvider() {
editor.apply()
}

private fun queryWearable(p: Array<out String>): Cursor = MatrixCursor(p).addRow(p) { key ->
when (key) {
Wearable.TOS_ACCEPTED -> getSettingsBoolean(key, false)
Wearable.NOTIFICATIONS_ENABLED -> getSettingsBoolean(key, true)
Wearable.MEDIA_CONTROL_ENABLED -> getSettingsBoolean(key, true)
Wearable.CALL_CONTROL_ENABLED -> getSettingsBoolean(key, true)
else -> throw IllegalArgumentException("Unknown key: $key")
}
}

private fun updateWearable(values: ContentValues) {
if (values.size() == 0) return
val editor = preferences.edit()
values.valueSet().forEach { (key, value) ->
when (key) {
Wearable.TOS_ACCEPTED -> editor.putBoolean(key, value as Boolean)
Wearable.NOTIFICATIONS_ENABLED -> editor.putBoolean(key, value as Boolean)
Wearable.MEDIA_CONTROL_ENABLED -> editor.putBoolean(key, value as Boolean)
Wearable.CALL_CONTROL_ENABLED -> editor.putBoolean(key, value as Boolean)
else -> throw IllegalArgumentException("Unknown key: $key")
}
}
editor.apply()
}

private fun MatrixCursor.addRow(
p: Array<out String>,
valueGetter: (String) -> Any?
Expand Down
2 changes: 2 additions & 0 deletions play-services-core/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -459,7 +459,9 @@

<activity
android:name="com.google.android.gms.wearable.consent.TermsOfServiceActivity"
android:exported="true"
android:process=":ui"
android:theme="@style/Theme.App.DayNight.Dialog.Alert.NoActionBar"
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 @@ -78,6 +78,13 @@ protected void prepareSelfCheckList(Context context, List<SelfCheckGroup> checks
}
permissions.add(READ_PHONE_STATE);
permissions.add(RECEIVE_SMS);
if (SDK_INT >= 31) {
permissions.add("android.permission.BLUETOOTH_CONNECT");
permissions.add("android.permission.BLUETOOTH_SCAN");
}
if (SDK_INT >= 26) {
permissions.add("android.permission.ANSWER_PHONE_CALLS");
}
checks.add(new PermissionCheckGroup(permissions.toArray(new String[0])) {
@Override
public void doChecks(Context context, ResultCollector collector) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,10 @@ class SettingsFragment : ResourceSettingsFragment() {
findNavController().navigate(requireContext(), R.id.openSafetyNetSettings)
true
}
findPreference<Preference>(PREF_WEARABLE)!!.onPreferenceClickListener = Preference.OnPreferenceClickListener {
findNavController().navigate(requireContext(), R.id.openWearableSettings)
true
}
findPreference<Preference>(PREF_LOCATION)!!.onPreferenceClickListener = Preference.OnPreferenceClickListener {
findNavController().navigate(requireContext(), R.id.openLocationSettings)
true
Expand Down Expand Up @@ -116,6 +120,13 @@ class SettingsFragment : ResourceSettingsFragment() {

findPreference<Preference>(PREF_CHECKIN)!!.setSummary(if (CheckinPreferences.isEnabled(requireContext())) org.microg.gms.base.core.R.string.service_status_enabled_short else org.microg.gms.base.core.R.string.service_status_disabled_short)
findPreference<Preference>(PREF_SNET)!!.setSummary(if (SafetyNetPreferences.isEnabled(requireContext())) org.microg.gms.base.core.R.string.service_status_enabled_short else org.microg.gms.base.core.R.string.service_status_disabled_short)
findPreference<Preference>(PREF_WEARABLE)!!.setSummary(
if (org.microg.gms.wearable.WearablePreferences.isTosAccepted(requireContext())) {
org.microg.gms.base.core.R.string.service_status_enabled_short
} else {
org.microg.gms.base.core.R.string.service_status_disabled_short
}
)

lifecycleScope.launchWhenResumed {
val entries = getAllSettingsProviders(requireContext()).flatMap { it.getEntriesDynamic(requireContext()) }
Expand All @@ -134,6 +145,7 @@ class SettingsFragment : ResourceSettingsFragment() {
const val PREF_ABOUT = "pref_about"
const val PREF_GCM = "pref_gcm"
const val PREF_SNET = "pref_snet"
const val PREF_WEARABLE = "pref_wearable"
const val PREF_LOCATION = "pref_location"
const val PREF_CHECKIN = "pref_checkin"
const val PREF_VENDING = "pref_vending"
Expand Down
8 changes: 8 additions & 0 deletions play-services-core/src/main/res/navigation/nav_settings.xml
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,9 @@
<action
android:id="@+id/openSafetyNetSettings"
app:destination="@id/safetyNetFragment" />
<action
android:id="@+id/openWearableSettings"
app:destination="@id/wearableFragment" />
<action
android:id="@+id/openVendingSettings"
app:destination="@id/vendingFragment" />
Expand Down Expand Up @@ -171,6 +174,11 @@
app:argType="string" />
</fragment>

<fragment
android:id="@+id/wearableFragment"
android:name="org.microg.gms.wearable.ui.WearablePreferencesFragment"
android:label="@string/service_name_wearable" />

<!-- Play Store services -->

<fragment
Expand Down
4 changes: 4 additions & 0 deletions play-services-core/src/main/res/xml/preferences_start.xml
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,10 @@
android:icon="@drawable/ic_certificate"
android:key="pref_snet"
android:title="@string/service_name_device_attestation" />
<Preference
android:icon="@drawable/ic_watch"
android:key="pref_wearable"
android:title="@string/service_name_wearable" />
<Preference
android:icon="@drawable/ic_shop"
android:key="pref_vending"
Expand Down
10 changes: 10 additions & 0 deletions play-services-wearable/core/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,11 @@ dependencies {
implementation project(':play-services-wearable')

implementation "org.microg:wearable:$wearableVersion"

implementation "androidx.appcompat:appcompat:$appcompatVersion"
implementation "androidx.preference:preference-ktx:$preferenceVersion"

testImplementation 'junit:junit:4.13.2'
}

android {
Expand All @@ -35,6 +40,7 @@ android {

sourceSets {
main.java.srcDirs += 'src/main/kotlin'
test.java.srcDirs += 'src/test/java'
}

lintOptions {
Expand All @@ -49,6 +55,10 @@ android {
kotlinOptions {
jvmTarget = 1.8
}

testOptions {
unitTests.returnDefaultValues = true
}
}

apply from: '../../gradle/publish-android.gradle'
Expand Down
19 changes: 19 additions & 0 deletions play-services-wearable/core/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,25 @@

<manifest xmlns:android="http://schemas.android.com/apk/res/android">

<uses-permission android:name="android.permission.BLUETOOTH" android:maxSdkVersion="30" />
<uses-permission android:name="android.permission.BLUETOOTH_ADMIN" android:maxSdkVersion="30" />
<uses-permission android:name="android.permission.BLUETOOTH_CONNECT" />
<uses-permission android:name="android.permission.BLUETOOTH_SCAN" />
<uses-permission android:name="android.permission.ANSWER_PHONE_CALLS" />

<uses-feature
android:name="android.hardware.bluetooth"
android:required="false" />

<application>
<service
android:name="org.microg.gms.wearable.WearableNotificationListenerService"
android:exported="true"
android:label="@string/service_name_wearable"
android:permission="android.permission.BIND_NOTIFICATION_LISTENER_SERVICE">
<intent-filter>
<action android:name="android.service.notification.NotificationListenerService" />
</intent-filter>
</service>
</application>
</manifest>
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
/*
* SPDX-FileCopyrightText: 2026, microG Project Team
* SPDX-License-Identifier: Apache-2.0
*/

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

import android.app.Activity;
import android.content.Intent;
import android.os.Bundle;
import android.widget.Button;
import android.widget.TextView;

import org.microg.gms.wearable.WearablePreferences;
import org.microg.gms.wearable.core.R;

/**
* Shown by Galaxy Watch / Wear OS companion apps via {@code com.google.android.gms.wearable.TOS}.
* Accepting stores the consent so pairing can continue; declining cancels the companion flow.
*/
public class TermsOfServiceActivity extends Activity {

public static final String EXTRA_TOS_ACCEPTED = "tosAccepted";

@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
if (WearablePreferences.isTosAccepted(this)) {
finishAccepted();
return;
}
setContentView(R.layout.activity_wearable_tos);
TextView body = findViewById(R.id.wearable_tos_body);
body.setText(R.string.wearable_tos_body);
Button accept = findViewById(R.id.wearable_tos_accept);
Button decline = findViewById(R.id.wearable_tos_decline);
accept.setOnClickListener(v -> {
WearablePreferences.setTosAccepted(this, true);
finishAccepted();
});
decline.setOnClickListener(v -> {
WearablePreferences.setTosAccepted(this, false);
setResult(RESULT_CANCELED);
finish();
});
}

private void finishAccepted() {
Intent result = new Intent();
result.putExtra(EXTRA_TOS_ACCEPTED, true);
setResult(RESULT_OK, result);
finish();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
/*
* SPDX-FileCopyrightText: 2026, microG Project Team
* SPDX-License-Identifier: Apache-2.0
*/

package org.microg.gms.wearable;

import com.google.android.gms.wearable.ConnectionConfiguration;

import org.microg.gms.wearable.bluetooth.WearableBtUuids;

/**
* Classifies {@link ConnectionConfiguration} rows so WearableImpl can start the matching transport.
* <p>
* GMS uses {@code type=1} for Bluetooth Classic (Wear OS companion), {@code type=3} for the
* local TCP debug socket, and a Bluetooth MAC in {@code address} even when type is omitted.
*/
public final class WearableConnectionKind {
public static final int TYPE_BLUETOOTH = 1;
public static final int TYPE_CLOUD = 2;
public static final int TYPE_NETWORK = 3;

private WearableConnectionKind() {
}

public static boolean isBluetoothAddress(String address) {
return WearableBtUuids.isBluetoothAddress(address);
}

public static boolean isBluetooth(ConnectionConfiguration config) {
if (config == null) {
return false;
}
if (config.type == TYPE_BLUETOOTH) {
return true;
}
return isBluetoothAddress(config.address);
}

public static boolean isTcpServer(ConnectionConfiguration config) {
if (config == null) {
return false;
}
return "server".equals(config.name) || config.type == TYPE_NETWORK;
}
}
Loading