Skip to content
Draft
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 @@ -320,6 +320,18 @@ 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 AUTO_ACCEPT_TOS = "wearable_auto_accept_tos"

val PROJECTION = arrayOf(
AUTO_ACCEPT_TOS
)
}

private fun <T> withoutCallingIdentity(f: () -> T): T {
val identity = Binder.clearCallingIdentity()
try {
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 @@ -434,6 +437,25 @@ class SettingsProvider : ContentProvider() {
editor.apply()
}

private fun queryWearable(p: Array<out String>): Cursor = MatrixCursor(p).addRow(p) { key ->
when (key) {
Wearable.AUTO_ACCEPT_TOS -> getSettingsBoolean(key, false)
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.AUTO_ACCEPT_TOS -> 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
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,17 @@ package com.google.android.gms.wearable.consent

import android.os.Bundle
import androidx.appcompat.app.AppCompatActivity
import org.microg.gms.wearable.WearablePreferences

class TermsOfServiceActivity : AppCompatActivity() {

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setResult(RESULT_CANCELED)
if (WearablePreferences.isAutoAcceptTosEnabled(this)) {
setResult(RESULT_OK)
} else {
setResult(RESULT_CANCELED)
}
finish()
}
}
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_WEARABLE)!!.onPreferenceClickListener = Preference.OnPreferenceClickListener {
findNavController().navigate(requireContext(), R.id.openWearableSettings)
true
}

findPreference<Preference>(PREF_ABOUT)!!.apply {
onPreferenceClickListener = Preference.OnPreferenceClickListener {
Expand Down Expand Up @@ -139,6 +143,7 @@ class SettingsFragment : ResourceSettingsFragment() {
const val PREF_VENDING = "pref_vending"
const val PREF_WORK_PROFILE = "pref_work_profile"
const val PREF_ACCOUNTS = "pref_accounts"
const val PREF_WEARABLE = "pref_wearable"
}

init {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
/*
* SPDX-FileCopyrightText: 2025 microG Project Team
* SPDX-License-Identifier: Apache-2.0
*/

package org.microg.gms.ui

import android.os.Bundle
import androidx.lifecycle.lifecycleScope
import androidx.preference.Preference
import androidx.preference.PreferenceFragmentCompat
import androidx.preference.TwoStatePreference
import com.google.android.gms.R
import org.microg.gms.wearable.WearablePreferences

class WearableFragment : PreferenceFragmentCompat() {
private lateinit var autoAcceptTos: TwoStatePreference

override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
addPreferencesFromResource(R.xml.preferences_wearable)
}

override fun onBindPreferences() {
autoAcceptTos = preferenceScreen.findPreference(PREF_AUTO_ACCEPT_TOS) ?: autoAcceptTos
autoAcceptTos.onPreferenceChangeListener = Preference.OnPreferenceChangeListener { _, newValue ->
val appContext = requireContext().applicationContext
lifecycleScope.launchWhenResumed {
if (newValue is Boolean) {
WearablePreferences.setAutoAcceptTosEnabled(appContext, newValue)
}
updateContent()
}
true
}
}

override fun onResume() {
super.onResume()
updateContent()
}

private fun updateContent() {
val appContext = requireContext().applicationContext
lifecycleScope.launchWhenResumed {
autoAcceptTos.isChecked = WearablePreferences.isAutoAcceptTosEnabled(appContext)
}
}

companion object {
const val PREF_AUTO_ACCEPT_TOS = "wearable_auto_accept_tos"
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
/*
* SPDX-FileCopyrightText: 2025 microG Project Team
* SPDX-License-Identifier: Apache-2.0
*/

package org.microg.gms.wearable

import android.content.Context
import org.microg.gms.settings.SettingsContract

object WearablePreferences {
@JvmStatic
fun isAutoAcceptTosEnabled(context: Context): Boolean {
val projection = arrayOf(SettingsContract.Wearable.AUTO_ACCEPT_TOS)
return SettingsContract.getSettings(context, SettingsContract.Wearable.getContentUri(context), projection) { c ->
c.getInt(0) != 0
}
}

@JvmStatic
fun setAutoAcceptTosEnabled(context: Context, enabled: Boolean) {
SettingsContract.setSettings(context, SettingsContract.Wearable.getContentUri(context)) {
put(SettingsContract.Wearable.AUTO_ACCEPT_TOS, enabled)
}
}
}
10 changes: 10 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 @@ -29,6 +29,9 @@
<action
android:id="@+id/openWorkProfileSettings"
app:destination="@id/workProfileFragment" />
<action
android:id="@+id/openWearableSettings"
app:destination="@id/wearableFragment" />
<action
android:id="@+id/openMoreGoogleSettings"
app:destination="@id/googleMoreFragment" />
Expand Down Expand Up @@ -188,6 +191,13 @@
android:name="org.microg.gms.ui.WorkProfileFragment"
android:label="@string/service_name_work_profile" />

<!-- Wearable -->

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

<!-- More -->

<fragment
Expand Down
5 changes: 5 additions & 0 deletions play-services-core/src/main/res/values/strings.xml
Original file line number Diff line number Diff line change
Expand Up @@ -456,4 +456,9 @@ Please set up a password, PIN, or pattern lock screen."</string>
<string name="location_sharing_turn_off_confirm">Turn off</string>
<string name="location_sharing_confirm_dialog_title">Enable Location Sharing</string>
<string name="location_sharing_confirm_dialog_text">People you share your location with can always see:\n·Your name and photo\n·Your device\'s recent location,even when you\'re not using a Google service\n·Your device\'s battery power,and if it\'s charging\n·Your arrival and departure time,if they add a Location Sharing notification</string>

<string name="service_name_wearable">Wearable</string>
<string name="pref_wearable_tos_category">Terms of Service</string>
<string name="pref_wearable_auto_accept_tos_switch">Auto-accept Wearable TOS</string>
<string name="pref_wearable_auto_accept_tos_summary">Automatically accept the Wearable Terms of Service when requested. Disabled by default.</string>
</resources>
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 @@ -52,6 +52,10 @@
android:icon="@drawable/ic_work"
android:key="pref_work_profile"
android:title="@string/service_name_work_profile" />
<Preference
android:icon="@drawable/ic_link"
android:key="pref_wearable"
android:title="@string/service_name_wearable" />
<Preference
android:icon="@drawable/dots_horizontal"
android:key="pref_google_more"
Expand Down
20 changes: 20 additions & 0 deletions play-services-core/src/main/res/xml/preferences_wearable.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
<?xml version="1.0" encoding="utf-8"?><!--
~ SPDX-FileCopyrightText: 2025 microG Project Team
~ SPDX-License-Identifier: Apache-2.0
-->

<PreferenceScreen xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">

<PreferenceCategory
android:title="@string/pref_wearable_tos_category"
app:iconSpaceReserved="false">
<SwitchPreferenceCompat
android:title="@string/pref_wearable_auto_accept_tos_switch"
android:summary="@string/pref_wearable_auto_accept_tos_summary"
android:key="wearable_auto_accept_tos"
android:persistent="false"
app:iconSpaceReserved="false" />
</PreferenceCategory>

</PreferenceScreen>
26 changes: 26 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,32 @@

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

<uses-permission android:name="android.permission.BLUETOOTH" />
<uses-permission android:name="android.permission.BLUETOOTH_ADMIN" />
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />

<application>

<service
android:name="org.microg.gms.wearable.WearableService"
android:exported="true">
<intent-filter>
<action android:name="com.google.android.gms.wearable.BIND" />
</intent-filter>
</service>

<service
android:name="org.microg.gms.wearable.location.WearableLocationService"
android:exported="true">
<intent-filter>
<action android:name="com.google.android.gms.wearable.MESSAGE_RECEIVED" />
<data
android:host="*"
android:pathPrefix="/com/google/android/location/fused/wearable"
android:scheme="wear" />
</intent-filter>
</service>

</application>
</manifest>
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,22 @@ public synchronized Cursor getDataItemsByHostAndPath(String packageName, String
return getDataItemsByHostAndPath(getReadableDatabase(), packageName, signatureDigest, host, path);
}

/**
* Returns a cursor with columns (host, capability) for all non-deleted capability data items.
* Capabilities are stored as data items with path '/capabilities/&lt;pkg&gt;/&lt;sig&gt;/&lt;capabilityName&gt;'.
*/
public synchronized Cursor getAllCapabilityItems() {
// Extract the last path segment (capability name) using reverse string operations:
// instr(reverse(path), '/') finds the position of the first '/' from the end,
// then substr takes everything after that position (i.e. the last path segment).
return getReadableDatabase().rawQuery(
"SELECT DISTINCT host, " +
"substr(path, length(path) - instr(reverse(path), '/') + 2) AS capability " +
"FROM appKeyDataItems " +
"WHERE path LIKE '/capabilities/%' AND deleted=0",
null);
}

@Override
public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
if (oldVersion != VERSION) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@
import com.google.android.gms.wearable.Asset;
import com.google.android.gms.wearable.ConnectionConfiguration;
import com.google.android.gms.wearable.Node;
import com.google.android.gms.wearable.internal.CapabilityInfoParcelable;
import com.google.android.gms.wearable.internal.IWearableListener;
import com.google.android.gms.wearable.internal.MessageEventParcelable;
import com.google.android.gms.wearable.internal.NodeParcelable;
Expand Down Expand Up @@ -487,6 +488,32 @@ public DataHolder getDataItemsByUriAsHolder(Uri uri, String packageName) {
return dataHolder;
}

public List<CapabilityInfoParcelable> getAllCapabilityInfos() {
Map<String, List<NodeParcelable>> capabilityNodes = new HashMap<>();
Cursor cursor = nodeDatabase.getAllCapabilityItems();
if (cursor != null) {
try {
while (cursor.moveToNext()) {
String nodeId = cursor.getString(0);
String capability = cursor.getString(1);
if (capability != null && !capability.isEmpty()) {
if (!capabilityNodes.containsKey(capability)) {
capabilityNodes.put(capability, new ArrayList<>());
}
capabilityNodes.get(capability).add(new NodeParcelable(nodeId, nodeId));
}
}
} finally {
cursor.close();
}
}
List<CapabilityInfoParcelable> result = new ArrayList<>();
for (Map.Entry<String, List<NodeParcelable>> entry : capabilityNodes.entrySet()) {
result.add(new CapabilityInfoParcelable(entry.getKey(), entry.getValue()));
}
return result;
}

public synchronized void addListener(String packageName, IWearableListener listener, IntentFilter[] filters) {
if (!listeners.containsKey(packageName)) {
listeners.put(packageName, new ArrayList<ListenerInfo>());
Expand Down Expand Up @@ -581,7 +608,7 @@ private void closeConnection(String nodeId) {
} catch (IOException e1) {
Log.w(TAG, e1);
}
if (connection == sct.getWearableConnection()) {
if (sct != null && connection == sct.getWearableConnection()) {
sct.close();
sct = null;
}
Expand Down
Loading