From e5668acc49f4ea2ad3d96966f76dca7e8afc5a6b Mon Sep 17 00:00:00 2001 From: deadYokai Date: Thu, 25 Dec 2025 13:24:52 +0200 Subject: [PATCH 01/29] Added bare layout for wearable TOS Added some callbacks Added feature list to WearableService.java Added android:exported="true", required by android 12, for TOS only, cuz i don't know if in other needed true or false --- .../src/main/AndroidManifest.xml | 1 + .../consent/TermsOfServiceActivity.kt | 39 ++++++++++- .../main/res/layout/activity_wearable_tos.xml | 66 +++++++++++++++++++ .../microg/gms/wearable/WearableService.java | 41 +++++++++++- .../gms/wearable/WearableServiceImpl.java | 2 + .../GetCloudSyncOptInOutDoneResponse.java | 12 ++++ .../GetCloudSyncOptInStatusResponse.java | 15 +++++ 7 files changed, 174 insertions(+), 2 deletions(-) create mode 100644 play-services-core/src/main/res/layout/activity_wearable_tos.xml diff --git a/play-services-core/src/main/AndroidManifest.xml b/play-services-core/src/main/AndroidManifest.xml index 6f593efdf8..5d996b908e 100644 --- a/play-services-core/src/main/AndroidManifest.xml +++ b/play-services-core/src/main/AndroidManifest.xml @@ -465,6 +465,7 @@ diff --git a/play-services-core/src/main/kotlin/com/google/android/gms/wearable/consent/TermsOfServiceActivity.kt b/play-services-core/src/main/kotlin/com/google/android/gms/wearable/consent/TermsOfServiceActivity.kt index 83246ba405..16ac9ac5c6 100644 --- a/play-services-core/src/main/kotlin/com/google/android/gms/wearable/consent/TermsOfServiceActivity.kt +++ b/play-services-core/src/main/kotlin/com/google/android/gms/wearable/consent/TermsOfServiceActivity.kt @@ -5,14 +5,51 @@ package com.google.android.gms.wearable.consent +import android.content.Intent import android.os.Bundle import androidx.appcompat.app.AppCompatActivity +import com.google.android.gms.R +import com.google.android.material.button.MaterialButton + class TermsOfServiceActivity : AppCompatActivity() { + private var acceptButton: MaterialButton? = null + private var declineButton: MaterialButton? = null + override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) - setResult(RESULT_CANCELED) +// setResult(RESULT_CANCELED) +// finish() + + // TODO: make consent list + setContentView(R.layout.activity_wearable_tos); + + acceptButton = findViewById(R.id.terms_of_service_accept_button); + declineButton = findViewById(R.id.terms_of_service_decline_button); + + acceptButton?.setOnClickListener { acceptConsents() } + declineButton?.setOnClickListener { declineConsents() } + + } + + private fun acceptConsents() { + val result = Intent().apply { + putExtra("consents_accepted", true) + putExtra("tos_accepted", true) + putExtra("privacy_policy_accepted", true) + } + + setResult(RESULT_OK, result) + finish() + } + + private fun declineConsents() { + val result = Intent().apply { + putExtra("consents_accepted", false) + } + + setResult(RESULT_CANCELED, result) finish() } } \ No newline at end of file diff --git a/play-services-core/src/main/res/layout/activity_wearable_tos.xml b/play-services-core/src/main/res/layout/activity_wearable_tos.xml new file mode 100644 index 0000000000..b1cabc1dfb --- /dev/null +++ b/play-services-core/src/main/res/layout/activity_wearable_tos.xml @@ -0,0 +1,66 @@ + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/play-services-wearable/core/src/main/java/org/microg/gms/wearable/WearableService.java b/play-services-wearable/core/src/main/java/org/microg/gms/wearable/WearableService.java index c9f3194ede..49b79bafb4 100644 --- a/play-services-wearable/core/src/main/java/org/microg/gms/wearable/WearableService.java +++ b/play-services-wearable/core/src/main/java/org/microg/gms/wearable/WearableService.java @@ -18,6 +18,8 @@ import android.os.RemoteException; +import com.google.android.gms.common.Feature; +import com.google.android.gms.common.internal.ConnectionInfo; import com.google.android.gms.common.internal.GetServiceRequest; import com.google.android.gms.common.internal.IGmsCallbacks; @@ -29,6 +31,40 @@ public class WearableService extends BaseService { private WearableImpl wearable; + // All what i found + // for now, just to not spam outdated GMS at my companion + public static final Feature[] FEATURES = new Feature[]{ + new Feature("app_client", 4L), + new Feature("carrier_auth", 1L), + new Feature("wear3_oem_companion", 1L), + new Feature("wear_await_data_sync_complete", 1L), + new Feature("wear_backup_restore", 6L), + new Feature("wear_consent", 2L), + new Feature("wear_consent_recordoptin", 1L), + new Feature("wear_consent_recordoptin_swaadl", 1L), + new Feature("wear_consent_supervised", 2L), + new Feature("wear_get_phone_switching_feature_status", 1L), + new Feature("wear_fast_pair_account_key_sync", 1L), + new Feature("wear_fast_pair_get_account_keys", 1L), + new Feature("wear_fast_pair_get_account_key_by_account", 1L), + new Feature("wear_flush_batched_data", 1L), + new Feature("wear_get_related_configs", 1L), + new Feature("wear_get_node_id", 1L), + new Feature("wear_logging_service", 2L), + new Feature("wear_retry_connection", 1L), + new Feature("wear_set_cloud_sync_setting_by_node", 1L), + new Feature("wear_first_party_consents", 2L), + new Feature("wear_update_config", 1L), + new Feature("wear_update_connection_retry_strategy", 1L), + new Feature("wear_update_delay_config", 1L), + new Feature("wearable_services", 1L), + new Feature("wear_cancel_migration", 1L), + new Feature("wear_customizable_screens", 2L), + new Feature("wear_wifi_immediate_connect", 1L), + new Feature("wear_get_node_active_network_metered", 1L), + new Feature("wear_consents_per_watch", 3L) + }; + public WearableService() { super("GmsWearSvc", GmsService.WEAR); } @@ -50,6 +86,9 @@ public void onDestroy() { @Override public void handleServiceRequest(IGmsCallbacks callback, GetServiceRequest request, GmsService service) throws RemoteException { PackageUtils.getAndCheckCallingPackage(this, request.packageName); - callback.onPostInitComplete(0, new WearableServiceImpl(this, wearable, request.packageName), null); + ConnectionInfo connectionInfo = new ConnectionInfo(); + connectionInfo.features = FEATURES; + callback.onPostInitCompleteWithConnectionInfo(0, new WearableServiceImpl(this, wearable, request.packageName), connectionInfo); + } } diff --git a/play-services-wearable/core/src/main/java/org/microg/gms/wearable/WearableServiceImpl.java b/play-services-wearable/core/src/main/java/org/microg/gms/wearable/WearableServiceImpl.java index 1fb2c589eb..c2bd9fb4db 100644 --- a/play-services-wearable/core/src/main/java/org/microg/gms/wearable/WearableServiceImpl.java +++ b/play-services-wearable/core/src/main/java/org/microg/gms/wearable/WearableServiceImpl.java @@ -235,6 +235,7 @@ public void optInCloudSync(IWearableCallbacks callbacks, boolean enable) throws @Deprecated public void getCloudSyncOptInDone(IWearableCallbacks callbacks) throws RemoteException { Log.d(TAG, "unimplemented Method: getCloudSyncOptInDone"); + callbacks.onGetCloudSyncOptInOutDoneResponse(new GetCloudSyncOptInOutDoneResponse(0, false)); } @Override @@ -250,6 +251,7 @@ public void getCloudSyncSetting(IWearableCallbacks callbacks) throws RemoteExcep @Override public void getCloudSyncOptInStatus(IWearableCallbacks callbacks) throws RemoteException { Log.d(TAG, "unimplemented Method: getCloudSyncOptInStatus"); + callbacks.onGetCloudSyncOptInStatusResponse(new GetCloudSyncOptInStatusResponse(0, false, true)); } @Override diff --git a/play-services-wearable/src/main/java/com/google/android/gms/wearable/internal/GetCloudSyncOptInOutDoneResponse.java b/play-services-wearable/src/main/java/com/google/android/gms/wearable/internal/GetCloudSyncOptInOutDoneResponse.java index 1c3c63ec74..f72a051804 100644 --- a/play-services-wearable/src/main/java/com/google/android/gms/wearable/internal/GetCloudSyncOptInOutDoneResponse.java +++ b/play-services-wearable/src/main/java/com/google/android/gms/wearable/internal/GetCloudSyncOptInOutDoneResponse.java @@ -22,5 +22,17 @@ public class GetCloudSyncOptInOutDoneResponse extends AutoSafeParcelable { @SafeParceled(1) private int versionCode = 1; + @SafeParceled(2) + public int statusCode; + @SafeParceled(3) + public boolean isOptedIn; + + public GetCloudSyncOptInOutDoneResponse() {} + + public GetCloudSyncOptInOutDoneResponse(int statusCode, boolean isOptedIn) { + this.statusCode = statusCode; + this.isOptedIn = isOptedIn; + } + public static final Creator CREATOR = new AutoCreator(GetCloudSyncOptInOutDoneResponse.class); } diff --git a/play-services-wearable/src/main/java/com/google/android/gms/wearable/internal/GetCloudSyncOptInStatusResponse.java b/play-services-wearable/src/main/java/com/google/android/gms/wearable/internal/GetCloudSyncOptInStatusResponse.java index da21331261..b4aaf5d3ad 100644 --- a/play-services-wearable/src/main/java/com/google/android/gms/wearable/internal/GetCloudSyncOptInStatusResponse.java +++ b/play-services-wearable/src/main/java/com/google/android/gms/wearable/internal/GetCloudSyncOptInStatusResponse.java @@ -22,5 +22,20 @@ public class GetCloudSyncOptInStatusResponse extends AutoSafeParcelable { @SafeParceled(1) private int versionCode = 1; + @SafeParceled(2) + public int statusCode; + @SafeParceled(3) + public boolean isOptedIn; + @SafeParceled(4) + public boolean isDone; + + public GetCloudSyncOptInStatusResponse() {} + + public GetCloudSyncOptInStatusResponse(int statusCode, boolean isOptedIn, boolean isDone) { + this.statusCode = statusCode; + this.isOptedIn = isOptedIn; + this.isDone = isDone; + } + public static final Creator CREATOR = new AutoCreator(GetCloudSyncOptInStatusResponse.class); } From 5ae505497b91ba6057ec0bd8b3e5787e402c4d25 Mon Sep 17 00:00:00 2001 From: deadYokai Date: Sat, 27 Dec 2025 02:20:04 +0200 Subject: [PATCH 02/29] Added founded callbacks to IWearableCallbacks.aidl --- .../wearable/internal/IWearableCallbacks.aidl | 26 +++++++ .../wearable/internal/AcceptTermsRequest.java | 38 ++++++++++ .../wearable/internal/ConsentResponse.java | 69 +++++++++++++++++++ .../internal/ConsentStatusRequest.java | 15 ++++ .../wearable/internal/GetTermsResponse.java | 20 ++++++ .../internal/RecordTermConsentRequest.java | 31 +++++++++ 6 files changed, 199 insertions(+) create mode 100644 play-services-wearable/src/main/java/com/google/android/gms/wearable/internal/AcceptTermsRequest.java create mode 100644 play-services-wearable/src/main/java/com/google/android/gms/wearable/internal/ConsentResponse.java create mode 100644 play-services-wearable/src/main/java/com/google/android/gms/wearable/internal/ConsentStatusRequest.java create mode 100644 play-services-wearable/src/main/java/com/google/android/gms/wearable/internal/GetTermsResponse.java create mode 100644 play-services-wearable/src/main/java/com/google/android/gms/wearable/internal/RecordTermConsentRequest.java diff --git a/play-services-wearable/src/main/aidl/com/google/android/gms/wearable/internal/IWearableCallbacks.aidl b/play-services-wearable/src/main/aidl/com/google/android/gms/wearable/internal/IWearableCallbacks.aidl index ffa91cb9e3..0b4fa6f9fb 100644 --- a/play-services-wearable/src/main/aidl/com/google/android/gms/wearable/internal/IWearableCallbacks.aidl +++ b/play-services-wearable/src/main/aidl/com/google/android/gms/wearable/internal/IWearableCallbacks.aidl @@ -26,6 +26,9 @@ import com.google.android.gms.wearable.internal.RemoveLocalCapabilityResponse; import com.google.android.gms.wearable.internal.SendMessageResponse; import com.google.android.gms.wearable.internal.StorageInfoResponse; +import com.google.android.gms.wearable.internal.AcceptTermsRequest; +import com.google.android.gms.wearable.internal.ConsentResponse; + interface IWearableCallbacks { // Config void onGetConfigResponse(in GetConfigResponse response) = 1; @@ -49,6 +52,7 @@ interface IWearableCallbacks { // Channels void onOpenChannelResponse(in OpenChannelResponse response) = 13; void onCloseChannelResponse(in CloseChannelResponse response) = 14; + void onCloseChannelResponse2(in CloseChannelResponse response) = 15; // found two entries in google gms void onGetChannelInputStreamResponse(in GetChannelInputStreamResponse response) = 16; void onGetChannelOutputStreamResponse(in GetChannelOutputStreamResponse response) = 17; void onChannelReceiveFileResponse(in ChannelReceiveFileResponse response) = 18; @@ -62,4 +66,26 @@ interface IWearableCallbacks { void onGetAllCapabilitiesResponse(in GetAllCapabilitiesResponse response) = 22; void onAddLocalCapabilityResponse(in AddLocalCapabilityResponse response) = 25; void onRemoveLocalCapabilityResponse(in RemoveLocalCapabilityResponse response) = 26; + + // Terms of service + void onGetTermsResponse(in GetTermsResponse response) = 48; + void onConsentResponse(in ConsentResponse response) = 37; + + // Fastpair + void onGetFastpairAccountKeyByAccountResponse(in GetFastpairAccountKeyByAccountResponse response) = 49; + void onGetFastpairAccountKeysResponse(in GetFastpairAccountKeysResponse response) = 47; + + // Uncategorized + void onGetRestoreStateResponse(in GetRestoreStateResponse response) = 46; + void onBooleanResponse(in BooleanResponse response) = 45; + void onGetCompanionPackageForNodeResponse(in GetCompanionPackageForNodeResponse response) = 36; + void onRpcResponse(in RpcResponse response) = 33; + void onGetEapIdResponse(in GetEapIdResponse response) = 34; + void onPerformEapAkaResponse(in PerformEapAkaResponse response) = 35; + void onGetNodeIdResponse(in GetNodeIdResponse response) = 38; + void onAppRecommendationsResponse(in AppRecommendationsResponse response) = 39; + void onGetAppThemeResponse(in GetAppThemeResponse response) = 40; + void onGetBackupSettingsSupportedResponse(in GetBackupSettingsSupportedResponse response) = 41; + void onGetRestoreSupportedResponse(in GetRestoreSupportedResponse response) = 42; + } diff --git a/play-services-wearable/src/main/java/com/google/android/gms/wearable/internal/AcceptTermsRequest.java b/play-services-wearable/src/main/java/com/google/android/gms/wearable/internal/AcceptTermsRequest.java new file mode 100644 index 0000000000..92eed5047a --- /dev/null +++ b/play-services-wearable/src/main/java/com/google/android/gms/wearable/internal/AcceptTermsRequest.java @@ -0,0 +1,38 @@ +package com.google.android.gms.wearable.internal; + +import org.microg.safeparcel.AutoSafeParcelable; +import org.microg.safeparcel.SafeParceled; + +import java.util.List; + +public class AcceptTermsRequest extends AutoSafeParcelable { + @SafeParceled(1) + public final int statusCode; // assuming this is statusCode + @SafeParceled(2) + public final List unk2; + @SafeParceled(3) + public final String unk3; + @SafeParceled(4) + public final String unk4; + @SafeParceled(5) + public final String unk5; + @SafeParceled(6) + public final String unk6; + @SafeParceled(7) + public final List unk7; + @SafeParceled(8) + public final boolean unk8; + + public AcceptTermsRequest(int statusCode, List unk2, String unk3, String unk4, String unk5, String unk6, List unk7, boolean unk8) { + this.statusCode = statusCode; + this.unk2 = unk2; + this.unk3 = unk3; + this.unk4 = unk4; + this.unk5 = unk5; + this.unk6 = unk6; + this.unk7 = unk7; + this.unk8 = unk8; + } + + public static final Creator CREATOR = new AutoCreator(AcceptTermsRequest.class); +} diff --git a/play-services-wearable/src/main/java/com/google/android/gms/wearable/internal/ConsentResponse.java b/play-services-wearable/src/main/java/com/google/android/gms/wearable/internal/ConsentResponse.java new file mode 100644 index 0000000000..8e39b9b113 --- /dev/null +++ b/play-services-wearable/src/main/java/com/google/android/gms/wearable/internal/ConsentResponse.java @@ -0,0 +1,69 @@ +package com.google.android.gms.wearable.internal; + +import org.microg.safeparcel.AutoSafeParcelable; +import org.microg.safeparcel.SafeParceled; + +import java.util.Arrays; +import java.util.List; + +public class ConsentResponse extends AutoSafeParcelable { + + @SafeParceled(1) + public final int statusCode; + @SafeParceled(2) + public final boolean hasTosConsent; + @SafeParceled(3) + public final boolean hasLoggingConsent; + @SafeParceled(4) + public final boolean hasCloudSyncConsent; + @SafeParceled(5) + public final boolean hasLocationConsent; + @SafeParceled(6) + public final List accountConsentRecords; + @SafeParceled(7) + public final String nodeId; + @SafeParceled(8) + public final Long lastUpdateRequestedTime; + + public ConsentResponse(int statusCode, boolean hasTosConsent, boolean hasLoggingConsent, boolean hasCloudSyncConsent, boolean hasLocationConsent, List accountConsentRecords, String nodeId, Long lastUpdateRequestedTime) { + this.statusCode = statusCode; + this.hasTosConsent = hasTosConsent; + this.hasLoggingConsent = hasLoggingConsent; + this.hasCloudSyncConsent = hasCloudSyncConsent; + this.hasLocationConsent = hasLocationConsent; + this.accountConsentRecords = accountConsentRecords; + this.nodeId = nodeId; + this.lastUpdateRequestedTime = lastUpdateRequestedTime; + } + + public final int hashCode() { + return Arrays.hashCode(new Object[]{ + this.statusCode, + this.hasTosConsent, + this.hasLoggingConsent, + this.hasCloudSyncConsent, + this.hasLocationConsent, + this.accountConsentRecords, + this.nodeId, + this.lastUpdateRequestedTime + }); + } + + @Override + public String toString() { + final StringBuilder sb = new StringBuilder("ConsentResponse {"); + sb.append("statusCode = ").append(this.statusCode); + sb.append("hasTosConsent = ").append(this.hasTosConsent); + sb.append("hasLoggingConsent = ").append(this.hasLoggingConsent); + sb.append("hasCloudSyncConsent = ").append(this.hasCloudSyncConsent); + sb.append("hasLocationConsent = ").append(this.hasLocationConsent); + sb.append("accountConsentRecords = ").append(this.accountConsentRecords); + sb.append("nodeId = ").append(this.nodeId); + sb.append("lastUpdateRequestedTime = ").append(this.lastUpdateRequestedTime); + sb.append('}'); + return sb.toString(); + + } + + public static final Creator CREATOR = new AutoCreator(ConsentResponse.class); +} diff --git a/play-services-wearable/src/main/java/com/google/android/gms/wearable/internal/ConsentStatusRequest.java b/play-services-wearable/src/main/java/com/google/android/gms/wearable/internal/ConsentStatusRequest.java new file mode 100644 index 0000000000..d0d3819ed2 --- /dev/null +++ b/play-services-wearable/src/main/java/com/google/android/gms/wearable/internal/ConsentStatusRequest.java @@ -0,0 +1,15 @@ +package com.google.android.gms.wearable.internal; + +import org.microg.safeparcel.AutoSafeParcelable; +import org.microg.safeparcel.SafeParceled; + +public class ConsentStatusRequest extends AutoSafeParcelable { + @SafeParceled(1) + public final String unk; + + public ConsentStatusRequest(String unk) { + this.unk = unk; + } + + public static final Creator CREATOR = new AutoCreator(ConsentStatusRequest.class); +} diff --git a/play-services-wearable/src/main/java/com/google/android/gms/wearable/internal/GetTermsResponse.java b/play-services-wearable/src/main/java/com/google/android/gms/wearable/internal/GetTermsResponse.java new file mode 100644 index 0000000000..c34430a932 --- /dev/null +++ b/play-services-wearable/src/main/java/com/google/android/gms/wearable/internal/GetTermsResponse.java @@ -0,0 +1,20 @@ +package com.google.android.gms.wearable.internal; + +import org.microg.safeparcel.AutoSafeParcelable; +import org.microg.safeparcel.SafeParceled; + +import java.util.List; + +public class GetTermsResponse extends AutoSafeParcelable { + @SafeParceled(1) + public final int statusCode; + @SafeParceled(2) + public final List consents; // correct name is unknown, but assuming this is a consent list + + public GetTermsResponse(int statusCode, List consents) { + this.statusCode = statusCode; + this.consents = consents; + } + + public static final Creator CREATOR = new AutoCreator(GetTermsResponse.class); +} diff --git a/play-services-wearable/src/main/java/com/google/android/gms/wearable/internal/RecordTermConsentRequest.java b/play-services-wearable/src/main/java/com/google/android/gms/wearable/internal/RecordTermConsentRequest.java new file mode 100644 index 0000000000..5db356439a --- /dev/null +++ b/play-services-wearable/src/main/java/com/google/android/gms/wearable/internal/RecordTermConsentRequest.java @@ -0,0 +1,31 @@ +package com.google.android.gms.wearable.internal; + +import org.microg.safeparcel.AutoSafeParcelable; +import org.microg.safeparcel.SafeParceled; + +public class RecordTermConsentRequest extends AutoSafeParcelable { + @SafeParceled(1) + public final int unk1; + @SafeParceled(2) + public final int unk2; + @SafeParceled(3) + public final boolean unk3; + @SafeParceled(4) + public final String unk4; + @SafeParceled(5) + public final String unk5; + @SafeParceled(6) + public final String unk6; + + public RecordTermConsentRequest(int unk1, int unk2, boolean unk3, String unk4, String unk5, String unk6) { + this.unk1 = unk1; + this.unk2 = unk2; + this.unk3 = unk3; + this.unk4 = unk4; + this.unk5 = unk5; + this.unk6 = unk6; + } + + public static final Creator CREATOR = new AutoCreator(RecordTermConsentRequest.class); + +} From 30a2cad90dc4eb759159197b6acf9a2aa4f35d7b Mon Sep 17 00:00:00 2001 From: deadYokai Date: Sat, 27 Dec 2025 06:13:50 +0200 Subject: [PATCH 03/29] Added missing classes (stubs) + aidls --- .../internal/AppRecommendationsResponse.aidl | 3 + .../wearable/internal/BooleanResponse.aidl | 3 + .../wearable/internal/ConsentResponse.aidl | 3 + .../internal/GetAppThemeResponse.aidl | 3 + .../GetBackupSettingsSupportedResponse.aidl | 3 + .../GetCompanionPackageForNodeResponse.aidl | 3 + .../wearable/internal/GetEapIdResponse.aidl | 3 + ...etFastpairAccountKeyByAccountResponse.aidl | 3 + .../GetFastpairAccountKeysResponse.aidl | 3 + .../wearable/internal/GetNodeIdResponse.aidl | 3 + .../internal/GetRestoreStateResponse.aidl | 3 + .../internal/GetRestoreSupportedResponse.aidl | 3 + .../wearable/internal/GetTermsResponse.aidl | 3 + .../wearable/internal/IWearableCallbacks.aidl | 16 ++- .../internal/PerformEapAkaResponse.aidl | 3 + .../gms/wearable/internal/RpcResponse.aidl | 3 + .../wearable/internal/AcceptTermsRequest.java | 16 +++ .../internal/AppRecommendationsResponse.java | 25 +++++ .../wearable/internal/BooleanResponse.java | 25 +++++ .../wearable/internal/ConsentResponse.java | 16 +++ .../internal/ConsentStatusRequest.java | 16 +++ .../internal/GetAppThemeResponse.java | 25 +++++ .../GetBackupSettingsSupportedResponse.java | 25 +++++ .../GetCompanionPackageForNodeResponse.java | 25 +++++ .../wearable/internal/GetEapIdResponse.java | 25 +++++ ...etFastpairAccountKeyByAccountResponse.java | 25 +++++ .../GetFastpairAccountKeysResponse.java | 25 +++++ .../wearable/internal/GetNodeIdResponse.java | 25 +++++ .../internal/GetRestoreStateResponse.java | 25 +++++ .../internal/GetRestoreSupportedResponse.java | 25 +++++ .../wearable/internal/GetTermsResponse.java | 16 +++ .../internal/PerformEapAkaResponse.java | 25 +++++ .../internal/RecordTermConsentRequest.java | 16 +++ .../gms/wearable/internal/RpcResponse.java | 25 +++++ .../gms/wearable/BaseWearableCallbacks.java | 97 +++++++++++++++++++ 35 files changed, 562 insertions(+), 1 deletion(-) create mode 100644 play-services-wearable/src/main/aidl/com/google/android/gms/wearable/internal/AppRecommendationsResponse.aidl create mode 100644 play-services-wearable/src/main/aidl/com/google/android/gms/wearable/internal/BooleanResponse.aidl create mode 100644 play-services-wearable/src/main/aidl/com/google/android/gms/wearable/internal/ConsentResponse.aidl create mode 100644 play-services-wearable/src/main/aidl/com/google/android/gms/wearable/internal/GetAppThemeResponse.aidl create mode 100644 play-services-wearable/src/main/aidl/com/google/android/gms/wearable/internal/GetBackupSettingsSupportedResponse.aidl create mode 100644 play-services-wearable/src/main/aidl/com/google/android/gms/wearable/internal/GetCompanionPackageForNodeResponse.aidl create mode 100644 play-services-wearable/src/main/aidl/com/google/android/gms/wearable/internal/GetEapIdResponse.aidl create mode 100644 play-services-wearable/src/main/aidl/com/google/android/gms/wearable/internal/GetFastpairAccountKeyByAccountResponse.aidl create mode 100644 play-services-wearable/src/main/aidl/com/google/android/gms/wearable/internal/GetFastpairAccountKeysResponse.aidl create mode 100644 play-services-wearable/src/main/aidl/com/google/android/gms/wearable/internal/GetNodeIdResponse.aidl create mode 100644 play-services-wearable/src/main/aidl/com/google/android/gms/wearable/internal/GetRestoreStateResponse.aidl create mode 100644 play-services-wearable/src/main/aidl/com/google/android/gms/wearable/internal/GetRestoreSupportedResponse.aidl create mode 100644 play-services-wearable/src/main/aidl/com/google/android/gms/wearable/internal/GetTermsResponse.aidl create mode 100644 play-services-wearable/src/main/aidl/com/google/android/gms/wearable/internal/PerformEapAkaResponse.aidl create mode 100644 play-services-wearable/src/main/aidl/com/google/android/gms/wearable/internal/RpcResponse.aidl create mode 100644 play-services-wearable/src/main/java/com/google/android/gms/wearable/internal/AppRecommendationsResponse.java create mode 100644 play-services-wearable/src/main/java/com/google/android/gms/wearable/internal/BooleanResponse.java create mode 100644 play-services-wearable/src/main/java/com/google/android/gms/wearable/internal/GetAppThemeResponse.java create mode 100644 play-services-wearable/src/main/java/com/google/android/gms/wearable/internal/GetBackupSettingsSupportedResponse.java create mode 100644 play-services-wearable/src/main/java/com/google/android/gms/wearable/internal/GetCompanionPackageForNodeResponse.java create mode 100644 play-services-wearable/src/main/java/com/google/android/gms/wearable/internal/GetEapIdResponse.java create mode 100644 play-services-wearable/src/main/java/com/google/android/gms/wearable/internal/GetFastpairAccountKeyByAccountResponse.java create mode 100644 play-services-wearable/src/main/java/com/google/android/gms/wearable/internal/GetFastpairAccountKeysResponse.java create mode 100644 play-services-wearable/src/main/java/com/google/android/gms/wearable/internal/GetNodeIdResponse.java create mode 100644 play-services-wearable/src/main/java/com/google/android/gms/wearable/internal/GetRestoreStateResponse.java create mode 100644 play-services-wearable/src/main/java/com/google/android/gms/wearable/internal/GetRestoreSupportedResponse.java create mode 100644 play-services-wearable/src/main/java/com/google/android/gms/wearable/internal/PerformEapAkaResponse.java create mode 100644 play-services-wearable/src/main/java/com/google/android/gms/wearable/internal/RpcResponse.java diff --git a/play-services-wearable/src/main/aidl/com/google/android/gms/wearable/internal/AppRecommendationsResponse.aidl b/play-services-wearable/src/main/aidl/com/google/android/gms/wearable/internal/AppRecommendationsResponse.aidl new file mode 100644 index 0000000000..70bfbfd7a4 --- /dev/null +++ b/play-services-wearable/src/main/aidl/com/google/android/gms/wearable/internal/AppRecommendationsResponse.aidl @@ -0,0 +1,3 @@ +package com.google.android.gms.wearable.internal; + +parcelable AppRecommendationsResponse; diff --git a/play-services-wearable/src/main/aidl/com/google/android/gms/wearable/internal/BooleanResponse.aidl b/play-services-wearable/src/main/aidl/com/google/android/gms/wearable/internal/BooleanResponse.aidl new file mode 100644 index 0000000000..9d4f7bc77a --- /dev/null +++ b/play-services-wearable/src/main/aidl/com/google/android/gms/wearable/internal/BooleanResponse.aidl @@ -0,0 +1,3 @@ +package com.google.android.gms.wearable.internal; + +parcelable BooleanResponse; diff --git a/play-services-wearable/src/main/aidl/com/google/android/gms/wearable/internal/ConsentResponse.aidl b/play-services-wearable/src/main/aidl/com/google/android/gms/wearable/internal/ConsentResponse.aidl new file mode 100644 index 0000000000..8f555d0c30 --- /dev/null +++ b/play-services-wearable/src/main/aidl/com/google/android/gms/wearable/internal/ConsentResponse.aidl @@ -0,0 +1,3 @@ +package com.google.android.gms.wearable.internal; + +parcelable ConsentResponse; diff --git a/play-services-wearable/src/main/aidl/com/google/android/gms/wearable/internal/GetAppThemeResponse.aidl b/play-services-wearable/src/main/aidl/com/google/android/gms/wearable/internal/GetAppThemeResponse.aidl new file mode 100644 index 0000000000..c674117ae1 --- /dev/null +++ b/play-services-wearable/src/main/aidl/com/google/android/gms/wearable/internal/GetAppThemeResponse.aidl @@ -0,0 +1,3 @@ +package com.google.android.gms.wearable.internal; + +parcelable GetAppThemeResponse; diff --git a/play-services-wearable/src/main/aidl/com/google/android/gms/wearable/internal/GetBackupSettingsSupportedResponse.aidl b/play-services-wearable/src/main/aidl/com/google/android/gms/wearable/internal/GetBackupSettingsSupportedResponse.aidl new file mode 100644 index 0000000000..870ef17d16 --- /dev/null +++ b/play-services-wearable/src/main/aidl/com/google/android/gms/wearable/internal/GetBackupSettingsSupportedResponse.aidl @@ -0,0 +1,3 @@ +package com.google.android.gms.wearable.internal; + +parcelable GetBackupSettingsSupportedResponse; diff --git a/play-services-wearable/src/main/aidl/com/google/android/gms/wearable/internal/GetCompanionPackageForNodeResponse.aidl b/play-services-wearable/src/main/aidl/com/google/android/gms/wearable/internal/GetCompanionPackageForNodeResponse.aidl new file mode 100644 index 0000000000..50f6a8f2e5 --- /dev/null +++ b/play-services-wearable/src/main/aidl/com/google/android/gms/wearable/internal/GetCompanionPackageForNodeResponse.aidl @@ -0,0 +1,3 @@ +package com.google.android.gms.wearable.internal; + +parcelable GetCompanionPackageForNodeResponse; diff --git a/play-services-wearable/src/main/aidl/com/google/android/gms/wearable/internal/GetEapIdResponse.aidl b/play-services-wearable/src/main/aidl/com/google/android/gms/wearable/internal/GetEapIdResponse.aidl new file mode 100644 index 0000000000..159e162dd4 --- /dev/null +++ b/play-services-wearable/src/main/aidl/com/google/android/gms/wearable/internal/GetEapIdResponse.aidl @@ -0,0 +1,3 @@ +package com.google.android.gms.wearable.internal; + +parcelable GetEapIdResponse; diff --git a/play-services-wearable/src/main/aidl/com/google/android/gms/wearable/internal/GetFastpairAccountKeyByAccountResponse.aidl b/play-services-wearable/src/main/aidl/com/google/android/gms/wearable/internal/GetFastpairAccountKeyByAccountResponse.aidl new file mode 100644 index 0000000000..1d63772bb4 --- /dev/null +++ b/play-services-wearable/src/main/aidl/com/google/android/gms/wearable/internal/GetFastpairAccountKeyByAccountResponse.aidl @@ -0,0 +1,3 @@ +package com.google.android.gms.wearable.internal; + +parcelable GetFastpairAccountKeyByAccountResponse; diff --git a/play-services-wearable/src/main/aidl/com/google/android/gms/wearable/internal/GetFastpairAccountKeysResponse.aidl b/play-services-wearable/src/main/aidl/com/google/android/gms/wearable/internal/GetFastpairAccountKeysResponse.aidl new file mode 100644 index 0000000000..1c8aa904d9 --- /dev/null +++ b/play-services-wearable/src/main/aidl/com/google/android/gms/wearable/internal/GetFastpairAccountKeysResponse.aidl @@ -0,0 +1,3 @@ +package com.google.android.gms.wearable.internal; + +parcelable GetFastpairAccountKeysResponse; diff --git a/play-services-wearable/src/main/aidl/com/google/android/gms/wearable/internal/GetNodeIdResponse.aidl b/play-services-wearable/src/main/aidl/com/google/android/gms/wearable/internal/GetNodeIdResponse.aidl new file mode 100644 index 0000000000..65781b09a2 --- /dev/null +++ b/play-services-wearable/src/main/aidl/com/google/android/gms/wearable/internal/GetNodeIdResponse.aidl @@ -0,0 +1,3 @@ +package com.google.android.gms.wearable.internal; + +parcelable GetNodeIdResponse; diff --git a/play-services-wearable/src/main/aidl/com/google/android/gms/wearable/internal/GetRestoreStateResponse.aidl b/play-services-wearable/src/main/aidl/com/google/android/gms/wearable/internal/GetRestoreStateResponse.aidl new file mode 100644 index 0000000000..12482e13e9 --- /dev/null +++ b/play-services-wearable/src/main/aidl/com/google/android/gms/wearable/internal/GetRestoreStateResponse.aidl @@ -0,0 +1,3 @@ +package com.google.android.gms.wearable.internal; + +parcelable GetRestoreStateResponse; diff --git a/play-services-wearable/src/main/aidl/com/google/android/gms/wearable/internal/GetRestoreSupportedResponse.aidl b/play-services-wearable/src/main/aidl/com/google/android/gms/wearable/internal/GetRestoreSupportedResponse.aidl new file mode 100644 index 0000000000..84aeaefe3a --- /dev/null +++ b/play-services-wearable/src/main/aidl/com/google/android/gms/wearable/internal/GetRestoreSupportedResponse.aidl @@ -0,0 +1,3 @@ +package com.google.android.gms.wearable.internal; + +parcelable GetRestoreSupportedResponse; diff --git a/play-services-wearable/src/main/aidl/com/google/android/gms/wearable/internal/GetTermsResponse.aidl b/play-services-wearable/src/main/aidl/com/google/android/gms/wearable/internal/GetTermsResponse.aidl new file mode 100644 index 0000000000..8462b8fe37 --- /dev/null +++ b/play-services-wearable/src/main/aidl/com/google/android/gms/wearable/internal/GetTermsResponse.aidl @@ -0,0 +1,3 @@ +package com.google.android.gms.wearable.internal; + +parcelable GetTermsResponse; diff --git a/play-services-wearable/src/main/aidl/com/google/android/gms/wearable/internal/IWearableCallbacks.aidl b/play-services-wearable/src/main/aidl/com/google/android/gms/wearable/internal/IWearableCallbacks.aidl index 0b4fa6f9fb..87b9003d4e 100644 --- a/play-services-wearable/src/main/aidl/com/google/android/gms/wearable/internal/IWearableCallbacks.aidl +++ b/play-services-wearable/src/main/aidl/com/google/android/gms/wearable/internal/IWearableCallbacks.aidl @@ -26,8 +26,21 @@ import com.google.android.gms.wearable.internal.RemoveLocalCapabilityResponse; import com.google.android.gms.wearable.internal.SendMessageResponse; import com.google.android.gms.wearable.internal.StorageInfoResponse; -import com.google.android.gms.wearable.internal.AcceptTermsRequest; import com.google.android.gms.wearable.internal.ConsentResponse; +import com.google.android.gms.wearable.internal.GetTermsResponse; +import com.google.android.gms.wearable.internal.GetFastpairAccountKeyByAccountResponse; +import com.google.android.gms.wearable.internal.GetFastpairAccountKeysResponse; +import com.google.android.gms.wearable.internal.GetRestoreStateResponse; +import com.google.android.gms.wearable.internal.BooleanResponse; +import com.google.android.gms.wearable.internal.GetCompanionPackageForNodeResponse; +import com.google.android.gms.wearable.internal.RpcResponse; +import com.google.android.gms.wearable.internal.GetEapIdResponse; +import com.google.android.gms.wearable.internal.PerformEapAkaResponse; +import com.google.android.gms.wearable.internal.GetNodeIdResponse; +import com.google.android.gms.wearable.internal.GetBackupSettingsSupportedResponse; +import com.google.android.gms.wearable.internal.GetAppThemeResponse; +import com.google.android.gms.wearable.internal.AppRecommendationsResponse; +import com.google.android.gms.wearable.internal.GetRestoreSupportedResponse; interface IWearableCallbacks { // Config @@ -89,3 +102,4 @@ interface IWearableCallbacks { void onGetRestoreSupportedResponse(in GetRestoreSupportedResponse response) = 42; } + diff --git a/play-services-wearable/src/main/aidl/com/google/android/gms/wearable/internal/PerformEapAkaResponse.aidl b/play-services-wearable/src/main/aidl/com/google/android/gms/wearable/internal/PerformEapAkaResponse.aidl new file mode 100644 index 0000000000..52831b7cd8 --- /dev/null +++ b/play-services-wearable/src/main/aidl/com/google/android/gms/wearable/internal/PerformEapAkaResponse.aidl @@ -0,0 +1,3 @@ +package com.google.android.gms.wearable.internal; + +parcelable PerformEapAkaResponse; diff --git a/play-services-wearable/src/main/aidl/com/google/android/gms/wearable/internal/RpcResponse.aidl b/play-services-wearable/src/main/aidl/com/google/android/gms/wearable/internal/RpcResponse.aidl new file mode 100644 index 0000000000..2f47e9e3d3 --- /dev/null +++ b/play-services-wearable/src/main/aidl/com/google/android/gms/wearable/internal/RpcResponse.aidl @@ -0,0 +1,3 @@ +package com.google.android.gms.wearable.internal; + +parcelable RpcResponse; diff --git a/play-services-wearable/src/main/java/com/google/android/gms/wearable/internal/AcceptTermsRequest.java b/play-services-wearable/src/main/java/com/google/android/gms/wearable/internal/AcceptTermsRequest.java index 92eed5047a..e04bd60834 100644 --- a/play-services-wearable/src/main/java/com/google/android/gms/wearable/internal/AcceptTermsRequest.java +++ b/play-services-wearable/src/main/java/com/google/android/gms/wearable/internal/AcceptTermsRequest.java @@ -1,3 +1,19 @@ +/* + * Copyright 2013-2025 microG Project Team + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + package com.google.android.gms.wearable.internal; import org.microg.safeparcel.AutoSafeParcelable; diff --git a/play-services-wearable/src/main/java/com/google/android/gms/wearable/internal/AppRecommendationsResponse.java b/play-services-wearable/src/main/java/com/google/android/gms/wearable/internal/AppRecommendationsResponse.java new file mode 100644 index 0000000000..af03ef54d0 --- /dev/null +++ b/play-services-wearable/src/main/java/com/google/android/gms/wearable/internal/AppRecommendationsResponse.java @@ -0,0 +1,25 @@ +/* + * Copyright 2013-2025 microG Project Team + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.android.gms.wearable.internal; + +import org.microg.safeparcel.AutoSafeParcelable; +import org.microg.safeparcel.SafeParceled; + +public class AppRecommendationsResponse extends AutoSafeParcelable { + + public static final Creator CREATOR = new AutoCreator(AppRecommendationsResponse.class); +} diff --git a/play-services-wearable/src/main/java/com/google/android/gms/wearable/internal/BooleanResponse.java b/play-services-wearable/src/main/java/com/google/android/gms/wearable/internal/BooleanResponse.java new file mode 100644 index 0000000000..4a18ecf2b6 --- /dev/null +++ b/play-services-wearable/src/main/java/com/google/android/gms/wearable/internal/BooleanResponse.java @@ -0,0 +1,25 @@ +/* + * Copyright 2013-2025 microG Project Team + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.android.gms.wearable.internal; + +import org.microg.safeparcel.AutoSafeParcelable; +import org.microg.safeparcel.SafeParceled; + +public class BooleanResponse extends AutoSafeParcelable { + + public static final Creator CREATOR = new AutoCreator(BooleanResponse.class); +} diff --git a/play-services-wearable/src/main/java/com/google/android/gms/wearable/internal/ConsentResponse.java b/play-services-wearable/src/main/java/com/google/android/gms/wearable/internal/ConsentResponse.java index 8e39b9b113..3d99fef815 100644 --- a/play-services-wearable/src/main/java/com/google/android/gms/wearable/internal/ConsentResponse.java +++ b/play-services-wearable/src/main/java/com/google/android/gms/wearable/internal/ConsentResponse.java @@ -1,3 +1,19 @@ +/* + * Copyright 2013-2025 microG Project Team + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + package com.google.android.gms.wearable.internal; import org.microg.safeparcel.AutoSafeParcelable; diff --git a/play-services-wearable/src/main/java/com/google/android/gms/wearable/internal/ConsentStatusRequest.java b/play-services-wearable/src/main/java/com/google/android/gms/wearable/internal/ConsentStatusRequest.java index d0d3819ed2..0bf45dc3ff 100644 --- a/play-services-wearable/src/main/java/com/google/android/gms/wearable/internal/ConsentStatusRequest.java +++ b/play-services-wearable/src/main/java/com/google/android/gms/wearable/internal/ConsentStatusRequest.java @@ -1,3 +1,19 @@ +/* + * Copyright 2013-2025 microG Project Team + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + package com.google.android.gms.wearable.internal; import org.microg.safeparcel.AutoSafeParcelable; diff --git a/play-services-wearable/src/main/java/com/google/android/gms/wearable/internal/GetAppThemeResponse.java b/play-services-wearable/src/main/java/com/google/android/gms/wearable/internal/GetAppThemeResponse.java new file mode 100644 index 0000000000..5fac138221 --- /dev/null +++ b/play-services-wearable/src/main/java/com/google/android/gms/wearable/internal/GetAppThemeResponse.java @@ -0,0 +1,25 @@ +/* + * Copyright 2013-2025 microG Project Team + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.android.gms.wearable.internal; + +import org.microg.safeparcel.AutoSafeParcelable; +import org.microg.safeparcel.SafeParceled; + +public class GetAppThemeResponse extends AutoSafeParcelable { + + public static final Creator CREATOR = new AutoCreator(GetAppThemeResponse.class); +} diff --git a/play-services-wearable/src/main/java/com/google/android/gms/wearable/internal/GetBackupSettingsSupportedResponse.java b/play-services-wearable/src/main/java/com/google/android/gms/wearable/internal/GetBackupSettingsSupportedResponse.java new file mode 100644 index 0000000000..552b7f974b --- /dev/null +++ b/play-services-wearable/src/main/java/com/google/android/gms/wearable/internal/GetBackupSettingsSupportedResponse.java @@ -0,0 +1,25 @@ +/* + * Copyright 2013-2025 microG Project Team + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.android.gms.wearable.internal; + +import org.microg.safeparcel.AutoSafeParcelable; +import org.microg.safeparcel.SafeParceled; + +public class GetBackupSettingsSupportedResponse extends AutoSafeParcelable { + + public static final Creator CREATOR = new AutoCreator(GetBackupSettingsSupportedResponse.class); +} diff --git a/play-services-wearable/src/main/java/com/google/android/gms/wearable/internal/GetCompanionPackageForNodeResponse.java b/play-services-wearable/src/main/java/com/google/android/gms/wearable/internal/GetCompanionPackageForNodeResponse.java new file mode 100644 index 0000000000..25f5106c96 --- /dev/null +++ b/play-services-wearable/src/main/java/com/google/android/gms/wearable/internal/GetCompanionPackageForNodeResponse.java @@ -0,0 +1,25 @@ +/* + * Copyright 2013-2025 microG Project Team + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.android.gms.wearable.internal; + +import org.microg.safeparcel.AutoSafeParcelable; +import org.microg.safeparcel.SafeParceled; + +public class GetCompanionPackageForNodeResponse extends AutoSafeParcelable { + + public static final Creator CREATOR = new AutoCreator(GetCompanionPackageForNodeResponse.class); +} diff --git a/play-services-wearable/src/main/java/com/google/android/gms/wearable/internal/GetEapIdResponse.java b/play-services-wearable/src/main/java/com/google/android/gms/wearable/internal/GetEapIdResponse.java new file mode 100644 index 0000000000..fe9f3afca3 --- /dev/null +++ b/play-services-wearable/src/main/java/com/google/android/gms/wearable/internal/GetEapIdResponse.java @@ -0,0 +1,25 @@ +/* + * Copyright 2013-2025 microG Project Team + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.android.gms.wearable.internal; + +import org.microg.safeparcel.AutoSafeParcelable; +import org.microg.safeparcel.SafeParceled; + +public class GetEapIdResponse extends AutoSafeParcelable { + + public static final Creator CREATOR = new AutoCreator(GetEapIdResponse.class); +} diff --git a/play-services-wearable/src/main/java/com/google/android/gms/wearable/internal/GetFastpairAccountKeyByAccountResponse.java b/play-services-wearable/src/main/java/com/google/android/gms/wearable/internal/GetFastpairAccountKeyByAccountResponse.java new file mode 100644 index 0000000000..b024413a11 --- /dev/null +++ b/play-services-wearable/src/main/java/com/google/android/gms/wearable/internal/GetFastpairAccountKeyByAccountResponse.java @@ -0,0 +1,25 @@ +/* + * Copyright 2013-2025 microG Project Team + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.android.gms.wearable.internal; + +import org.microg.safeparcel.AutoSafeParcelable; +import org.microg.safeparcel.SafeParceled; + +public class GetFastpairAccountKeyByAccountResponse extends AutoSafeParcelable { + + public static final Creator CREATOR = new AutoCreator(GetFastpairAccountKeyByAccountResponse.class); +} diff --git a/play-services-wearable/src/main/java/com/google/android/gms/wearable/internal/GetFastpairAccountKeysResponse.java b/play-services-wearable/src/main/java/com/google/android/gms/wearable/internal/GetFastpairAccountKeysResponse.java new file mode 100644 index 0000000000..0c13333419 --- /dev/null +++ b/play-services-wearable/src/main/java/com/google/android/gms/wearable/internal/GetFastpairAccountKeysResponse.java @@ -0,0 +1,25 @@ +/* + * Copyright 2013-2025 microG Project Team + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.android.gms.wearable.internal; + +import org.microg.safeparcel.AutoSafeParcelable; +import org.microg.safeparcel.SafeParceled; + +public class GetFastpairAccountKeysResponse extends AutoSafeParcelable { + + public static final Creator CREATOR = new AutoCreator(GetFastpairAccountKeysResponse.class); +} diff --git a/play-services-wearable/src/main/java/com/google/android/gms/wearable/internal/GetNodeIdResponse.java b/play-services-wearable/src/main/java/com/google/android/gms/wearable/internal/GetNodeIdResponse.java new file mode 100644 index 0000000000..9b3f18331e --- /dev/null +++ b/play-services-wearable/src/main/java/com/google/android/gms/wearable/internal/GetNodeIdResponse.java @@ -0,0 +1,25 @@ +/* + * Copyright 2013-2025 microG Project Team + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.android.gms.wearable.internal; + +import org.microg.safeparcel.AutoSafeParcelable; +import org.microg.safeparcel.SafeParceled; + +public class GetNodeIdResponse extends AutoSafeParcelable { + + public static final Creator CREATOR = new AutoCreator(GetNodeIdResponse.class); +} diff --git a/play-services-wearable/src/main/java/com/google/android/gms/wearable/internal/GetRestoreStateResponse.java b/play-services-wearable/src/main/java/com/google/android/gms/wearable/internal/GetRestoreStateResponse.java new file mode 100644 index 0000000000..3a590520f9 --- /dev/null +++ b/play-services-wearable/src/main/java/com/google/android/gms/wearable/internal/GetRestoreStateResponse.java @@ -0,0 +1,25 @@ +/* + * Copyright 2013-2025 microG Project Team + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.android.gms.wearable.internal; + +import org.microg.safeparcel.AutoSafeParcelable; +import org.microg.safeparcel.SafeParceled; + +public class GetRestoreStateResponse extends AutoSafeParcelable { + + public static final Creator CREATOR = new AutoCreator(GetRestoreStateResponse.class); +} diff --git a/play-services-wearable/src/main/java/com/google/android/gms/wearable/internal/GetRestoreSupportedResponse.java b/play-services-wearable/src/main/java/com/google/android/gms/wearable/internal/GetRestoreSupportedResponse.java new file mode 100644 index 0000000000..1c67559fe9 --- /dev/null +++ b/play-services-wearable/src/main/java/com/google/android/gms/wearable/internal/GetRestoreSupportedResponse.java @@ -0,0 +1,25 @@ +/* + * Copyright 2013-2025 microG Project Team + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.android.gms.wearable.internal; + +import org.microg.safeparcel.AutoSafeParcelable; +import org.microg.safeparcel.SafeParceled; + +public class GetRestoreSupportedResponse extends AutoSafeParcelable { + + public static final Creator CREATOR = new AutoCreator(GetRestoreSupportedResponse.class); +} diff --git a/play-services-wearable/src/main/java/com/google/android/gms/wearable/internal/GetTermsResponse.java b/play-services-wearable/src/main/java/com/google/android/gms/wearable/internal/GetTermsResponse.java index c34430a932..3005495bff 100644 --- a/play-services-wearable/src/main/java/com/google/android/gms/wearable/internal/GetTermsResponse.java +++ b/play-services-wearable/src/main/java/com/google/android/gms/wearable/internal/GetTermsResponse.java @@ -1,3 +1,19 @@ +/* + * Copyright 2013-2025 microG Project Team + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + package com.google.android.gms.wearable.internal; import org.microg.safeparcel.AutoSafeParcelable; diff --git a/play-services-wearable/src/main/java/com/google/android/gms/wearable/internal/PerformEapAkaResponse.java b/play-services-wearable/src/main/java/com/google/android/gms/wearable/internal/PerformEapAkaResponse.java new file mode 100644 index 0000000000..cd2030a473 --- /dev/null +++ b/play-services-wearable/src/main/java/com/google/android/gms/wearable/internal/PerformEapAkaResponse.java @@ -0,0 +1,25 @@ +/* + * Copyright 2013-2025 microG Project Team + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.android.gms.wearable.internal; + +import org.microg.safeparcel.AutoSafeParcelable; +import org.microg.safeparcel.SafeParceled; + +public class PerformEapAkaResponse extends AutoSafeParcelable { + + public static final Creator CREATOR = new AutoCreator(PerformEapAkaResponse.class); +} diff --git a/play-services-wearable/src/main/java/com/google/android/gms/wearable/internal/RecordTermConsentRequest.java b/play-services-wearable/src/main/java/com/google/android/gms/wearable/internal/RecordTermConsentRequest.java index 5db356439a..111b94d5c6 100644 --- a/play-services-wearable/src/main/java/com/google/android/gms/wearable/internal/RecordTermConsentRequest.java +++ b/play-services-wearable/src/main/java/com/google/android/gms/wearable/internal/RecordTermConsentRequest.java @@ -1,3 +1,19 @@ +/* + * Copyright 2013-2025 microG Project Team + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + package com.google.android.gms.wearable.internal; import org.microg.safeparcel.AutoSafeParcelable; diff --git a/play-services-wearable/src/main/java/com/google/android/gms/wearable/internal/RpcResponse.java b/play-services-wearable/src/main/java/com/google/android/gms/wearable/internal/RpcResponse.java new file mode 100644 index 0000000000..0978bcd5db --- /dev/null +++ b/play-services-wearable/src/main/java/com/google/android/gms/wearable/internal/RpcResponse.java @@ -0,0 +1,25 @@ +/* + * Copyright 2013-2025 microG Project Team + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.android.gms.wearable.internal; + +import org.microg.safeparcel.AutoSafeParcelable; +import org.microg.safeparcel.SafeParceled; + +public class RpcResponse extends AutoSafeParcelable { + + public static final Creator CREATOR = new AutoCreator(RpcResponse.class); +} diff --git a/play-services-wearable/src/main/java/org/microg/gms/wearable/BaseWearableCallbacks.java b/play-services-wearable/src/main/java/org/microg/gms/wearable/BaseWearableCallbacks.java index 14778527a5..e12c86d541 100644 --- a/play-services-wearable/src/main/java/org/microg/gms/wearable/BaseWearableCallbacks.java +++ b/play-services-wearable/src/main/java/org/microg/gms/wearable/BaseWearableCallbacks.java @@ -22,27 +22,42 @@ import com.google.android.gms.common.api.Status; import com.google.android.gms.common.data.DataHolder; import com.google.android.gms.wearable.internal.AddLocalCapabilityResponse; +import com.google.android.gms.wearable.internal.AppRecommendationsResponse; +import com.google.android.gms.wearable.internal.BooleanResponse; import com.google.android.gms.wearable.internal.ChannelReceiveFileResponse; import com.google.android.gms.wearable.internal.ChannelSendFileResponse; import com.google.android.gms.wearable.internal.CloseChannelResponse; +import com.google.android.gms.wearable.internal.ConsentResponse; import com.google.android.gms.wearable.internal.DeleteDataItemsResponse; import com.google.android.gms.wearable.internal.GetAllCapabilitiesResponse; +import com.google.android.gms.wearable.internal.GetAppThemeResponse; +import com.google.android.gms.wearable.internal.GetBackupSettingsSupportedResponse; import com.google.android.gms.wearable.internal.GetCapabilityResponse; import com.google.android.gms.wearable.internal.GetChannelInputStreamResponse; import com.google.android.gms.wearable.internal.GetChannelOutputStreamResponse; import com.google.android.gms.wearable.internal.GetCloudSyncOptInOutDoneResponse; import com.google.android.gms.wearable.internal.GetCloudSyncOptInStatusResponse; import com.google.android.gms.wearable.internal.GetCloudSyncSettingResponse; +import com.google.android.gms.wearable.internal.GetCompanionPackageForNodeResponse; import com.google.android.gms.wearable.internal.GetConfigResponse; import com.google.android.gms.wearable.internal.GetConfigsResponse; import com.google.android.gms.wearable.internal.GetConnectedNodesResponse; import com.google.android.gms.wearable.internal.GetDataItemResponse; +import com.google.android.gms.wearable.internal.GetEapIdResponse; +import com.google.android.gms.wearable.internal.GetFastpairAccountKeyByAccountResponse; +import com.google.android.gms.wearable.internal.GetFastpairAccountKeysResponse; import com.google.android.gms.wearable.internal.GetFdForAssetResponse; import com.google.android.gms.wearable.internal.GetLocalNodeResponse; +import com.google.android.gms.wearable.internal.GetNodeIdResponse; +import com.google.android.gms.wearable.internal.GetRestoreStateResponse; +import com.google.android.gms.wearable.internal.GetRestoreSupportedResponse; +import com.google.android.gms.wearable.internal.GetTermsResponse; import com.google.android.gms.wearable.internal.IWearableCallbacks; import com.google.android.gms.wearable.internal.OpenChannelResponse; +import com.google.android.gms.wearable.internal.PerformEapAkaResponse; import com.google.android.gms.wearable.internal.PutDataResponse; import com.google.android.gms.wearable.internal.RemoveLocalCapabilityResponse; +import com.google.android.gms.wearable.internal.RpcResponse; import com.google.android.gms.wearable.internal.SendMessageResponse; import com.google.android.gms.wearable.internal.StorageInfoResponse; @@ -115,6 +130,12 @@ public void onCloseChannelResponse(CloseChannelResponse response) throws RemoteE } + @Override + public void onCloseChannelResponse2(CloseChannelResponse response) throws RemoteException { + Log.d(TAG, "unimplemented Method: onCloseChannelResponse2"); + + } + @Override public void onGetChannelInputStreamResponse(GetChannelInputStreamResponse response) throws RemoteException { Log.d(TAG, "unimplemented Method: onGetChannelInputStreamResponse"); @@ -175,6 +196,82 @@ public void onRemoveLocalCapabilityResponse(RemoveLocalCapabilityResponse respon } + @Override + public void onGetTermsResponse(GetTermsResponse response) throws RemoteException { + Log.d(TAG, "unimplemented Method: onGetTermsResponse"); + + } + + @Override + public void onConsentResponse(ConsentResponse response) throws RemoteException { + Log.d(TAG, "unimplemented Method: onConsentResponse"); + } + + @Override + public void onGetFastpairAccountKeyByAccountResponse(GetFastpairAccountKeyByAccountResponse response) throws RemoteException { + Log.d(TAG, "unimplemented Method: onGetFastpairAccountKeyByAccountResponse"); + } + + @Override + public void onGetFastpairAccountKeysResponse(GetFastpairAccountKeysResponse response) throws RemoteException { + Log.d(TAG, "unimplemented Method: onGetFastpairAccountKeysResponse"); + } + + @Override + public void onGetRestoreStateResponse(GetRestoreStateResponse response) throws RemoteException { + Log.d(TAG, "unimplemented Method: onGetRestoreStateResponse"); + } + + @Override + public void onBooleanResponse(BooleanResponse response) throws RemoteException { + Log.d(TAG, "unimplemented Method: onBooleanResponse"); + } + + @Override + public void onGetCompanionPackageForNodeResponse(GetCompanionPackageForNodeResponse response) throws RemoteException { + Log.d(TAG, "unimplemented Method: onGetCompanionPackageForNodeResponse"); + } + + @Override + public void onRpcResponse(RpcResponse response) throws RemoteException { + Log.d(TAG, "unimplemented Method: onRpcResponse"); + } + + @Override + public void onGetEapIdResponse(GetEapIdResponse response) throws RemoteException { + Log.d(TAG, "unimplemented Method: onGetEapIdResponse"); + } + + @Override + public void onPerformEapAkaResponse(PerformEapAkaResponse response) throws RemoteException { + Log.d(TAG, "unimplemented Method: onPerformEapAkaResponse"); + } + + @Override + public void onGetNodeIdResponse(GetNodeIdResponse response) throws RemoteException { + Log.d(TAG, "unimplemented Method: onGetNodeIdResponse"); + } + + @Override + public void onAppRecommendationsResponse(AppRecommendationsResponse response) throws RemoteException { + Log.d(TAG, "unimplemented Method: onAppRecommendationsResponse"); + } + + @Override + public void onGetAppThemeResponse(GetAppThemeResponse response) throws RemoteException { + Log.d(TAG, "unimplemented Method: onGetAppThemeResponse"); + } + + @Override + public void onGetBackupSettingsSupportedResponse(GetBackupSettingsSupportedResponse response) throws RemoteException { + Log.d(TAG, "unimplemented Method: onGetBackupSettingsSupportedResponse"); + } + + @Override + public void onGetRestoreSupportedResponse(GetRestoreSupportedResponse response) throws RemoteException { + Log.d(TAG, "unimplemented Method: onGetRestoreSupportedResponse"); + } + @Override public void onGetConfigsResponse(GetConfigsResponse response) throws RemoteException { Log.d(TAG, "unimplemented Method: onGetConfigsResponse"); From 75029a8d9f7d19e97f0aaf6e59717792333e7c67 Mon Sep 17 00:00:00 2001 From: deadYokai Date: Mon, 29 Dec 2025 01:28:46 +0200 Subject: [PATCH 04/29] Added some aidl files and consent stuff --- .../gms/wearable/WearableServiceImpl.java | 94 +++++++++++++++++++ .../internal/AddAccountToConsentRequest.aidl | 3 + .../wearable/internal/IWearableService.aidl | 14 +++ .../wearable/internal/LogCounterRequest.aidl | 3 + .../wearable/internal/LogEventRequest.aidl | 3 + .../wearable/internal/LogTimerRequest.aidl | 3 + .../internal/AddAccountToConsentRequest.java | 36 +++++++ .../wearable/internal/ConsentResponse.java | 36 +++---- .../internal/ConsentStatusRequest.java | 6 +- .../wearable/internal/GetTermsResponse.java | 6 +- .../wearable/internal/LogCounterRequest.java | 52 ++++++++++ .../wearable/internal/LogEventRequest.java | 38 ++++++++ .../wearable/internal/LogTimerRequest.java | 45 +++++++++ 13 files changed, 317 insertions(+), 22 deletions(-) create mode 100644 play-services-wearable/src/main/aidl/com/google/android/gms/wearable/internal/AddAccountToConsentRequest.aidl create mode 100644 play-services-wearable/src/main/aidl/com/google/android/gms/wearable/internal/LogCounterRequest.aidl create mode 100644 play-services-wearable/src/main/aidl/com/google/android/gms/wearable/internal/LogEventRequest.aidl create mode 100644 play-services-wearable/src/main/aidl/com/google/android/gms/wearable/internal/LogTimerRequest.aidl create mode 100644 play-services-wearable/src/main/java/com/google/android/gms/wearable/internal/AddAccountToConsentRequest.java create mode 100644 play-services-wearable/src/main/java/com/google/android/gms/wearable/internal/LogCounterRequest.java create mode 100644 play-services-wearable/src/main/java/com/google/android/gms/wearable/internal/LogEventRequest.java create mode 100644 play-services-wearable/src/main/java/com/google/android/gms/wearable/internal/LogTimerRequest.java diff --git a/play-services-wearable/core/src/main/java/org/microg/gms/wearable/WearableServiceImpl.java b/play-services-wearable/core/src/main/java/org/microg/gms/wearable/WearableServiceImpl.java index c2bd9fb4db..f0b13f44dd 100644 --- a/play-services-wearable/core/src/main/java/org/microg/gms/wearable/WearableServiceImpl.java +++ b/play-services-wearable/core/src/main/java/org/microg/gms/wearable/WearableServiceImpl.java @@ -26,6 +26,7 @@ import android.util.Log; import com.google.android.gms.common.api.Status; +import com.google.android.gms.tasks.Task; import com.google.android.gms.wearable.Asset; import com.google.android.gms.wearable.ConnectionConfiguration; import com.google.android.gms.wearable.internal.*; @@ -241,6 +242,11 @@ public void getCloudSyncOptInDone(IWearableCallbacks callbacks) throws RemoteExc @Override public void setCloudSyncSetting(IWearableCallbacks callbacks, boolean enable) throws RemoteException { Log.d(TAG, "unimplemented Method: setCloudSyncSetting"); + + postMain(callbacks, () -> { + // dummy stuff + callbacks.onStatus(new Status(0)); + }); } @Override @@ -259,6 +265,89 @@ public void sendRemoteCommand(IWearableCallbacks callbacks, byte b) throws Remot Log.d(TAG, "unimplemented Method: sendRemoteCommand: " + b); } + @Override + public void getConsentStatus(IWearableCallbacks callbacks) throws RemoteException { + Log.d(TAG, "unimplemented Method: getConsentStatus"); + + // needed proper implementation + this.wearable.networkHandler.post(new CallbackRunnable(callbacks) { + @Override + public void run(IWearableCallbacks callbacks) throws RemoteException { + try { + // get data from Tos activity? idk, + // maybe need some Consent manager or something + ConsentResponse cr = new ConsentResponse( + 0, + true, + false, + false, + false, + null, + wearable.getLocalNodeId(), + System.currentTimeMillis() + ); + callbacks.onConsentResponse(cr); + Log.d(TAG, cr.toString()); + + } catch (Exception e) { + Log.e(TAG, "getConsentStatus exception", e); + callbacks.onConsentResponse(new ConsentResponse( + 13, false, false, false, false, + null, null, null + )); + } + } + }); + } + + @Override + public void addAccountToConsent(IWearableCallbacks callbacks, AddAccountToConsentRequest request) throws RemoteException { + Log.d(TAG, "unimplemented Method addAccountToConsent: " + + "account=" + request.accountName + + ", consent=" + request.consentGranted); + + } + + @Override + public void logCounter(IWearableCallbacks callbacks, LogCounterRequest request) throws RemoteException { + Log.d(TAG, "unimplemented Method logCounter: " + + request.counterName + + ", value=" + request.value + + ", increment=" + request.increment); + + postMain(callbacks, () -> { + callbacks.onStatus(new Status(0)); + }); + } + + @Override + public void logEvent(IWearableCallbacks callbacks, LogEventRequest request) throws RemoteException { + Log.d(TAG, "unimplemented Method logEvent: data length=" + + (request.eventData != null ? request.eventData.length : 0)); + + postMain(callbacks, () -> { + callbacks.onStatus(new Status(0)); + }); + } + + @Override + public void logTimer(IWearableCallbacks callbacks, LogTimerRequest request) throws RemoteException { + Log.d(TAG, "unimplemented Method logTimer: " + request.timerName + + ", timestamp=" + request.timestamp); + + postMain(callbacks, () -> { + callbacks.onStatus(new Status(0)); + }); + } + + @Override + public void clearLogs(IWearableCallbacks callbacks) throws RemoteException { + Log.d(TAG, "unimplemented Method clearLogs"); + postMain(callbacks, () -> { + callbacks.onStatus(new Status(0)); + }); + } + @Override public void getLocalNode(IWearableCallbacks callbacks) throws RemoteException { postMain(callbacks, () -> { @@ -423,6 +512,11 @@ public void readChannelOutputFromFd(IWearableCallbacks callbacks, String s, Parc @Override public void syncWifiCredentials(IWearableCallbacks callbacks) throws RemoteException { Log.d(TAG, "unimplemented Method: syncWifiCredentials"); + + postMain(callbacks, () -> { + // dummy stuff + callbacks.onStatus(new Status(0)); + }); } /* diff --git a/play-services-wearable/src/main/aidl/com/google/android/gms/wearable/internal/AddAccountToConsentRequest.aidl b/play-services-wearable/src/main/aidl/com/google/android/gms/wearable/internal/AddAccountToConsentRequest.aidl new file mode 100644 index 0000000000..e85686d61b --- /dev/null +++ b/play-services-wearable/src/main/aidl/com/google/android/gms/wearable/internal/AddAccountToConsentRequest.aidl @@ -0,0 +1,3 @@ +package com.google.android.gms.wearable.internal; + +parcelable AddAccountToConsentRequest; \ No newline at end of file diff --git a/play-services-wearable/src/main/aidl/com/google/android/gms/wearable/internal/IWearableService.aidl b/play-services-wearable/src/main/aidl/com/google/android/gms/wearable/internal/IWearableService.aidl index 423dc8b3a9..31c9fee3c7 100644 --- a/play-services-wearable/src/main/aidl/com/google/android/gms/wearable/internal/IWearableService.aidl +++ b/play-services-wearable/src/main/aidl/com/google/android/gms/wearable/internal/IWearableService.aidl @@ -10,6 +10,12 @@ import com.google.android.gms.wearable.internal.IChannelStreamCallbacks; import com.google.android.gms.wearable.internal.IWearableCallbacks; import com.google.android.gms.wearable.internal.IWearableService; +import com.google.android.gms.wearable.internal.AddAccountToConsentRequest; + +import com.google.android.gms.wearable.internal.LogCounterRequest; +import com.google.android.gms.wearable.internal.LogEventRequest; +import com.google.android.gms.wearable.internal.LogTimerRequest; + interface IWearableService { // Configs void putConfig(IWearableCallbacks callbacks, in ConnectionConfiguration config) = 19; @@ -74,6 +80,14 @@ interface IWearableService { void sendRemoteCommand(IWearableCallbacks callbacks, byte b) = 52; + void getConsentStatus(IWearableCallbacks callbacks) = 64; + void addAccountToConsent(IWearableCallbacks callbacks, in AddAccountToConsentRequest request) = 65; + + void logCounter(IWearableCallbacks callbacks, in LogCounterRequest request) = 105; + void logEvent(IWearableCallbacks callbacks, in LogEventRequest request) = 106; + void logTimer(IWearableCallbacks callbacks, in LogTimerRequest request) = 107; + void clearLogs(IWearableCallbacks callbacks) = 108; // just assuming this is clearLogs + // deprecated Connection void putConnection(IWearableCallbacks callbacks, in ConnectionConfiguration config) = 1; void getConnection(IWearableCallbacks callbacks) = 2; diff --git a/play-services-wearable/src/main/aidl/com/google/android/gms/wearable/internal/LogCounterRequest.aidl b/play-services-wearable/src/main/aidl/com/google/android/gms/wearable/internal/LogCounterRequest.aidl new file mode 100644 index 0000000000..709645b8bd --- /dev/null +++ b/play-services-wearable/src/main/aidl/com/google/android/gms/wearable/internal/LogCounterRequest.aidl @@ -0,0 +1,3 @@ +package com.google.android.gms.wearable.internal; + +parcelable LogCounterRequest; \ No newline at end of file diff --git a/play-services-wearable/src/main/aidl/com/google/android/gms/wearable/internal/LogEventRequest.aidl b/play-services-wearable/src/main/aidl/com/google/android/gms/wearable/internal/LogEventRequest.aidl new file mode 100644 index 0000000000..e41a6e3c29 --- /dev/null +++ b/play-services-wearable/src/main/aidl/com/google/android/gms/wearable/internal/LogEventRequest.aidl @@ -0,0 +1,3 @@ +package com.google.android.gms.wearable.internal; + +parcelable LogEventRequest; \ No newline at end of file diff --git a/play-services-wearable/src/main/aidl/com/google/android/gms/wearable/internal/LogTimerRequest.aidl b/play-services-wearable/src/main/aidl/com/google/android/gms/wearable/internal/LogTimerRequest.aidl new file mode 100644 index 0000000000..7298d5fcbf --- /dev/null +++ b/play-services-wearable/src/main/aidl/com/google/android/gms/wearable/internal/LogTimerRequest.aidl @@ -0,0 +1,3 @@ +package com.google.android.gms.wearable.internal; + +parcelable LogTimerRequest; \ No newline at end of file diff --git a/play-services-wearable/src/main/java/com/google/android/gms/wearable/internal/AddAccountToConsentRequest.java b/play-services-wearable/src/main/java/com/google/android/gms/wearable/internal/AddAccountToConsentRequest.java new file mode 100644 index 0000000000..efa54aec9d --- /dev/null +++ b/play-services-wearable/src/main/java/com/google/android/gms/wearable/internal/AddAccountToConsentRequest.java @@ -0,0 +1,36 @@ +/* + * Copyright 2013-2025 microG Project Team + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.android.gms.wearable.internal; + +import org.microg.safeparcel.AutoSafeParcelable; +import org.microg.safeparcel.SafeParceled; + +public class AddAccountToConsentRequest extends AutoSafeParcelable { + @SafeParceled(1) + public String accountName; + @SafeParceled(2) + public boolean consentGranted; + + private AddAccountToConsentRequest() {} + + public AddAccountToConsentRequest(String accountName, boolean consentGranted) { + this.accountName = accountName; + this.consentGranted = consentGranted; + } + + public static final Creator CREATOR = new AutoCreator(AddAccountToConsentRequest.class); +} diff --git a/play-services-wearable/src/main/java/com/google/android/gms/wearable/internal/ConsentResponse.java b/play-services-wearable/src/main/java/com/google/android/gms/wearable/internal/ConsentResponse.java index 3d99fef815..a32c1d709f 100644 --- a/play-services-wearable/src/main/java/com/google/android/gms/wearable/internal/ConsentResponse.java +++ b/play-services-wearable/src/main/java/com/google/android/gms/wearable/internal/ConsentResponse.java @@ -25,21 +25,23 @@ public class ConsentResponse extends AutoSafeParcelable { @SafeParceled(1) - public final int statusCode; + public int statusCode; @SafeParceled(2) - public final boolean hasTosConsent; + public boolean hasTosConsent; @SafeParceled(3) - public final boolean hasLoggingConsent; + public boolean hasLoggingConsent; @SafeParceled(4) - public final boolean hasCloudSyncConsent; + public boolean hasCloudSyncConsent; @SafeParceled(5) - public final boolean hasLocationConsent; + public boolean hasLocationConsent; @SafeParceled(6) - public final List accountConsentRecords; + public List accountConsentRecords; @SafeParceled(7) - public final String nodeId; + public String nodeId; @SafeParceled(8) - public final Long lastUpdateRequestedTime; + public Long lastUpdateRequestedTime; + + private ConsentResponse() {} public ConsentResponse(int statusCode, boolean hasTosConsent, boolean hasLoggingConsent, boolean hasCloudSyncConsent, boolean hasLocationConsent, List accountConsentRecords, String nodeId, Long lastUpdateRequestedTime) { this.statusCode = statusCode; @@ -68,15 +70,15 @@ public final int hashCode() { @Override public String toString() { final StringBuilder sb = new StringBuilder("ConsentResponse {"); - sb.append("statusCode = ").append(this.statusCode); - sb.append("hasTosConsent = ").append(this.hasTosConsent); - sb.append("hasLoggingConsent = ").append(this.hasLoggingConsent); - sb.append("hasCloudSyncConsent = ").append(this.hasCloudSyncConsent); - sb.append("hasLocationConsent = ").append(this.hasLocationConsent); - sb.append("accountConsentRecords = ").append(this.accountConsentRecords); - sb.append("nodeId = ").append(this.nodeId); - sb.append("lastUpdateRequestedTime = ").append(this.lastUpdateRequestedTime); - sb.append('}'); + sb.append("\nstatusCode = ").append(this.statusCode); + sb.append("\nhasTosConsent = ").append(this.hasTosConsent); + sb.append("\nhasLoggingConsent = ").append(this.hasLoggingConsent); + sb.append("\nhasCloudSyncConsent = ").append(this.hasCloudSyncConsent); + sb.append("\nhasLocationConsent = ").append(this.hasLocationConsent); + sb.append("\naccountConsentRecords = ").append(this.accountConsentRecords); + sb.append("\nnodeId = ").append(this.nodeId); + sb.append("\nlastUpdateRequestedTime = ").append(this.lastUpdateRequestedTime); + sb.append("\n}\n"); return sb.toString(); } diff --git a/play-services-wearable/src/main/java/com/google/android/gms/wearable/internal/ConsentStatusRequest.java b/play-services-wearable/src/main/java/com/google/android/gms/wearable/internal/ConsentStatusRequest.java index 0bf45dc3ff..02e7eaa028 100644 --- a/play-services-wearable/src/main/java/com/google/android/gms/wearable/internal/ConsentStatusRequest.java +++ b/play-services-wearable/src/main/java/com/google/android/gms/wearable/internal/ConsentStatusRequest.java @@ -21,10 +21,10 @@ public class ConsentStatusRequest extends AutoSafeParcelable { @SafeParceled(1) - public final String unk; + public String status; - public ConsentStatusRequest(String unk) { - this.unk = unk; + public ConsentStatusRequest(String status) { + this.status = status; } public static final Creator CREATOR = new AutoCreator(ConsentStatusRequest.class); diff --git a/play-services-wearable/src/main/java/com/google/android/gms/wearable/internal/GetTermsResponse.java b/play-services-wearable/src/main/java/com/google/android/gms/wearable/internal/GetTermsResponse.java index 3005495bff..1d1249f17e 100644 --- a/play-services-wearable/src/main/java/com/google/android/gms/wearable/internal/GetTermsResponse.java +++ b/play-services-wearable/src/main/java/com/google/android/gms/wearable/internal/GetTermsResponse.java @@ -23,9 +23,11 @@ public class GetTermsResponse extends AutoSafeParcelable { @SafeParceled(1) - public final int statusCode; + public int statusCode; @SafeParceled(2) - public final List consents; // correct name is unknown, but assuming this is a consent list + public List consents; // correct name is unknown, but assuming this is a consent list + + private GetTermsResponse() {} public GetTermsResponse(int statusCode, List consents) { this.statusCode = statusCode; diff --git a/play-services-wearable/src/main/java/com/google/android/gms/wearable/internal/LogCounterRequest.java b/play-services-wearable/src/main/java/com/google/android/gms/wearable/internal/LogCounterRequest.java new file mode 100644 index 0000000000..8b5d965b9d --- /dev/null +++ b/play-services-wearable/src/main/java/com/google/android/gms/wearable/internal/LogCounterRequest.java @@ -0,0 +1,52 @@ +/* + * Copyright 2013-2025 microG Project Team + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.android.gms.wearable.internal; + +import org.microg.safeparcel.AutoSafeParcelable; +import org.microg.safeparcel.SafeParceled; + +public class LogCounterRequest extends AutoSafeParcelable { + @SafeParceled(1) + public String counterName; + @SafeParceled(2) + public long value; + @SafeParceled(3) + public byte[] counterData; + @SafeParceled(4) + public long timestamp; + @SafeParceled(5) + public boolean increment; + + private LogCounterRequest() {} + + public LogCounterRequest(String counterName, long value, byte[] counterData, long timestamp, boolean increment) { + this.counterName = counterName; + this.value = value; + this.counterData = counterData; + this.timestamp = timestamp; + this.increment = increment; + } + + @Override + public String toString() { + return "LogCounterRequest{counterName='" + counterName + "', value=" + value + + ", timestamp=" + timestamp + ", increment=" + increment + + ", counterData.length=" + (counterData != null ? counterData.length : 0) + "}"; + } + + public static final Creator CREATOR = new AutoCreator<>(LogCounterRequest.class); +} \ No newline at end of file diff --git a/play-services-wearable/src/main/java/com/google/android/gms/wearable/internal/LogEventRequest.java b/play-services-wearable/src/main/java/com/google/android/gms/wearable/internal/LogEventRequest.java new file mode 100644 index 0000000000..04c0bddf2a --- /dev/null +++ b/play-services-wearable/src/main/java/com/google/android/gms/wearable/internal/LogEventRequest.java @@ -0,0 +1,38 @@ +/* + * Copyright 2013-2025 microG Project Team + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.android.gms.wearable.internal; + +import org.microg.safeparcel.AutoSafeParcelable; +import org.microg.safeparcel.SafeParceled; + +public class LogEventRequest extends AutoSafeParcelable { + @SafeParceled(1) + public byte[] eventData; + + private LogEventRequest() {} + + public LogEventRequest(byte[] eventData) { + this.eventData = eventData; + } + + @Override + public String toString() { + return "LogEventRequest{eventData.length=" + (eventData != null ? eventData.length : 0) + "}"; + } + + public static final Creator CREATOR = new AutoCreator<>(LogEventRequest.class); +} \ No newline at end of file diff --git a/play-services-wearable/src/main/java/com/google/android/gms/wearable/internal/LogTimerRequest.java b/play-services-wearable/src/main/java/com/google/android/gms/wearable/internal/LogTimerRequest.java new file mode 100644 index 0000000000..f42443dd0d --- /dev/null +++ b/play-services-wearable/src/main/java/com/google/android/gms/wearable/internal/LogTimerRequest.java @@ -0,0 +1,45 @@ +/* + * Copyright 2013-2025 microG Project Team + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.android.gms.wearable.internal; + +import org.microg.safeparcel.AutoSafeParcelable; +import org.microg.safeparcel.SafeParceled; + +public class LogTimerRequest extends AutoSafeParcelable { + @SafeParceled(1) + public String timerName; + @SafeParceled(2) + public long timestamp; + @SafeParceled(3) + public byte[] timerData; + + private LogTimerRequest() {} + + public LogTimerRequest(String timerName, long timestamp, byte[] timerData) { + this.timerName = timerName; + this.timestamp = timestamp; + this.timerData = timerData; + } + + @Override + public String toString() { + return "LogTimerRequest{timerName='" + timerName + "', timestamp=" + timestamp + + ", timerData.length=" + (timerData != null ? timerData.length : 0) + "}"; + } + + public static final Creator CREATOR = new AutoCreator<>(LogTimerRequest.class); +} From 54f8fc7fab07b9b56071d78bafb604f4a65b032f Mon Sep 17 00:00:00 2001 From: deadYokai Date: Mon, 29 Dec 2025 10:09:29 +0200 Subject: [PATCH 05/29] capability, connectionConfiguration rework --- .../gms/wearable/CapabilityManager.java | 145 +++++++++++++++-- .../gms/wearable/WearableServiceImpl.java | 146 ++++++++++++++++-- .../wearable/internal/IWearableService.aidl | 2 + .../gms/wearable/ConnectionConfiguration.java | 70 +++++++-- .../gms/wearable/ConnectionDelayFilters.java | 40 +++++ .../gms/wearable/ConnectionRestrictions.java | 36 +++++ .../android/gms/wearable/DataItemFilter.java | 46 ++++++ .../wearable/internal/BooleanResponse.java | 30 ++++ .../internal/GetAllCapabilitiesResponse.java | 7 + 9 files changed, 482 insertions(+), 40 deletions(-) create mode 100644 play-services-wearable/src/main/java/com/google/android/gms/wearable/ConnectionDelayFilters.java create mode 100644 play-services-wearable/src/main/java/com/google/android/gms/wearable/ConnectionRestrictions.java create mode 100644 play-services-wearable/src/main/java/com/google/android/gms/wearable/DataItemFilter.java diff --git a/play-services-wearable/core/src/main/java/org/microg/gms/wearable/CapabilityManager.java b/play-services-wearable/core/src/main/java/org/microg/gms/wearable/CapabilityManager.java index 0c8f59ffc6..8a10674968 100644 --- a/play-services-wearable/core/src/main/java/org/microg/gms/wearable/CapabilityManager.java +++ b/play-services-wearable/core/src/main/java/org/microg/gms/wearable/CapabilityManager.java @@ -18,6 +18,7 @@ import android.content.Context; import android.net.Uri; +import android.util.Log; import com.google.android.gms.common.api.CommonStatusCodes; import com.google.android.gms.common.data.DataHolder; @@ -27,15 +28,20 @@ import org.microg.gms.common.PackageUtils; +import java.nio.charset.StandardCharsets; import java.util.HashSet; import java.util.Set; public class CapabilityManager { + private static final String TAG = "CapabilityManager"; + private static final Uri ROOT = Uri.parse("wear:/capabilities/"); private final Context context; private final WearableImpl wearable; private final String packageName; + private final Object lock = new Object(); + private Set capabilities = new HashSet(); public CapabilityManager(Context context, WearableImpl wearable, String packageName) { @@ -44,6 +50,35 @@ public CapabilityManager(Context context, WearableImpl wearable, String packageN this.packageName = packageName; } + public enum CapabilityType { + STATIC("s", "+", "+#"), + DYNAMIC("d", "-", "-#"); + + public final String typeCode; + public final String addSymbol; + public final String addSymbolWithHash; + + CapabilityType(String typeCode, String addSymbol, String addSymbolWithHash) { + this.typeCode = typeCode; + this.addSymbol = addSymbol; + this.addSymbolWithHash = addSymbolWithHash; + } + + public static CapabilityType fromBytes(byte[] data) { + if (data == null || data.length == 0) return DYNAMIC; + + String code = new String(data, 0, 1, StandardCharsets.UTF_8); + + if (STATIC.typeCode.equals(code)) return STATIC; + + return DYNAMIC; + } + + public byte[] toBytes() { + return typeCode.getBytes(StandardCharsets.UTF_8); + } + } + private Uri buildCapabilityUri(String capability, boolean withAuthority) { Uri.Builder builder = ROOT.buildUpon(); if (withAuthority) builder.authority(wearable.getLocalNodeId()); @@ -56,30 +91,110 @@ private Uri buildCapabilityUri(String capability, boolean withAuthority) { public Set getNodesForCapability(String capability) { DataHolder dataHolder = wearable.getDataItemsByUriAsHolder(buildCapabilityUri(capability, false), packageName); Set nodes = new HashSet<>(); - for (int i = 0; i < dataHolder.getCount(); i++) { - nodes.add(dataHolder.getString("host", i, 0)); + try{ + for (int i = 0; i < dataHolder.getCount(); i++) { + nodes.add(dataHolder.getString("host", i, 0)); + } + } finally { + dataHolder.close(); } - dataHolder.close(); return nodes; } public int add(String capability) { - if (this.capabilities.contains(capability)) { - return WearableStatusCodes.DUPLICATE_CAPABILITY; + return addWithType(capability, CapabilityType.DYNAMIC); +// if (this.capabilities.contains(capability)) { +// return WearableStatusCodes.DUPLICATE_CAPABILITY; +// } +// DataItemInternal dataItem = new DataItemInternal(buildCapabilityUri(capability, true)); +// DataItemRecord record = wearable.putDataItem(packageName, PackageUtils.firstSignatureDigest(context, packageName), wearable.getLocalNodeId(), dataItem); +// this.capabilities.add(capability); +// wearable.syncRecordToAll(record); +// return CommonStatusCodes.SUCCESS; + } + + public int addWithType(String capability, CapabilityType type) { + synchronized (lock) { + Uri uri = buildCapabilityUri(capability, true); + DataHolder existingData = wearable.getDataItemsByUriAsHolder(uri, packageName); + + try { + if (existingData.getCount() > 0) { + byte[] data = existingData.getByteArray("data", 0, 0); + CapabilityType existingType = CapabilityType.fromBytes(data); + + if (existingType == CapabilityType.STATIC || type == CapabilityType.DYNAMIC) { + return WearableStatusCodes.DUPLICATE_CAPABILITY; + } + } + } finally { + existingData.close(); + } + + DataItemInternal dataItem = new DataItemInternal(uri); + dataItem.data = type.toBytes(); + + DataItemRecord record = wearable.putDataItem( + packageName, + PackageUtils.firstSignatureDigest(context, packageName), + wearable.getLocalNodeId(), + dataItem + ); + + if (record != null) { + capabilities.add(capability); + wearable.syncRecordToAll(record); + Log.d(TAG, "Added capability: " + capability + " (type=" + type + ")"); + return CommonStatusCodes.SUCCESS; + } else { + Log.e(TAG, "Failed to add capability: " + capability); + return CommonStatusCodes.ERROR; + } } - DataItemInternal dataItem = new DataItemInternal(buildCapabilityUri(capability, true)); - DataItemRecord record = wearable.putDataItem(packageName, PackageUtils.firstSignatureDigest(context, packageName), wearable.getLocalNodeId(), dataItem); - this.capabilities.add(capability); - wearable.syncRecordToAll(record); - return CommonStatusCodes.SUCCESS; } public int remove(String capability) { - if (!this.capabilities.contains(capability)) { - return WearableStatusCodes.UNKNOWN_CAPABILITY; + synchronized (lock) { + if (!capabilities.contains(capability)) { + Uri uri = buildCapabilityUri(capability, true); + DataHolder existingData = wearable.getDataItemsByUriAsHolder(uri, packageName); + try { + if (existingData.getCount() == 0) { + Log.w(TAG, "Capability not found: " + capability); + return WearableStatusCodes.UNKNOWN_CAPABILITY; + } + } finally { + existingData.close(); + } + } + +// if (!this.capabilities.contains(capability)) { +// return WearableStatusCodes.UNKNOWN_CAPABILITY; +// } + wearable.deleteDataItems(buildCapabilityUri(capability, true), packageName); + capabilities.remove(capability); + Log.d(TAG, "Removed capability: " + capability); + return CommonStatusCodes.SUCCESS; + } + } + + public Set getLocalCapabilities() { + synchronized (lock) { + return new HashSet<>(capabilities); + } + } + + public boolean hasCapability(String capability) { + synchronized (lock) { + return capabilities.contains(capability); + } + } + + public void clearAll() { + synchronized (lock) { + for (String capability: new HashSet<>(capabilities)) { + remove(capability); + } } - wearable.deleteDataItems(buildCapabilityUri(capability, true), packageName); - capabilities.remove(capability); - return CommonStatusCodes.SUCCESS; } } diff --git a/play-services-wearable/core/src/main/java/org/microg/gms/wearable/WearableServiceImpl.java b/play-services-wearable/core/src/main/java/org/microg/gms/wearable/WearableServiceImpl.java index f0b13f44dd..1582177eab 100644 --- a/play-services-wearable/core/src/main/java/org/microg/gms/wearable/WearableServiceImpl.java +++ b/play-services-wearable/core/src/main/java/org/microg/gms/wearable/WearableServiceImpl.java @@ -26,14 +26,19 @@ import android.util.Log; import com.google.android.gms.common.api.Status; +import com.google.android.gms.common.data.DataHolder; import com.google.android.gms.tasks.Task; import com.google.android.gms.wearable.Asset; +import com.google.android.gms.wearable.CapabilityApi; import com.google.android.gms.wearable.ConnectionConfiguration; import com.google.android.gms.wearable.internal.*; import java.io.FileNotFoundException; import java.util.ArrayList; +import java.util.HashMap; +import java.util.HashSet; import java.util.List; +import java.util.Map; import java.util.Set; public class WearableServiceImpl extends IWearableService.Stub { @@ -308,6 +313,18 @@ public void addAccountToConsent(IWearableCallbacks callbacks, AddAccountToConsen } + @Override + public void someBoolUnknown(IWearableCallbacks callbacks) throws RemoteException { + // not sure what it is, but i thinking this is to do something with a certificate verification + postMain(callbacks, () -> { + try { + callbacks.onBooleanResponse(new BooleanResponse(0, true)); + } catch (Exception e) { + callbacks.onBooleanResponse(new BooleanResponse(8, false)); + } + }); + } + @Override public void logCounter(IWearableCallbacks callbacks, LogCounterRequest request) throws RemoteException { Log.d(TAG, "unimplemented Method logCounter: " @@ -372,41 +389,146 @@ public void getConnectedNodes(IWearableCallbacks callbacks) throws RemoteExcepti @Override public void getConnectedCapability(IWearableCallbacks callbacks, String capability, int nodeFilter) throws RemoteException { - Log.d(TAG, "unimplemented Method: getConnectedCapability " + capability + ", " + nodeFilter); + Log.d(TAG, "getConnectedCapability: " + capability + ", nodeFilter=" + nodeFilter); postMain(callbacks, () -> { - List nodes = new ArrayList<>(); - for (String host : capabilities.getNodesForCapability(capability)) { - nodes.add(new NodeParcelable(host, host)); + try { + List nodes = new ArrayList<>(); + Set nodeIds = capabilities.getNodesForCapability(capability); + + for (String nodeId : nodeIds) { + if (shouldIncludeNode(nodeId, nodeFilter)) { + nodes.add(new NodeParcelable(nodeId, nodeId)); + } + } + + CapabilityInfoParcelable capabilityInfo = new CapabilityInfoParcelable(capability, nodes); + callbacks.onGetCapabilityResponse(new GetCapabilityResponse(0, capabilityInfo)); + } catch (Exception e) { + Log.e(TAG, "getConnectedCapability failed", e); + callbacks.onGetCapabilityResponse(new GetCapabilityResponse(13, null)); } - CapabilityInfoParcelable capabilityInfo = new CapabilityInfoParcelable(capability, nodes); - callbacks.onGetCapabilityResponse(new GetCapabilityResponse(0, capabilityInfo)); }); } @Override public void getAllCapabilities(IWearableCallbacks callbacks, int nodeFilter) throws RemoteException { - Log.d(TAG, "unimplemented Method: getConnectedCapaibilties: " + nodeFilter); - callbacks.onGetAllCapabilitiesResponse(new GetAllCapabilitiesResponse()); +// Log.d(TAG, "unimplemented Method: getConnectedCapaibilties: " + nodeFilter); +// callbacks.onGetAllCapabilitiesResponse(new GetAllCapabilitiesResponse()); + + Log.d(TAG, "getAllCapabilities: nodeFilter=" + nodeFilter); + postMain(callbacks, () -> { + try { + Map capabilitiesMap = new HashMap<>(); + + DataHolder dataHolder = wearable.getDataItemsByUriAsHolder( + Uri.parse("wear:/capabilities/"), packageName + ); + + try { + Set processedCapabilities = new HashSet<>(); + + for (int i = 0; i < dataHolder.getCount(); i++) { + String uri = dataHolder.getString("path", i, 0); + if (uri != null && uri.startsWith("/capabilities/")) { + String[] segments = uri.split("/"); + if (segments.length >= 4) { + String capabilityName = Uri.decode(segments[segments.length - 1]); + if (!processedCapabilities.contains(capabilityName)) { + processedCapabilities.add(capabilityName); + + List nodes = new ArrayList<>(); + Set nodeIds = capabilities.getNodesForCapability(capabilityName); + + for (String nodeId: nodeIds) { + if (shouldIncludeNode(nodeId, nodeFilter)){ + nodes.add(new NodeParcelable(nodeId, nodeId)); + } + } + + if (!nodes.isEmpty() || nodeFilter == 0) { + capabilitiesMap.put(capabilityName, new CapabilityInfoParcelable(capabilityName, nodes)); + } + } + } + } + } + } finally { + dataHolder.close(); + } + } catch (Exception e) { + Log.e(TAG, "getAllCapabilities failed", e); + callbacks.onGetAllCapabilitiesResponse(new GetAllCapabilitiesResponse(13, new ArrayList<>())); + } + }); + } + + private boolean shouldIncludeNode(String nodeId, int nodeFilter) { + switch (nodeFilter) { + case 0: + return true; + case 1: + case 2: + ConnectionConfiguration[] configs = wearable.getConfigurations(); + if (configs != null) { + for (ConnectionConfiguration config: configs) { + if (nodeId.equals(config.nodeId) && config.connected) { + return true; + } + } + } + default: + Log.w(TAG, "Unknown node filter: " + nodeFilter + ", including all nodes"); + return true; + } } @Override public void addLocalCapability(IWearableCallbacks callbacks, String capability) throws RemoteException { - Log.d(TAG, "unimplemented Method: addLocalCapability: " + capability); +// Log.d(TAG, "unimplemented Method: addLocalCapability: " + capability); + Log.d(TAG, "addLocalCapability: " + capability); + this.wearable.networkHandler.post(new CallbackRunnable(callbacks) { @Override public void run(IWearableCallbacks callbacks) throws RemoteException { - callbacks.onAddLocalCapabilityResponse(new AddLocalCapabilityResponse(capabilities.add(capability))); + try { + int statusCode = capabilities.add(capability); + callbacks.onAddLocalCapabilityResponse(new AddLocalCapabilityResponse(statusCode)); + + if (statusCode == 0) { + Log.d(TAG, "Successfully added local capability: " + capability); + } else { + Log.w(TAG, "Failed to add local capability: " + capability + ", status=" + statusCode); + } + } catch (Exception e) { + Log.e(TAG, "addLocalCapability exception", e); + callbacks.onAddLocalCapabilityResponse(new AddLocalCapabilityResponse(8)); + } } }); } @Override public void removeLocalCapability(IWearableCallbacks callbacks, String capability) throws RemoteException { - Log.d(TAG, "unimplemented Method: removeLocalCapability: " + capability); +// Log.d(TAG, "unimplemented Method: removeLocalCapability: " + capability); + Log.d(TAG, "removeLocalCapability: " + capability); + this.wearable.networkHandler.post(new CallbackRunnable(callbacks) { @Override public void run(IWearableCallbacks callbacks) throws RemoteException { - callbacks.onRemoveLocalCapabilityResponse(new RemoveLocalCapabilityResponse(capabilities.remove(capability))); + try { + int statusCode = capabilities.remove(capability); + callbacks.onRemoveLocalCapabilityResponse(new RemoveLocalCapabilityResponse(statusCode)); + + if (statusCode == 0) { + Log.d(TAG, "Successfully removed local capability: " + capability); + } else { + Log.w(TAG, "Failed to remove local capability: " + capability + ", status=" + statusCode); + } + + } catch (Exception e) { + Log.e(TAG, "removeLocalCapability exception", e); + callbacks.onRemoveLocalCapabilityResponse(new RemoveLocalCapabilityResponse(8)); + } } }); } diff --git a/play-services-wearable/src/main/aidl/com/google/android/gms/wearable/internal/IWearableService.aidl b/play-services-wearable/src/main/aidl/com/google/android/gms/wearable/internal/IWearableService.aidl index 31c9fee3c7..c7398bfd8c 100644 --- a/play-services-wearable/src/main/aidl/com/google/android/gms/wearable/internal/IWearableService.aidl +++ b/play-services-wearable/src/main/aidl/com/google/android/gms/wearable/internal/IWearableService.aidl @@ -83,6 +83,8 @@ interface IWearableService { void getConsentStatus(IWearableCallbacks callbacks) = 64; void addAccountToConsent(IWearableCallbacks callbacks, in AddAccountToConsentRequest request) = 65; + void someBoolUnknown(IWearableCallbacks callbacks) = 84; // cannot figure out name + void logCounter(IWearableCallbacks callbacks, in LogCounterRequest request) = 105; void logEvent(IWearableCallbacks callbacks, in LogEventRequest request) = 106; void logTimer(IWearableCallbacks callbacks, in LogTimerRequest request) = 107; diff --git a/play-services-wearable/src/main/java/com/google/android/gms/wearable/ConnectionConfiguration.java b/play-services-wearable/src/main/java/com/google/android/gms/wearable/ConnectionConfiguration.java index 214481da2b..cc2dc8f742 100644 --- a/play-services-wearable/src/main/java/com/google/android/gms/wearable/ConnectionConfiguration.java +++ b/play-services-wearable/src/main/java/com/google/android/gms/wearable/ConnectionConfiguration.java @@ -19,20 +19,22 @@ import org.microg.safeparcel.AutoSafeParcelable; import org.microg.safeparcel.SafeParceled; +import java.util.List; + public class ConnectionConfiguration extends AutoSafeParcelable { @SafeParceled(1) private int versionCode = 1; @SafeParceled(2) - public final String name; + public String name; @SafeParceled(3) - public final String address; + public String address; @SafeParceled(4) - public final int type; + public int type; @SafeParceled(5) - public final int role; + public int role; @SafeParceled(6) - public final boolean enabled; + public boolean enabled; @SafeParceled(7) public boolean connected = false; @SafeParceled(8) @@ -41,28 +43,62 @@ public class ConnectionConfiguration extends AutoSafeParcelable { public boolean btlePriority = true; @SafeParceled(10) public String nodeId; + @SafeParceled(11) + public String packageName; + @SafeParceled(12) + public int connectionRetryStrategy; + @SafeParceled(13) + public List allowedConfigPackages; + @SafeParceled(14) + public boolean migrating; + @SafeParceled(15) + public boolean dataItemSyncEnabled; + @SafeParceled(16) + public ConnectionRestrictions connectionRestrictions; + @SafeParceled(17) + public boolean removeConnectionWhenBondRemovedByUser; + @SafeParceled(18) + public ConnectionDelayFilters connectionDelayFilters; + @SafeParceled(19) + public int maxSupportedRemoteAndroidSdkVersion; private ConnectionConfiguration() { - name = address = null; - type = role = 0; - enabled = false; } public ConnectionConfiguration(String name, String address, int type, int role, boolean enabled) { - this.name = name; - this.address = address; - this.type = type; - this.role = role; - this.enabled = enabled; + this(name, address, type, role, enabled, false, null, false, null, null, 0, null, false, false, null, false, null, 0); } public ConnectionConfiguration(String name, String address, int type, int role, boolean enabled, String nodeId) { + this(name, address, type, role, enabled, false, null, false, nodeId, null, 0, null, false, false, null, false, null, 0); + } + + public ConnectionConfiguration(String name, String address, int type, int role, boolean enabled, + boolean connected, String peerNodeId, boolean btlePriority, + String nodeId, String packageName, int connectionRetryStrategy, + List allowedConfigPackages, boolean migrating, + boolean dataItemSyncEnabled, ConnectionRestrictions connectionRestrictions, + boolean removeConnectionWhenBondRemovedByUser, + ConnectionDelayFilters connectionDelayFilters, + int maxSupportedRemoteAndroidSdkVersion) { this.name = name; this.address = address; this.type = type; this.role = role; this.enabled = enabled; + this.connected = connected; + this.peerNodeId = peerNodeId; + this.btlePriority = btlePriority; this.nodeId = nodeId; + this.packageName = packageName; + this.connectionRetryStrategy = connectionRetryStrategy; + this.allowedConfigPackages = allowedConfigPackages; + this.migrating = migrating; + this.dataItemSyncEnabled = dataItemSyncEnabled; + this.connectionRestrictions = connectionRestrictions; + this.removeConnectionWhenBondRemovedByUser = removeConnectionWhenBondRemovedByUser; + this.connectionDelayFilters = connectionDelayFilters; + this.maxSupportedRemoteAndroidSdkVersion = maxSupportedRemoteAndroidSdkVersion; } @Override @@ -77,6 +113,14 @@ public String toString() { sb.append(", peerNodeId='").append(peerNodeId).append('\''); sb.append(", btlePriority=").append(btlePriority); sb.append(", nodeId='").append(nodeId).append('\''); + sb.append(", packageName='").append(packageName).append('\''); + sb.append(", connectionRetryStrategy='").append(connectionRetryStrategy).append('\''); + sb.append(", allowedConfigPackages='").append(allowedConfigPackages).append('\''); + sb.append(", migrating='").append(migrating).append('\''); + sb.append(", dataItemSyncEnabled='").append(dataItemSyncEnabled).append('\''); + sb.append(", connectionRestrictions='").append(connectionRestrictions).append('\''); + sb.append(", removeConnectionWhenBondRemovedByUser='").append(removeConnectionWhenBondRemovedByUser).append('\''); + sb.append(", maxSupportedRemoteAndroidSdkVersion='").append(maxSupportedRemoteAndroidSdkVersion).append('\''); sb.append('}'); return sb.toString(); } diff --git a/play-services-wearable/src/main/java/com/google/android/gms/wearable/ConnectionDelayFilters.java b/play-services-wearable/src/main/java/com/google/android/gms/wearable/ConnectionDelayFilters.java new file mode 100644 index 0000000000..60ddb670b4 --- /dev/null +++ b/play-services-wearable/src/main/java/com/google/android/gms/wearable/ConnectionDelayFilters.java @@ -0,0 +1,40 @@ +package com.google.android.gms.wearable; + +import org.microg.safeparcel.AutoSafeParcelable; +import org.microg.safeparcel.SafeParceled; + +import java.util.List; +import java.util.Objects; + +public class ConnectionDelayFilters extends AutoSafeParcelable { + @SafeParceled(1) + public List dataItemFilters; + + private ConnectionDelayFilters() {} + + public ConnectionDelayFilters(List dataItemFilters) { + this.dataItemFilters = dataItemFilters; + } + + @Override + public boolean equals(Object obj) { + if (obj instanceof ConnectionDelayFilters) { + return Objects.equals(this.dataItemFilters, ((ConnectionDelayFilters) obj).dataItemFilters); + } + return false; + } + + @Override + public int hashCode() { + return Objects.hash(dataItemFilters); + } + + @Override + public String toString() { + return "ConnectionDelayFilters{" + + "dataItemFilters=" + dataItemFilters + + '}'; + } + + public static final Creator CREATOR = new AutoCreator<>(ConnectionDelayFilters.class); +} \ No newline at end of file diff --git a/play-services-wearable/src/main/java/com/google/android/gms/wearable/ConnectionRestrictions.java b/play-services-wearable/src/main/java/com/google/android/gms/wearable/ConnectionRestrictions.java new file mode 100644 index 0000000000..5cf63db2c5 --- /dev/null +++ b/play-services-wearable/src/main/java/com/google/android/gms/wearable/ConnectionRestrictions.java @@ -0,0 +1,36 @@ +package com.google.android.gms.wearable; + +import org.microg.safeparcel.AutoSafeParcelable; +import org.microg.safeparcel.SafeParceled; + +import java.util.List; + +public class ConnectionRestrictions extends AutoSafeParcelable { + @SafeParceled(1) + public List allowedDataItemFilters; + @SafeParceled(2) + public List allowedCapabilities; + @SafeParceled(3) + public List allowedPackages; + + private ConnectionRestrictions() {} + + public ConnectionRestrictions(List allowedDataItemFilters, + List allowedCapabilities, + List allowedPackages) { + this.allowedDataItemFilters = allowedDataItemFilters; + this.allowedCapabilities = allowedCapabilities; + this.allowedPackages = allowedPackages; + } + + @Override + public String toString() { + return "ConnectionRestrictions{" + + "allowedDataItemFilters=" + allowedDataItemFilters + + ", allowedCapabilities=" + allowedCapabilities + + ", allowedPackages=" + allowedPackages + + '}'; + } + + public static final Creator CREATOR = new AutoCreator<>(ConnectionRestrictions.class); +} \ No newline at end of file diff --git a/play-services-wearable/src/main/java/com/google/android/gms/wearable/DataItemFilter.java b/play-services-wearable/src/main/java/com/google/android/gms/wearable/DataItemFilter.java new file mode 100644 index 0000000000..05f65cb9a8 --- /dev/null +++ b/play-services-wearable/src/main/java/com/google/android/gms/wearable/DataItemFilter.java @@ -0,0 +1,46 @@ +package com.google.android.gms.wearable; + +import android.net.Uri; + +import org.microg.safeparcel.AutoSafeParcelable; +import org.microg.safeparcel.SafeParceled; + +import java.util.Objects; + +public class DataItemFilter extends AutoSafeParcelable { + @SafeParceled(1) + public Uri uri; + @SafeParceled(2) + public int filterType; + + private DataItemFilter() {} + + public DataItemFilter(Uri uri, int filterType) { + this.uri = uri; + this.filterType = filterType; + } + + @Override + public boolean equals(Object obj) { + if (!(obj instanceof DataItemFilter)) { + return false; + } + DataItemFilter other = (DataItemFilter) obj; + return Objects.equals(this.uri, other.uri) && this.filterType == other.filterType; + } + + @Override + public int hashCode() { + return Objects.hash(uri, filterType); + } + + @Override + public String toString() { + return "DataItemFilter{" + + "uri=" + uri + + ", filterType=" + filterType + + '}'; + } + + public static final Creator CREATOR = new AutoCreator<>(DataItemFilter.class); +} \ No newline at end of file diff --git a/play-services-wearable/src/main/java/com/google/android/gms/wearable/internal/BooleanResponse.java b/play-services-wearable/src/main/java/com/google/android/gms/wearable/internal/BooleanResponse.java index 4a18ecf2b6..b6374c7598 100644 --- a/play-services-wearable/src/main/java/com/google/android/gms/wearable/internal/BooleanResponse.java +++ b/play-services-wearable/src/main/java/com/google/android/gms/wearable/internal/BooleanResponse.java @@ -19,7 +19,37 @@ import org.microg.safeparcel.AutoSafeParcelable; import org.microg.safeparcel.SafeParceled; +import java.util.Objects; + public class BooleanResponse extends AutoSafeParcelable { + @SafeParceled(1) + public int status; + @SafeParceled(2) + public boolean result; + + private BooleanResponse() {} + + public BooleanResponse(int status, boolean result) { + this.status = status; + this.result = result; + } + + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + if (!(obj instanceof BooleanResponse)) { + return false; + } + BooleanResponse other = (BooleanResponse) obj; + return this.status == other.status && this.result == other.result; + } + + @Override + public int hashCode() { + return Objects.hash(status, result); + } public static final Creator CREATOR = new AutoCreator(BooleanResponse.class); } diff --git a/play-services-wearable/src/main/java/com/google/android/gms/wearable/internal/GetAllCapabilitiesResponse.java b/play-services-wearable/src/main/java/com/google/android/gms/wearable/internal/GetAllCapabilitiesResponse.java index 56ed071cc2..86a5b4bec8 100644 --- a/play-services-wearable/src/main/java/com/google/android/gms/wearable/internal/GetAllCapabilitiesResponse.java +++ b/play-services-wearable/src/main/java/com/google/android/gms/wearable/internal/GetAllCapabilitiesResponse.java @@ -29,5 +29,12 @@ public class GetAllCapabilitiesResponse extends AutoSafeParcelable { @Field(3) public List capabilities; + private GetAllCapabilitiesResponse() {} + + public GetAllCapabilitiesResponse(int statusCode, List capabilities) { + this.statusCode = statusCode; + this.capabilities = capabilities; + } + public static final Creator CREATOR = findCreator(GetAllCapabilitiesResponse.class); } From f9c59c3c09563689dc5c21a0cb3e0f4b93ca8f96 Mon Sep 17 00:00:00 2001 From: deadYokai Date: Tue, 30 Dec 2025 08:41:54 +0200 Subject: [PATCH 06/29] added services `getNodeId`, `sendRequest` and option variants of `sendRequest`, `sendMessage` prepairing ConnectionConfiguration to bluetooth stuff, to proper pair devices filled some Parcable classes --- .../wearable/ConfigurationDatabaseHelper.java | 239 ++++++++++++++++-- .../org/microg/gms/wearable/WearableImpl.java | 28 +- .../gms/wearable/WearableServiceImpl.java | 71 +++++- .../android/gms/wearable/MessageOptions.aidl | 3 + .../wearable/internal/IWearableService.aidl | 11 +- .../android/gms/wearable/MessageOptions.java | 35 +++ .../GetCompanionPackageForNodeResponse.java | 13 + .../wearable/internal/GetNodeIdResponse.java | 13 + .../gms/wearable/internal/RpcResponse.java | 14 + 9 files changed, 398 insertions(+), 29 deletions(-) create mode 100644 play-services-wearable/src/main/aidl/com/google/android/gms/wearable/MessageOptions.aidl create mode 100644 play-services-wearable/src/main/java/com/google/android/gms/wearable/MessageOptions.java diff --git a/play-services-wearable/core/src/main/java/org/microg/gms/wearable/ConfigurationDatabaseHelper.java b/play-services-wearable/core/src/main/java/org/microg/gms/wearable/ConfigurationDatabaseHelper.java index 4aa4b58b97..ad1f895f9f 100644 --- a/play-services-wearable/core/src/main/java/org/microg/gms/wearable/ConfigurationDatabaseHelper.java +++ b/play-services-wearable/core/src/main/java/org/microg/gms/wearable/ConfigurationDatabaseHelper.java @@ -19,8 +19,10 @@ import android.content.ContentValues; import android.content.Context; import android.database.Cursor; +import android.database.SQLException; import android.database.sqlite.SQLiteDatabase; import android.database.sqlite.SQLiteOpenHelper; +import android.util.Log; import com.google.android.gms.wearable.ConnectionConfiguration; @@ -29,34 +31,154 @@ import java.util.Random; public class ConfigurationDatabaseHelper extends SQLiteOpenHelper { + private static final String TAG = "ConfigDB"; public static final String NULL_STRING = "NULL_STRING"; public static final String TABLE_NAME = "connectionConfigurations"; public static final String BY_NAME = "name=?"; + private static final String COLUMN_ID = "_id"; + private static final String COLUMN_ANDROID_ID = "androidId"; + private static final String COLUMN_ALLOWED_CONFIG_PACKAGES = "allowedConfigPackages"; + private static final String COLUMN_NAME = "name"; + private static final String COLUMN_PAIRED_BT_ADDRESS = "pairedBtAddress"; + private static final String COLUMN_CONNECTION_TYPE = "connectionType"; + private static final String COLUMN_ROLE = "role"; + private static final String COLUMN_CONNECTION_ENABLED = "connectionEnabled"; + private static final String COLUMN_NODE_ID = "nodeId"; + private static final String COLUMN_CRYPTO = "crypto"; + private static final String COLUMN_PACKAGE_NAME = "packageName"; + private static final String COLUMN_IS_MIGRATING = "isMigrating"; + private static final String COLUMN_DATA_ITEM_SYNC_ENABLED = "dataItemSyncEnabled"; + private static final String COLUMN_RESTRICTIONS = "restrictions"; + private static final String COLUMN_REMOVE_CONNECTION_WHEN_BOND_REMOVED = "removeConnectionWhenBondRemovedByUser"; + private static final String COLUMN_CONNECTION_DELAY_FILTERS = "connectionDelayFilters"; + private static final String COLUMN_MAX_SUPPORTED_REMOTE_ANDROID_SDK = "maxSupportedRemoteAndroidSdkVersion"; + + public static final int TYPE_BLUETOOTH_RFCOMM = 1; + public static final int TYPE_NETWORK = 2; + public static final int TYPE_BLE = 3; + public static final int TYPE_CLOUD = 4; + public static final int TYPE_BLUETOOTH_L2CAP = 5; + + public static final int ROLE_CLIENT = 1; + public static final int ROLE_SERVER = 2; + public ConfigurationDatabaseHelper(Context context) { super(context, "connectionconfig.db", null, 2); } @Override public void onCreate(SQLiteDatabase db) { - db.execSQL("CREATE TABLE connectionConfigurations (_id INTEGER PRIMARY KEY AUTOINCREMENT,androidId TEXT,name TEXT NOT NULL,pairedBtAddress TEXT NOT NULL,connectionType INTEGER NOT NULL,role INTEGER NOT NULL,connectionEnabled INTEGER NOT NULL,nodeId TEXT, UNIQUE(name) ON CONFLICT REPLACE);"); +// db.execSQL("CREATE TABLE connectionConfigurations (_id INTEGER PRIMARY KEY AUTOINCREMENT,androidId TEXT,name TEXT NOT NULL,pairedBtAddress TEXT NOT NULL,connectionType INTEGER NOT NULL,role INTEGER NOT NULL,connectionEnabled INTEGER NOT NULL,nodeId TEXT, UNIQUE(name) ON CONFLICT REPLACE);"); + try { + db.execSQL("CREATE TABLE " + TABLE_NAME + " (" + + COLUMN_ID + " INTEGER PRIMARY KEY AUTOINCREMENT," + + COLUMN_ANDROID_ID + " TEXT," + + COLUMN_ALLOWED_CONFIG_PACKAGES + " TEXT," + + COLUMN_NAME + " TEXT NOT NULL," + + COLUMN_PAIRED_BT_ADDRESS + " TEXT NOT NULL," + + COLUMN_CONNECTION_TYPE + " INTEGER NOT NULL," + + COLUMN_ROLE + " INTEGER NOT NULL," + + COLUMN_CONNECTION_ENABLED + " INTEGER NOT NULL," + + COLUMN_NODE_ID + " TEXT," + + COLUMN_CRYPTO + " TEXT," + + COLUMN_PACKAGE_NAME + " TEXT," + + COLUMN_IS_MIGRATING + " INTEGER DEFAULT 0," + + COLUMN_DATA_ITEM_SYNC_ENABLED + " INTEGER DEFAULT 1," + + COLUMN_RESTRICTIONS + " BLOB," + + COLUMN_REMOVE_CONNECTION_WHEN_BOND_REMOVED + " INTEGER DEFAULT 1," + + COLUMN_CONNECTION_DELAY_FILTERS + " BLOB," + + COLUMN_MAX_SUPPORTED_REMOTE_ANDROID_SDK + " INTEGER DEFAULT 0," + + " UNIQUE(" + COLUMN_NAME + ") ON CONFLICT REPLACE);"); + } catch (SQLException e) { + Log.e(TAG, "Error creating database", e); + throw e; + } + } @Override public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) { + try { + Log.i(TAG, "Upgrading database from version " + oldVersion + " to " + newVersion); + + if (oldVersion < 2) { + db.execSQL(String.format("ALTER TABLE %s ADD COLUMN %s TEXT;", + TABLE_NAME, COLUMN_NODE_ID)); + oldVersion = 2; + } + + if (oldVersion < 3) { + db.execSQL(String.format("ALTER TABLE %s ADD COLUMN %s TEXT;", + TABLE_NAME, COLUMN_CRYPTO)); + oldVersion = 3; + } + + if (oldVersion < 4) { + db.execSQL(String.format("ALTER TABLE %s ADD COLUMN %s TEXT;", + TABLE_NAME, COLUMN_PACKAGE_NAME)); + oldVersion = 4; + } + + if (oldVersion < 5) { + db.execSQL(String.format("ALTER TABLE %s ADD COLUMN %s TEXT;", + TABLE_NAME, COLUMN_ALLOWED_CONFIG_PACKAGES)); + oldVersion = 5; + } + + if (oldVersion < 6) { + db.execSQL(String.format("ALTER TABLE %s ADD COLUMN %s INTEGER DEFAULT 0;", + TABLE_NAME, COLUMN_IS_MIGRATING)); + oldVersion = 6; + } + + if (oldVersion < 7) { + db.execSQL(String.format("ALTER TABLE %s ADD COLUMN %s INTEGER DEFAULT 1;", + TABLE_NAME, COLUMN_DATA_ITEM_SYNC_ENABLED)); + oldVersion = 7; + } + + if (oldVersion < 8) { + db.execSQL(String.format("ALTER TABLE %s ADD COLUMN %s BLOB;", + TABLE_NAME, COLUMN_RESTRICTIONS)); + oldVersion = 8; + } + + if (oldVersion < 9) { + db.execSQL(String.format("ALTER TABLE %s ADD COLUMN %s INTEGER DEFAULT 1;", + TABLE_NAME, COLUMN_REMOVE_CONNECTION_WHEN_BOND_REMOVED)); + oldVersion = 9; + } + + if (oldVersion < 10) { + db.execSQL(String.format("ALTER TABLE %s ADD COLUMN %s BLOB;", + TABLE_NAME, COLUMN_CONNECTION_DELAY_FILTERS)); + oldVersion = 10; + } + + if (oldVersion < 11) { + db.execSQL(String.format("ALTER TABLE %s ADD COLUMN %s INTEGER DEFAULT 0;", + TABLE_NAME, COLUMN_MAX_SUPPORTED_REMOTE_ANDROID_SDK)); + } + } catch (SQLException e) { + Log.e(TAG, "Error upgrading database", e); + throw e; + } } private static ConnectionConfiguration configFromCursor(final Cursor cursor) { - String name = cursor.getString(cursor.getColumnIndexOrThrow("name")); - String pairedBtAddress = cursor.getString(cursor.getColumnIndexOrThrow("pairedBtAddress")); - int connectionType = cursor.getInt(cursor.getColumnIndexOrThrow("connectionType")); - int role = cursor.getInt(cursor.getColumnIndexOrThrow("role")); - int enabled = cursor.getInt(cursor.getColumnIndexOrThrow("connectionEnabled")); - String nodeId = cursor.getString(cursor.getColumnIndexOrThrow("nodeId")); + String name = cursor.getString(cursor.getColumnIndexOrThrow(COLUMN_NAME)); + String pairedBtAddress = cursor.getString(cursor.getColumnIndexOrThrow(COLUMN_PAIRED_BT_ADDRESS)); + int connectionType = cursor.getInt(cursor.getColumnIndexOrThrow(COLUMN_CONNECTION_TYPE)); + int role = cursor.getInt(cursor.getColumnIndexOrThrow(COLUMN_ROLE)); + int enabled = cursor.getInt(cursor.getColumnIndexOrThrow(COLUMN_CONNECTION_ENABLED)); + String nodeId = cursor.getString(cursor.getColumnIndexOrThrow(COLUMN_NODE_ID)); + if (NULL_STRING.equals(name)) name = null; if (NULL_STRING.equals(pairedBtAddress)) pairedBtAddress = null; + return new ConnectionConfiguration(name, pairedBtAddress, connectionType, role, enabled > 0, nodeId); } @@ -77,45 +199,112 @@ public void putConfiguration(ConnectionConfiguration config) { public void putConfiguration(ConnectionConfiguration config, String oldNodeId) { ContentValues contentValues = new ContentValues(); + if (config.name != null) { - contentValues.put("name", config.name); + contentValues.put(COLUMN_NAME, config.name); } else if (config.role == 2) { - contentValues.put("name", "server"); + contentValues.put(COLUMN_NAME, "server"); } else { - contentValues.put("name", "NULL_STRING"); + contentValues.put(COLUMN_NAME, NULL_STRING); } + if (config.address != null) { - contentValues.put("pairedBtAddress", config.address); + contentValues.put(COLUMN_PAIRED_BT_ADDRESS, config.address); } else { - contentValues.put("pairedBtAddress", "NULL_STRING"); + contentValues.put(COLUMN_PAIRED_BT_ADDRESS, NULL_STRING); } - contentValues.put("connectionType", config.type); - contentValues.put("role", config.role); - contentValues.put("connectionEnabled", true); - contentValues.put("nodeId", config.nodeId); + + contentValues.put(COLUMN_CONNECTION_TYPE, config.type); + contentValues.put(COLUMN_ROLE, config.role); + contentValues.put(COLUMN_CONNECTION_ENABLED, config.enabled ? 1 : 0); + contentValues.put(COLUMN_NODE_ID, config.nodeId); + if (oldNodeId == null) { getWritableDatabase().insert(TABLE_NAME, null, contentValues); } else { - getWritableDatabase().update(TABLE_NAME, contentValues, "nodeId=?", new String[]{oldNodeId}); + getWritableDatabase().update(TABLE_NAME, contentValues, COLUMN_NODE_ID + "=?", new String[]{oldNodeId}); } } public ConnectionConfiguration[] getAllConfigurations() { Cursor cursor = getReadableDatabase().query(TABLE_NAME, null, null, null, null, null, null); if (cursor != null) { - List configurations = new ArrayList(); - while (cursor.moveToNext()) { - configurations.add(configFromCursor(cursor)); + try { + List configurations = new ArrayList<>(); + while (cursor.moveToNext()) { + configurations.add(configFromCursor(cursor)); + } + return configurations.toArray(new ConnectionConfiguration[0]); + } finally { + cursor.close(); } - cursor.close(); - return configurations.toArray(new ConnectionConfiguration[configurations.size()]); + } + return new ConnectionConfiguration[0]; + } + + public void setEnabledState(String packageName, boolean enabled) { + Log.d(TAG, "setEnabledState(" + packageName + ", " + enabled + ")"); + + ConnectionConfiguration oldConfig = getConfiguration(packageName); + ContentValues values = new ContentValues(); + values.put(COLUMN_CONNECTION_ENABLED, enabled ? 1 : 0); + getWritableDatabase().updateWithOnConflict(TABLE_NAME, values, BY_NAME, new String[]{packageName != null ? packageName : NULL_STRING}, SQLiteDatabase.CONFLICT_REPLACE); + + ConnectionConfiguration config = getConfiguration(packageName); + Log.d(TAG, "setConnectionEnabled configName=" + packageName + ", connectionEnabled=" + enabled + ", originalConfig=" + oldConfig + ", updatedConfig=" + config); + + switch (config.type) { + case TYPE_CLOUD: + return; // abort on cloud type + case TYPE_BLUETOOTH_RFCOMM: + case TYPE_BLUETOOTH_L2CAP: + handleLegacy(config, enabled); + break; + case TYPE_NETWORK: + handleNetwork(config, enabled); + break; + case TYPE_BLE: + handleBle(config, enabled); + break; + default: + Log.w(TAG, "unimplemented config type: " + config.type); + } + } + + private void handleBle(ConnectionConfiguration config, boolean enabled) { + if (config.role == ROLE_CLIENT) { + if (enabled) { + // add ble client config + } else { + // remove ble client config + } + } else if (config.role == ROLE_SERVER) { + // update ble server config + } + } + + private void handleNetwork(ConnectionConfiguration config, boolean enabled) { + if (enabled) { + // initialize new network service } else { - return null; + // close network service } } - public void setEnabledState(String name, boolean enabled) { - getWritableDatabase().execSQL("UPDATE connectionConfigurations SET connectionEnabled=? WHERE name=?", new String[]{enabled ? "1" : "0", name}); + private void handleLegacy(ConnectionConfiguration config, boolean enabled) { + if (config.role == ROLE_CLIENT) { + if (enabled) { + // Add/Retry bluetooth client config + } else { + // remove bluetooth client config + } + } else if (config.role == ROLE_SERVER) { + if (enabled) { + // add bluetooth server config + } else { + // remove bluetooth server config + } + } } public int deleteConfiguration(String name) { diff --git a/play-services-wearable/core/src/main/java/org/microg/gms/wearable/WearableImpl.java b/play-services-wearable/core/src/main/java/org/microg/gms/wearable/WearableImpl.java index 1f0ed12669..da8b9c373e 100644 --- a/play-services-wearable/core/src/main/java/org/microg/gms/wearable/WearableImpl.java +++ b/play-services-wearable/core/src/main/java/org/microg/gms/wearable/WearableImpl.java @@ -33,6 +33,7 @@ import com.google.android.gms.common.data.DataHolder; import com.google.android.gms.wearable.Asset; import com.google.android.gms.wearable.ConnectionConfiguration; +import com.google.android.gms.wearable.MessageOptions; import com.google.android.gms.wearable.Node; import com.google.android.gms.wearable.internal.IWearableListener; import com.google.android.gms.wearable.internal.MessageEventParcelable; @@ -77,7 +78,7 @@ public class WearableImpl { private static final String TAG = "GmsWear"; - private static final int WEAR_TCP_PORT = 5601; + public static final int WEAR_TCP_PORT = 5601; private final Context context; private final NodeDatabaseHelper nodeDatabase; @@ -194,6 +195,18 @@ private String calculateDigest(byte[] data) { } } + public synchronized ConnectionConfiguration getConfiguration(String address) { + if (configurations == null) { + configurations = configDatabase.getAllConfigurations(); + } + + for (ConnectionConfiguration configuration : configurations) { + if (configuration.address.equals(address)) return configuration; + } + + return null; + } + public synchronized ConnectionConfiguration[] getConfigurations() { if (configurations == null) { configurations = configDatabase.getAllConfigurations(); @@ -535,6 +548,13 @@ public void createConnection(ConnectionConfiguration config) { Log.d(TAG, "putConfig[nyp]: " + config); configDatabase.putConfiguration(config); configurationsUpdated = true; + + if (configurations != null) { + ConnectionConfiguration[] newConfigs = new ConnectionConfiguration[configurations.length + 1]; + System.arraycopy(configurations, 0, newConfigs, 0, configurations.length); + newConfigs[configurations.length] = config; + configurations = newConfigs; + } } public int deleteDataItems(Uri uri, String packageName) { @@ -595,7 +615,7 @@ private void closeConnection(String nodeId) { Log.d(TAG, "Closed connection to " + nodeId + " on error"); } - public int sendMessage(String packageName, String targetNodeId, String path, byte[] data) { + public int sendMessage(String packageName, String targetNodeId, String path, byte[] data, MessageOptions options) { if (activeConnections.containsKey(targetNodeId)) { WearableConnection connection = activeConnections.get(targetNodeId); RpcHelper.RpcConnectionState state = rpcHelper.useConnectionState(packageName, targetNodeId, path); @@ -621,6 +641,10 @@ public int sendMessage(String packageName, String targetNodeId, String path, byt return -1; } + public int sendRequest(String packageName, String targetNodeId, String path, byte[] data, MessageOptions options) { + return -1; + } + public void stop() { try { this.networkHandlerLock.await(); diff --git a/play-services-wearable/core/src/main/java/org/microg/gms/wearable/WearableServiceImpl.java b/play-services-wearable/core/src/main/java/org/microg/gms/wearable/WearableServiceImpl.java index 1582177eab..1d48c3815f 100644 --- a/play-services-wearable/core/src/main/java/org/microg/gms/wearable/WearableServiceImpl.java +++ b/play-services-wearable/core/src/main/java/org/microg/gms/wearable/WearableServiceImpl.java @@ -31,6 +31,7 @@ import com.google.android.gms.wearable.Asset; import com.google.android.gms.wearable.CapabilityApi; import com.google.android.gms.wearable.ConnectionConfiguration; +import com.google.android.gms.wearable.MessageOptions; import com.google.android.gms.wearable.internal.*; import java.io.FileNotFoundException; @@ -195,13 +196,19 @@ public void run(IWearableCallbacks callbacks) throws RemoteException { @Override public void sendMessage(IWearableCallbacks callbacks, final String targetNodeId, final String path, final byte[] data) throws RemoteException { + Log.d(TAG, "sendMessage: " + targetNodeId + " / " + path + ": " + (data == null ? null : Base64.encodeToString(data, Base64.NO_WRAP))); + sendMessageWithOptions(callbacks, targetNodeId, path, data, new MessageOptions(0)); + } + + @Override + public void sendMessageWithOptions(IWearableCallbacks callbacks, final String targetNodeId, final String path, final byte[] data, MessageOptions options) throws RemoteException { Log.d(TAG, "sendMessage: " + targetNodeId + " / " + path + ": " + (data == null ? null : Base64.encodeToString(data, Base64.NO_WRAP))); this.wearable.networkHandler.post(new CallbackRunnable(callbacks) { @Override public void run(IWearableCallbacks callbacks) throws RemoteException { SendMessageResponse sendMessageResponse = new SendMessageResponse(); try { - sendMessageResponse.requestId = wearable.sendMessage(packageName, targetNodeId, path, data); + sendMessageResponse.requestId = wearable.sendMessage(packageName, targetNodeId, path, data, options); if (sendMessageResponse.requestId == -1) { sendMessageResponse.statusCode = 4000; } @@ -219,6 +226,44 @@ public void run(IWearableCallbacks callbacks) throws RemoteException { }); } + @Override + public void sendRequest(IWearableCallbacks callbacks, final String targetNodeId, final String path, final byte[] data) throws RemoteException { + Log.d(TAG, "sendRequest: " + targetNodeId + " / " + path + ": " + (data == null ? null : Base64.encodeToString(data, Base64.NO_WRAP))); + sendRequestWithOptions(callbacks, targetNodeId, path, data, new MessageOptions(0)); + } + + @Override + public void sendRequestWithOptions(IWearableCallbacks callbacks, final String targetNodeId, final String path, final byte[] data, MessageOptions options) throws RemoteException { + Log.d(TAG, "sendRequest: " + targetNodeId + " / " + path + ": " + (data == null ? null : Base64.encodeToString(data, Base64.NO_WRAP))); + this.wearable.networkHandler.post(new CallbackRunnable(callbacks) { + @Override + public void run(IWearableCallbacks callbacks) throws RemoteException { + RpcResponse rpcResponse = new RpcResponse(4004, -1, new byte[0]); + try { + rpcResponse.requestId = wearable.sendRequest(packageName, targetNodeId, path, data, options); + if (rpcResponse.requestId == -1) { + rpcResponse.statusCode = 4004; + } + } catch (Exception e) { + rpcResponse.statusCode = 8; + } + mainHandler.post(() -> { + try { + callbacks.onRpcResponse(rpcResponse); + } catch (RemoteException e) { + e.printStackTrace(); + } + }); + } + }); + } + + @Override + public void getCompanionPackageForNode(IWearableCallbacks callbacks, String nodeId) throws RemoteException { + Log.d(TAG, "unimplemented Method getCompanionPackageForNode"); + + } + @Override public void getFdForAsset(IWearableCallbacks callbacks, final Asset asset) throws RemoteException { Log.d(TAG, "getFdForAsset " + asset); @@ -376,6 +421,30 @@ public void getLocalNode(IWearableCallbacks callbacks) throws RemoteException { }); } + @Override + public void getNodeId(IWearableCallbacks callbacks, String address) throws RemoteException { + postNetwork(callbacks, () -> { + String resultNode; + ConnectionConfiguration configuration = wearable.getConfiguration(address); + try { + if (address == null || configuration == null || configuration.type == 4 || !address.equals(configuration.address)) { + resultNode = null; + } else { + resultNode = configuration.peerNodeId; + if (resultNode == null) resultNode = configuration.nodeId; + } + + if (resultNode != null) + callbacks.onGetNodeIdResponse(new GetNodeIdResponse(0, resultNode)); + else + callbacks.onGetNodeIdResponse(new GetNodeIdResponse(13, null)); + + } catch (Exception e) { + callbacks.onGetNodeIdResponse(new GetNodeIdResponse(8, null)); + } + }); + } + @Override public void getConnectedNodes(IWearableCallbacks callbacks) throws RemoteException { postMain(callbacks, () -> { diff --git a/play-services-wearable/src/main/aidl/com/google/android/gms/wearable/MessageOptions.aidl b/play-services-wearable/src/main/aidl/com/google/android/gms/wearable/MessageOptions.aidl new file mode 100644 index 0000000000..ea912c5f67 --- /dev/null +++ b/play-services-wearable/src/main/aidl/com/google/android/gms/wearable/MessageOptions.aidl @@ -0,0 +1,3 @@ +package com.google.android.gms.wearable; + +parcelable MessageOptions; \ No newline at end of file diff --git a/play-services-wearable/src/main/aidl/com/google/android/gms/wearable/internal/IWearableService.aidl b/play-services-wearable/src/main/aidl/com/google/android/gms/wearable/internal/IWearableService.aidl index c7398bfd8c..536b273c19 100644 --- a/play-services-wearable/src/main/aidl/com/google/android/gms/wearable/internal/IWearableService.aidl +++ b/play-services-wearable/src/main/aidl/com/google/android/gms/wearable/internal/IWearableService.aidl @@ -16,12 +16,14 @@ import com.google.android.gms.wearable.internal.LogCounterRequest; import com.google.android.gms.wearable.internal.LogEventRequest; import com.google.android.gms.wearable.internal.LogTimerRequest; +import com.google.android.gms.wearable.MessageOptions; + interface IWearableService { // Configs void putConfig(IWearableCallbacks callbacks, in ConnectionConfiguration config) = 19; void deleteConfig(IWearableCallbacks callbacks, String name) = 20; void getConfigs(IWearableCallbacks callbacks) = 21; - void enableConfig(IWearableCallbacks callbacks, String name) = 22; + void enableConfig(IWearableCallbacks callbacks, String name) = 22; // aka enableConnection void disableConfig(IWearableCallbacks callbacks, String name) = 23; // DataItems @@ -34,9 +36,14 @@ interface IWearableService { void deleteDataItemsWithFilter(IWearableCallbacks callbacks, in Uri uri, int typeFilter) = 40; void sendMessage(IWearableCallbacks callbacks, String targetNodeId, String path, in byte[] data) = 11; + void sendRequest(IWearableCallbacks callbacks, String targetNodeId, String path, in byte[] data) = 57; + void sendMessageWithOptions(IWearableCallbacks callbacks, String targetNodeId, String path, in byte[] data, in MessageOptions options) = 58; + void sendRequestWithOptions(IWearableCallbacks callbacks, String targetNodeId, String path, in byte[] data, in MessageOptions options) = 59; + void getFdForAsset(IWearableCallbacks callbacks, in Asset asset) = 12; void getLocalNode(IWearableCallbacks callbacks) = 13; + void getNodeId(IWearableCallbacks callbacks, String address) = 66; void getConnectedNodes(IWearableCallbacks callbacks) = 14; // Capabilties @@ -85,6 +92,8 @@ interface IWearableService { void someBoolUnknown(IWearableCallbacks callbacks) = 84; // cannot figure out name + void getCompanionPackageForNode(IWearableCallbacks callbacks, String nodeId) = 62; + void logCounter(IWearableCallbacks callbacks, in LogCounterRequest request) = 105; void logEvent(IWearableCallbacks callbacks, in LogEventRequest request) = 106; void logTimer(IWearableCallbacks callbacks, in LogTimerRequest request) = 107; diff --git a/play-services-wearable/src/main/java/com/google/android/gms/wearable/MessageOptions.java b/play-services-wearable/src/main/java/com/google/android/gms/wearable/MessageOptions.java new file mode 100644 index 0000000000..c31c5eef9b --- /dev/null +++ b/play-services-wearable/src/main/java/com/google/android/gms/wearable/MessageOptions.java @@ -0,0 +1,35 @@ +/* + * Copyright 2013-2025 microG Project Team + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.android.gms.wearable; + +import org.microg.safeparcel.AutoSafeParcelable; +import org.microg.safeparcel.SafeParceled; + +public class MessageOptions extends AutoSafeParcelable { + @SafeParceled(1) + public final int version = 1; + @SafeParceled(2) + public int priority; + + private MessageOptions() {} + + public MessageOptions(int priority) { + this.priority = priority; + } + + public static final Creator CREATOR = new AutoCreator(MessageOptions.class); +} diff --git a/play-services-wearable/src/main/java/com/google/android/gms/wearable/internal/GetCompanionPackageForNodeResponse.java b/play-services-wearable/src/main/java/com/google/android/gms/wearable/internal/GetCompanionPackageForNodeResponse.java index 25f5106c96..2e269025c7 100644 --- a/play-services-wearable/src/main/java/com/google/android/gms/wearable/internal/GetCompanionPackageForNodeResponse.java +++ b/play-services-wearable/src/main/java/com/google/android/gms/wearable/internal/GetCompanionPackageForNodeResponse.java @@ -20,6 +20,19 @@ import org.microg.safeparcel.SafeParceled; public class GetCompanionPackageForNodeResponse extends AutoSafeParcelable { + @SafeParceled(1) + private final int version = 1; + @SafeParceled(2) + private int statusCode; + @SafeParceled(3) + private String packageName; + + private GetCompanionPackageForNodeResponse() {} + + public GetCompanionPackageForNodeResponse(int statusCode, String packageName) { + this.statusCode = statusCode; + this.packageName = packageName; + } public static final Creator CREATOR = new AutoCreator(GetCompanionPackageForNodeResponse.class); } diff --git a/play-services-wearable/src/main/java/com/google/android/gms/wearable/internal/GetNodeIdResponse.java b/play-services-wearable/src/main/java/com/google/android/gms/wearable/internal/GetNodeIdResponse.java index 9b3f18331e..e2bcb93fe3 100644 --- a/play-services-wearable/src/main/java/com/google/android/gms/wearable/internal/GetNodeIdResponse.java +++ b/play-services-wearable/src/main/java/com/google/android/gms/wearable/internal/GetNodeIdResponse.java @@ -20,6 +20,19 @@ import org.microg.safeparcel.SafeParceled; public class GetNodeIdResponse extends AutoSafeParcelable { + @SafeParceled(1) + private final int ver = 1; + @SafeParceled(2) + public int status; + @SafeParceled(3) + public String nodeId; + + private GetNodeIdResponse() {} + + public GetNodeIdResponse(int status, String nodeId) { + this.status = status; + this.nodeId = nodeId; + } public static final Creator CREATOR = new AutoCreator(GetNodeIdResponse.class); } diff --git a/play-services-wearable/src/main/java/com/google/android/gms/wearable/internal/RpcResponse.java b/play-services-wearable/src/main/java/com/google/android/gms/wearable/internal/RpcResponse.java index 0978bcd5db..c4f01fe1d4 100644 --- a/play-services-wearable/src/main/java/com/google/android/gms/wearable/internal/RpcResponse.java +++ b/play-services-wearable/src/main/java/com/google/android/gms/wearable/internal/RpcResponse.java @@ -20,6 +20,20 @@ import org.microg.safeparcel.SafeParceled; public class RpcResponse extends AutoSafeParcelable { + @SafeParceled(1) + public int statusCode; + @SafeParceled(2) + public int requestId; + @SafeParceled(3) + public byte[] data; + + private RpcResponse() {} + + public RpcResponse(int statusCode, int requestId, byte[] data) { + this.statusCode = statusCode; + this.requestId = requestId; + this.data = data; + } public static final Creator CREATOR = new AutoCreator(RpcResponse.class); } From 465f5f01967bcafa1982a2f1568bcbfbdf669013 Mon Sep 17 00:00:00 2001 From: deadYokai Date: Wed, 31 Dec 2025 06:04:41 +0200 Subject: [PATCH 07/29] finaly successful connection and handshake some bluetooth stuff --- .../wearable/ConfigurationDatabaseHelper.java | 69 +--- .../microg/gms/wearable/DataItemRecord.java | 3 +- .../org/microg/gms/wearable/WearableImpl.java | 184 +++++++++- .../gms/wearable/WearableServiceImpl.java | 11 +- .../bluetooth/BleClientConnection.java | 168 +++++++++ .../wearable/bluetooth/BleClientManager.java | 153 ++++++++ .../wearable/bluetooth/BluetoothClient.java | 191 ++++++++++ .../bluetooth/BluetoothConnectionThread.java | 208 +++++++++++ .../wearable/bluetooth/BluetoothServer.java | 339 ++++++++++++++++++ .../BluetoothWearableConnection.java | 164 +++++++++ .../bluetooth/NetworkConnectionManager.java | 4 + .../bluetooth/NetworkConnectionThread.java | 4 + 12 files changed, 1416 insertions(+), 82 deletions(-) create mode 100644 play-services-wearable/core/src/main/java/org/microg/gms/wearable/bluetooth/BleClientConnection.java create mode 100644 play-services-wearable/core/src/main/java/org/microg/gms/wearable/bluetooth/BleClientManager.java create mode 100644 play-services-wearable/core/src/main/java/org/microg/gms/wearable/bluetooth/BluetoothClient.java create mode 100644 play-services-wearable/core/src/main/java/org/microg/gms/wearable/bluetooth/BluetoothConnectionThread.java create mode 100644 play-services-wearable/core/src/main/java/org/microg/gms/wearable/bluetooth/BluetoothServer.java create mode 100644 play-services-wearable/core/src/main/java/org/microg/gms/wearable/bluetooth/BluetoothWearableConnection.java create mode 100644 play-services-wearable/core/src/main/java/org/microg/gms/wearable/bluetooth/NetworkConnectionManager.java create mode 100644 play-services-wearable/core/src/main/java/org/microg/gms/wearable/bluetooth/NetworkConnectionThread.java diff --git a/play-services-wearable/core/src/main/java/org/microg/gms/wearable/ConfigurationDatabaseHelper.java b/play-services-wearable/core/src/main/java/org/microg/gms/wearable/ConfigurationDatabaseHelper.java index ad1f895f9f..5b96cdcc53 100644 --- a/play-services-wearable/core/src/main/java/org/microg/gms/wearable/ConfigurationDatabaseHelper.java +++ b/play-services-wearable/core/src/main/java/org/microg/gms/wearable/ConfigurationDatabaseHelper.java @@ -55,17 +55,8 @@ public class ConfigurationDatabaseHelper extends SQLiteOpenHelper { private static final String COLUMN_CONNECTION_DELAY_FILTERS = "connectionDelayFilters"; private static final String COLUMN_MAX_SUPPORTED_REMOTE_ANDROID_SDK = "maxSupportedRemoteAndroidSdkVersion"; - public static final int TYPE_BLUETOOTH_RFCOMM = 1; - public static final int TYPE_NETWORK = 2; - public static final int TYPE_BLE = 3; - public static final int TYPE_CLOUD = 4; - public static final int TYPE_BLUETOOTH_L2CAP = 5; - - public static final int ROLE_CLIENT = 1; - public static final int ROLE_SERVER = 2; - public ConfigurationDatabaseHelper(Context context) { - super(context, "connectionconfig.db", null, 2); + super(context, "connectionconfig.db", null, 11); } @Override @@ -243,68 +234,10 @@ public ConnectionConfiguration[] getAllConfigurations() { } public void setEnabledState(String packageName, boolean enabled) { - Log.d(TAG, "setEnabledState(" + packageName + ", " + enabled + ")"); - - ConnectionConfiguration oldConfig = getConfiguration(packageName); ContentValues values = new ContentValues(); values.put(COLUMN_CONNECTION_ENABLED, enabled ? 1 : 0); getWritableDatabase().updateWithOnConflict(TABLE_NAME, values, BY_NAME, new String[]{packageName != null ? packageName : NULL_STRING}, SQLiteDatabase.CONFLICT_REPLACE); - ConnectionConfiguration config = getConfiguration(packageName); - Log.d(TAG, "setConnectionEnabled configName=" + packageName + ", connectionEnabled=" + enabled + ", originalConfig=" + oldConfig + ", updatedConfig=" + config); - - switch (config.type) { - case TYPE_CLOUD: - return; // abort on cloud type - case TYPE_BLUETOOTH_RFCOMM: - case TYPE_BLUETOOTH_L2CAP: - handleLegacy(config, enabled); - break; - case TYPE_NETWORK: - handleNetwork(config, enabled); - break; - case TYPE_BLE: - handleBle(config, enabled); - break; - default: - Log.w(TAG, "unimplemented config type: " + config.type); - } - } - - private void handleBle(ConnectionConfiguration config, boolean enabled) { - if (config.role == ROLE_CLIENT) { - if (enabled) { - // add ble client config - } else { - // remove ble client config - } - } else if (config.role == ROLE_SERVER) { - // update ble server config - } - } - - private void handleNetwork(ConnectionConfiguration config, boolean enabled) { - if (enabled) { - // initialize new network service - } else { - // close network service - } - } - - private void handleLegacy(ConnectionConfiguration config, boolean enabled) { - if (config.role == ROLE_CLIENT) { - if (enabled) { - // Add/Retry bluetooth client config - } else { - // remove bluetooth client config - } - } else if (config.role == ROLE_SERVER) { - if (enabled) { - // add bluetooth server config - } else { - // remove bluetooth server config - } - } } public int deleteConfiguration(String name) { diff --git a/play-services-wearable/core/src/main/java/org/microg/gms/wearable/DataItemRecord.java b/play-services-wearable/core/src/main/java/org/microg/gms/wearable/DataItemRecord.java index 06052128c0..358b8b7507 100644 --- a/play-services-wearable/core/src/main/java/org/microg/gms/wearable/DataItemRecord.java +++ b/play-services-wearable/core/src/main/java/org/microg/gms/wearable/DataItemRecord.java @@ -164,7 +164,8 @@ public static DataItemRecord fromSetDataItem(SetDataItem setDataItem) { record.seqId = setDataItem.seqId; record.v1SeqId = -1; record.lastModified = setDataItem.lastModified; - record.deleted = setDataItem.deleted == null ? false : setDataItem.deleted; +// record.deleted = setDataItem.deleted == null ? false : setDataItem.deleted; + record.deleted = setDataItem.deleted; record.packageName = setDataItem.packageName; record.signatureDigest = setDataItem.signatureDigest; return record; diff --git a/play-services-wearable/core/src/main/java/org/microg/gms/wearable/WearableImpl.java b/play-services-wearable/core/src/main/java/org/microg/gms/wearable/WearableImpl.java index da8b9c373e..4184db0a2f 100644 --- a/play-services-wearable/core/src/main/java/org/microg/gms/wearable/WearableImpl.java +++ b/play-services-wearable/core/src/main/java/org/microg/gms/wearable/WearableImpl.java @@ -16,6 +16,7 @@ package org.microg.gms.wearable; +import android.Manifest; import android.content.Context; import android.content.Intent; import android.content.IntentFilter; @@ -29,6 +30,7 @@ import android.util.Log; import androidx.annotation.Nullable; +import androidx.annotation.RequiresPermission; import com.google.android.gms.common.data.DataHolder; import com.google.android.gms.wearable.Asset; @@ -43,6 +45,9 @@ import org.microg.gms.common.PackageUtils; import org.microg.gms.common.RemoteListenerProxy; import org.microg.gms.common.Utils; +import org.microg.gms.wearable.bluetooth.BleClientManager; +import org.microg.gms.wearable.bluetooth.BluetoothClient; +import org.microg.gms.wearable.bluetooth.BluetoothServer; import org.microg.wearable.SocketConnectionThread; import org.microg.wearable.WearableConnection; import org.microg.wearable.proto.AckAsset; @@ -70,6 +75,8 @@ import java.util.List; import java.util.Map; import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentMap; import java.util.concurrent.CountDownLatch; import okio.ByteString; @@ -94,6 +101,19 @@ public class WearableImpl { private CountDownLatch networkHandlerLock = new CountDownLatch(1); public Handler networkHandler; + private BluetoothClient bluetoothClient; + private BluetoothServer bluetoothServer; + private BleClientManager bleClientManager; + + public static final int TYPE_BLUETOOTH_RFCOMM = 1; + public static final int TYPE_NETWORK = 2; + public static final int TYPE_BLE = 3; + public static final int TYPE_CLOUD = 4; + + public static final int ROLE_CLIENT = 1; + public static final int ROLE_SERVER = 2; + + public WearableImpl(Context context, NodeDatabaseHelper nodeDatabase, ConfigurationDatabaseHelper configDatabase) { this.context = context; this.nodeDatabase = nodeDatabase; @@ -195,7 +215,21 @@ private String calculateDigest(byte[] data) { } } - public synchronized ConnectionConfiguration getConfiguration(String address) { + public synchronized ConnectionConfiguration getConfigurationByName(String name) { + if (configurations == null) { + configurations = configDatabase.getAllConfigurations(); + } + + for (ConnectionConfiguration configuration : configurations) { + if (configuration.name != null && configuration.name.equals(name)) { + return configuration; + } + } + + return null; + } + + public synchronized ConnectionConfiguration getConfigurationByAddress(String address) { if (configurations == null) { configurations = configDatabase.getAllConfigurations(); } @@ -365,6 +399,7 @@ public void onConnectReceived(WearableConnection connection, String nodeId, Conn activeConnections.put(connect.id, connection); onPeerConnected(new NodeParcelable(connect.id, connect.name)); // Fetch missing assets + syncToPeer(connect.id, nodeId, getCurrentSeqId(nodeId)); Cursor cursor = nodeDatabase.listMissingAssets(); if (cursor != null) { while (cursor.moveToNext()) { @@ -378,7 +413,7 @@ public void onConnectReceived(WearableConnection connection, String nodeId, Conn .permission(false) .build()).build()); } catch (IOException e) { - Log.w(TAG, e); + Log.w(TAG, "Error fetching asset", e); closeConnection(connect.id); } } @@ -518,23 +553,65 @@ public void removeListener(IWearableListener listener) { } } + @RequiresPermission(Manifest.permission.BLUETOOTH_CONNECT) public void enableConnection(String name) { configDatabase.setEnabledState(name, true); configurationsUpdated = true; - if (name.equals("server") && sct == null) { - Log.d(TAG, "Starting server on :" + WEAR_TCP_PORT); - (sct = SocketConnectionThread.serverListen(WEAR_TCP_PORT, new MessageHandler(context, this, configDatabase.getConfiguration(name)))).start(); +// if (name.equals("server") && sct == null) { +// Log.d(TAG, "Starting server on :" + WEAR_TCP_PORT); +// (sct = SocketConnectionThread.serverListen(WEAR_TCP_PORT, new MessageHandler(context, this, configDatabase.getConfiguration(name)))).start(); +// } + + ConnectionConfiguration config = configDatabase.getConfiguration(name); + + switch (config.type) { + case TYPE_CLOUD: + return; // abort on cloud type + case TYPE_BLUETOOTH_RFCOMM: + case 5: + handleLegacy(config, true); + break; + case TYPE_NETWORK: + handleNetwork(config, true); + break; + case TYPE_BLE: + handleBle(config, true); + break; + default: + Log.w(TAG, "unimplemented config type: " + config.type); } } + + + @RequiresPermission(Manifest.permission.BLUETOOTH_CONNECT) public void disableConnection(String name) { configDatabase.setEnabledState(name, false); configurationsUpdated = true; - if (name.equals("server") && sct != null) { - activeConnections.remove(sct.getWearableConnection()); - sct.close(); - sct.interrupt(); - sct = null; +// if (name.equals("server") && sct != null) { +// activeConnections.remove(sct.getWearableConnection()); +// sct.close(); +// sct.interrupt(); +// sct = null; +// } + + ConnectionConfiguration config = configDatabase.getConfiguration(name); + + switch (config.type) { + case TYPE_CLOUD: + return; // abort on cloud type + case TYPE_BLUETOOTH_RFCOMM: + case 5: + handleLegacy(config, false); + break; + case TYPE_NETWORK: + handleNetwork(config, false); + break; + case TYPE_BLE: + handleBle(config, false); + break; + default: + Log.w(TAG, "unimplemented config type: " + config.type); } } @@ -557,6 +634,93 @@ public void createConnection(ConnectionConfiguration config) { } } + + @RequiresPermission(Manifest.permission.BLUETOOTH_CONNECT) + private void handleBle(ConnectionConfiguration config, boolean enabled) { + if (config.role == ROLE_CLIENT) { + if (enabled) { + try { + networkHandlerLock.await(); + networkHandler.post(() -> { + if (bleClientManager == null) { + Log.d(TAG, "No BleClientManager found. Initializing a new one."); + bleClientManager = new BleClientManager(context); + } + bleClientManager.addConfiguration(config); + }); + } catch (InterruptedException e) { + Log.w(TAG, "Interrupted while starting BLE client", e); + } + } else { + try { + networkHandlerLock.await(); + networkHandler.post(() -> { + if (bleClientManager != null) { + bleClientManager.removeConfiguration(config); + } + }); + } catch (InterruptedException e) { + Log.w(TAG, "Interrupted while stopping BLE client", e); + } + } + } else if (config.role == ROLE_SERVER) { + // update ble server config + } + } + + private void handleNetwork(ConnectionConfiguration config, boolean enabled) { + if (enabled) { + // initialize new network service + } else { + // close network service + } + } + + private void handleLegacy(ConnectionConfiguration config, boolean enabled) { + try { + if (config.role == ROLE_CLIENT) { + if (enabled) { + networkHandlerLock.await(); + networkHandler.post(() -> { + if (bluetoothClient == null) { + Log.d(TAG, "No BluetoothClient found. Initializing a new one."); + bluetoothClient = new BluetoothClient(context, this); + } + bluetoothClient.addConfig(config); + }); + } else { + networkHandlerLock.await(); + networkHandler.post(() -> { + if (bluetoothClient != null) { + bluetoothClient.removeConfig(config); + } + }); + } + } else if (config.role == ROLE_SERVER) { + Log.d(TAG, "Bluetooth server not implemented"); + if (enabled) { +// networkHandlerLock.await(); +// networkHandler.post(() -> { +// if (bluetoothServer == null) { +// Log.d(TAG, "No BluetoothClient found. Initializing a new one."); +// bluetoothServer = new BluetoothServer(context); +// } +// bluetoothServer.addConfiguration(config); +// }); + } else { +// networkHandlerLock.await(); +// networkHandler.post(() -> { +// if (bluetoothServer != null) { +// bluetoothServer.removeConfiguration(config); +// } +// }); + } + } + } catch (InterruptedException e) { + Log.w(TAG, "Interrupted while duing stuff with bluetooth", e); + } + } + public int deleteDataItems(Uri uri, String packageName) { List records = nodeDatabase.deleteDataItems(packageName, PackageUtils.firstSignatureDigest(context, packageName), fixHost(uri.getHost(), false), uri.getPath()); for (DataItemRecord record : records) { diff --git a/play-services-wearable/core/src/main/java/org/microg/gms/wearable/WearableServiceImpl.java b/play-services-wearable/core/src/main/java/org/microg/gms/wearable/WearableServiceImpl.java index 1d48c3815f..f384cb0a72 100644 --- a/play-services-wearable/core/src/main/java/org/microg/gms/wearable/WearableServiceImpl.java +++ b/play-services-wearable/core/src/main/java/org/microg/gms/wearable/WearableServiceImpl.java @@ -16,6 +16,7 @@ package org.microg.gms.wearable; +import android.Manifest; import android.content.Context; import android.net.Uri; import android.os.Handler; @@ -25,11 +26,11 @@ import android.util.Base64; import android.util.Log; +import androidx.annotation.RequiresPermission; + import com.google.android.gms.common.api.Status; import com.google.android.gms.common.data.DataHolder; -import com.google.android.gms.tasks.Task; import com.google.android.gms.wearable.Asset; -import com.google.android.gms.wearable.CapabilityApi; import com.google.android.gms.wearable.ConnectionConfiguration; import com.google.android.gms.wearable.MessageOptions; import com.google.android.gms.wearable.internal.*; @@ -110,6 +111,7 @@ public void getConfigs(IWearableCallbacks callbacks) throws RemoteException { } + @RequiresPermission(Manifest.permission.BLUETOOTH_CONNECT) @Override public void enableConfig(IWearableCallbacks callbacks, final String name) throws RemoteException { Log.d(TAG, "enableConfig: " + name); @@ -119,6 +121,7 @@ public void enableConfig(IWearableCallbacks callbacks, final String name) throws }); } + @RequiresPermission(Manifest.permission.BLUETOOTH_CONNECT) @Override public void disableConfig(IWearableCallbacks callbacks, final String name) throws RemoteException { Log.d(TAG, "disableConfig: " + name); @@ -425,7 +428,7 @@ public void getLocalNode(IWearableCallbacks callbacks) throws RemoteException { public void getNodeId(IWearableCallbacks callbacks, String address) throws RemoteException { postNetwork(callbacks, () -> { String resultNode; - ConnectionConfiguration configuration = wearable.getConfiguration(address); + ConnectionConfiguration configuration = wearable.getConfigurationByAddress(address); try { if (address == null || configuration == null || configuration.type == 4 || !address.equals(configuration.address)) { resultNode = null; @@ -734,6 +737,7 @@ public void getConnection(IWearableCallbacks callbacks) throws RemoteException { }); } + @RequiresPermission(Manifest.permission.BLUETOOTH_CONNECT) @Override @Deprecated public void enableConnection(IWearableCallbacks callbacks) throws RemoteException { @@ -745,6 +749,7 @@ public void enableConnection(IWearableCallbacks callbacks) throws RemoteExceptio }); } + @RequiresPermission(Manifest.permission.BLUETOOTH_CONNECT) @Override @Deprecated public void disableConnection(IWearableCallbacks callbacks) throws RemoteException { diff --git a/play-services-wearable/core/src/main/java/org/microg/gms/wearable/bluetooth/BleClientConnection.java b/play-services-wearable/core/src/main/java/org/microg/gms/wearable/bluetooth/BleClientConnection.java new file mode 100644 index 0000000000..4e0f712f92 --- /dev/null +++ b/play-services-wearable/core/src/main/java/org/microg/gms/wearable/bluetooth/BleClientConnection.java @@ -0,0 +1,168 @@ +package org.microg.gms.wearable.bluetooth; + +import android.Manifest; +import android.bluetooth.BluetoothAdapter; +import android.bluetooth.BluetoothDevice; +import android.bluetooth.BluetoothGatt; +import android.bluetooth.BluetoothGattCallback; +import android.bluetooth.BluetoothGattCharacteristic; +import android.bluetooth.BluetoothProfile; +import android.content.Context; +import android.os.Build; +import android.os.Handler; +import android.os.Looper; +import android.util.Log; + +import androidx.annotation.RequiresPermission; + +import com.google.android.gms.wearable.ConnectionConfiguration; + +import java.io.Closeable; +import java.util.UUID; + +public class BleClientConnection extends Thread implements Closeable { + private static final String TAG = "GmsWearBleConn"; + + private static final UUID WEAR_SERVICE_UUID = UUID.fromString("0000fef7-0000-1000-8000-00805f9b34fb"); + + private final Context context; + private final ConnectionConfiguration config; + private final BluetoothAdapter bluetoothAdapter; + private final Handler handler; + private volatile boolean running = true; + private BluetoothGatt bluetoothGatt; + + public BleClientConnection(Context context, ConnectionConfiguration config, BluetoothAdapter adapter) { + super("BleClientConn-" + config.address); + this.context = context; + this.config = config; + this.bluetoothAdapter = adapter; + this.handler = new Handler(Looper.getMainLooper()); + } + + @RequiresPermission(Manifest.permission.BLUETOOTH_CONNECT) + @Override + public void run() { + Log.d(TAG, "BLE connection thread started for " + config.address); + + while (running && !isInterrupted()) { + try { + connect(); + synchronized (this) { + wait(); + } + } catch (InterruptedException e) { + Log.d(TAG, "BLE connection interrupted"); + break; + } catch (Exception e) { + Log.w(TAG, "BLE connection error: " + e.getMessage(), e); + disconnect(); + + if (running) { + try { + Thread.sleep(5000); // Wait before retry + } catch (InterruptedException ie) { + break; + } + } + } + } + + disconnect(); + } + + @RequiresPermission(Manifest.permission.BLUETOOTH_CONNECT) + private void connect() { + if (!running || bluetoothAdapter == null || !bluetoothAdapter.isEnabled()) { + throw new IllegalStateException("Bluetooth not available"); + } + + BluetoothDevice device = bluetoothAdapter.getRemoteDevice(config.address); + if (device == null) { + throw new IllegalStateException("Could not get remote device"); + } + + Log.d(TAG, "Connecting to BLE device " + config.address); + + handler.post(() -> { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { + bluetoothGatt = device.connectGatt(context, false, gattCallback, + BluetoothDevice.TRANSPORT_LE); + } else { + bluetoothGatt = device.connectGatt(context, false, gattCallback); + } + }); + } + + @RequiresPermission(Manifest.permission.BLUETOOTH_CONNECT) + private void disconnect() { + if (bluetoothGatt != null) { + handler.post(() -> { + try { + bluetoothGatt.disconnect(); + bluetoothGatt.close(); + } catch (Exception e) { + Log.w(TAG, "Error disconnecting GATT", e); + } + bluetoothGatt = null; + }); + } + } + + private final BluetoothGattCallback gattCallback = new BluetoothGattCallback() { + @RequiresPermission(Manifest.permission.BLUETOOTH_CONNECT) + @Override + public void onConnectionStateChange(BluetoothGatt gatt, int status, int newState) { + if (newState == BluetoothProfile.STATE_CONNECTED) { + Log.d(TAG, "BLE connected to " + config.address); + gatt.discoverServices(); + } else if (newState == BluetoothProfile.STATE_DISCONNECTED) { + Log.d(TAG, "BLE disconnected from " + config.address); + synchronized (BleClientConnection.this) { + BleClientConnection.this.notifyAll(); + } + } + } + + @Override + public void onServicesDiscovered(BluetoothGatt gatt, int status) { + if (status == BluetoothGatt.GATT_SUCCESS) { + Log.d(TAG, "BLE services discovered for " + config.address); + // Handle service discovery and setup characteristics + } else { + Log.w(TAG, "BLE service discovery failed: " + status); + } + } + + @Override + public void onCharacteristicRead(BluetoothGatt gatt, BluetoothGattCharacteristic characteristic, int status) { + if (status == BluetoothGatt.GATT_SUCCESS) { + Log.d(TAG, "BLE characteristic read"); + // Handle characteristic read + } + } + + @Override + public void onCharacteristicWrite(BluetoothGatt gatt, BluetoothGattCharacteristic characteristic, int status) { + if (status == BluetoothGatt.GATT_SUCCESS) { + Log.d(TAG, "BLE characteristic written"); + } + } + + @Override + public void onCharacteristicChanged(BluetoothGatt gatt, BluetoothGattCharacteristic characteristic) { + Log.d(TAG, "BLE characteristic changed"); + // Handle notifications + } + }; + + @RequiresPermission(Manifest.permission.BLUETOOTH_CONNECT) + @Override + public void close() { + Log.d(TAG, "Closing BLE connection for " + config.address); + running = false; + interrupt(); + disconnect(); + } + +} diff --git a/play-services-wearable/core/src/main/java/org/microg/gms/wearable/bluetooth/BleClientManager.java b/play-services-wearable/core/src/main/java/org/microg/gms/wearable/bluetooth/BleClientManager.java new file mode 100644 index 0000000000..6babf6eaae --- /dev/null +++ b/play-services-wearable/core/src/main/java/org/microg/gms/wearable/bluetooth/BleClientManager.java @@ -0,0 +1,153 @@ +package org.microg.gms.wearable.bluetooth; + +import android.Manifest; +import android.bluetooth.BluetoothAdapter; +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.Intent; +import android.content.IntentFilter; +import android.util.Log; + +import androidx.annotation.RequiresPermission; + +import com.google.android.gms.wearable.ConnectionConfiguration; + +import org.microg.gms.wearable.WearableImpl; + +import java.io.Closeable; +import java.util.HashMap; +import java.util.Map; + +public class BleClientManager implements Closeable { + private static final String TAG = "GmsWearBleClient"; + + private final Context context; + private final BluetoothAdapter bluetoothAdapter; + private final Map configurations = new HashMap<>(); + private final Map connections = new HashMap<>(); + private final BroadcastReceiver bluetoothStateReceiver; + + public BleClientManager(Context context) { + this.context = context; + this.bluetoothAdapter = BluetoothAdapter.getDefaultAdapter(); + + this.bluetoothStateReceiver = new BroadcastReceiver() { + @RequiresPermission(Manifest.permission.BLUETOOTH_CONNECT) + @Override + public void onReceive(Context context, Intent intent) { + if (BluetoothAdapter.ACTION_STATE_CHANGED.equals(intent.getAction())) { + int state = intent.getIntExtra(BluetoothAdapter.EXTRA_STATE, BluetoothAdapter.STATE_OFF); + onBluetoothAdapterStateChanged(state); + } + } + }; + + context.registerReceiver(bluetoothStateReceiver, new IntentFilter(BluetoothAdapter.ACTION_STATE_CHANGED)); + } + + public void addConfiguration(ConnectionConfiguration config) { + validateConfiguration(config); + + String address = config.address; + Log.d(TAG, "Adding BLE client configuration for " + address); + + if (configurations.containsKey(address)) { + Log.d(TAG, "Configuration already exists for " + address); + return; + } + + configurations.put(address, config); + + if (bluetoothAdapter == null || !bluetoothAdapter.isEnabled()) { + Log.w(TAG, "Bluetooth not available, deferring BLE connection"); + return; + } + + startConnection(config); + } + + @RequiresPermission(Manifest.permission.BLUETOOTH_CONNECT) + public void removeConfiguration(ConnectionConfiguration config) { + validateConfiguration(config); + + String address = config.address; + Log.d(TAG, "Removing BLE client configuration for " + address); + + BleClientConnection connection = connections.get(address); + if (connection != null) { + connection.close(); + connections.remove(address); + } + + configurations.remove(address); + } + + private void startConnection(ConnectionConfiguration config) { + String address = config.address; + if (connections.containsKey(address)) { + Log.d(TAG, "BLE connection already active for " + address); + return; + } + + Log.d(TAG, "Starting BLE connection for " + address); + BleClientConnection connection = new BleClientConnection(context, config, bluetoothAdapter); + connections.put(address, connection); + connection.start(); + } + + + @RequiresPermission(Manifest.permission.BLUETOOTH_CONNECT) + private void onBluetoothAdapterStateChanged(int state) { + Log.d(TAG, "Bluetooth adapter state changed to " + state); + + if (state == BluetoothAdapter.STATE_ON) { + // Start all configured connections + for (ConnectionConfiguration config : configurations.values()) { + String address = config.address; + if (!connections.containsKey(address)) { + startConnection(config); + } + } + } else if (state == BluetoothAdapter.STATE_OFF) { + // Close all connections + Log.d(TAG, "Closing all BLE connections due to adapter off"); + for (BleClientConnection connection : connections.values()) { + connection.close(); + } + connections.clear(); + } + } + + private static void validateConfiguration(ConnectionConfiguration config) { + if (config == null || config.address == null) { + throw new IllegalArgumentException("Invalid configuration"); + } + + if (config.type != WearableImpl.TYPE_BLE) { + throw new IllegalArgumentException("Invalid connection type for BLE: " + config.type); + } + + if (config.role != WearableImpl.ROLE_CLIENT) { + throw new IllegalArgumentException("Invalid role for BLE client: " + config.role); + } + } + + @RequiresPermission(Manifest.permission.BLUETOOTH_CONNECT) + @Override + public void close() { + Log.d(TAG, "Closing BleClientManager"); + + try { + context.unregisterReceiver(bluetoothStateReceiver); + } catch (Exception e) { + Log.w(TAG, "Error unregistering receiver", e); + } + + for (BleClientConnection connection : connections.values()) { + connection.close(); + } + connections.clear(); + configurations.clear(); + } + +} diff --git a/play-services-wearable/core/src/main/java/org/microg/gms/wearable/bluetooth/BluetoothClient.java b/play-services-wearable/core/src/main/java/org/microg/gms/wearable/bluetooth/BluetoothClient.java new file mode 100644 index 0000000000..d3f7fc58ed --- /dev/null +++ b/play-services-wearable/core/src/main/java/org/microg/gms/wearable/bluetooth/BluetoothClient.java @@ -0,0 +1,191 @@ +package org.microg.gms.wearable.bluetooth; + +import android.bluetooth.BluetoothAdapter; +import android.bluetooth.BluetoothDevice; +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.Intent; +import android.content.IntentFilter; +import android.util.Log; + +import com.google.android.gms.wearable.ConnectionConfiguration; + +import org.microg.gms.wearable.WearableImpl; + +import java.io.Closeable; +import java.util.HashMap; +import java.util.Map; + +public class BluetoothClient implements Closeable { + private static final String TAG = "GmsWearBtClient"; + + private final Context context; + private final BluetoothAdapter btAdapter; + private final BroadcastReceiver btStateReceiver; + private final BroadcastReceiver aclConnReceiver; + + private final Map configurations = new HashMap<>(); + private final Map connections = new HashMap<>(); + + private final WearableImpl wearableImpl; + + + public BluetoothClient(Context context, WearableImpl wearableImpl) { + this.context = context; + this.btAdapter = BluetoothAdapter.getDefaultAdapter(); + + this.wearableImpl = wearableImpl; + + this.btStateReceiver = new BroadcastReceiver() { + @Override + public void onReceive(Context context, Intent intent) { + if (BluetoothAdapter.ACTION_CONNECTION_STATE_CHANGED.equals(intent.getAction())) { + int state = intent.getIntExtra(BluetoothAdapter.EXTRA_STATE, BluetoothAdapter.STATE_OFF); + onBtAdapterStateChaged(state); + } + } + }; + + this.aclConnReceiver = new BroadcastReceiver() { + @Override + public void onReceive(Context context, Intent intent) { + if (BluetoothDevice.ACTION_ACL_CONNECTED.equals(intent.getAction())) { + BluetoothDevice device = intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE); + if (device != null) onAclConnected(device); + } + } + }; + + context.registerReceiver(btStateReceiver, new IntentFilter(BluetoothAdapter.ACTION_CONNECTION_STATE_CHANGED)); + context.registerReceiver(aclConnReceiver, new IntentFilter(BluetoothDevice.ACTION_ACL_CONNECTED)); + } + + public void addConfig(ConnectionConfiguration config) { + validateConfig(config); + + String address = config.address; + if (configurations.containsKey(address)) { + Log.d(TAG, "Configuration already exists for " + address + ", reconnecting"); + BluetoothConnectionThread thread = connections.get(address); + if (thread != null && btAdapter != null && btAdapter.isEnabled()) { + thread.retryConnection(); + } + return; + } + + configurations.put(address, config); + + if (btAdapter == null || !btAdapter.isEnabled()) { + Log.w(TAG, "Bluetooth adapter not available or disabled, deferring connection"); + return; + } + + startConnection(config); + } + + public void removeConfig(ConnectionConfiguration config) { + validateConfig(config); + + String address = config.address; + Log.d(TAG, "Removing configuration for " + address); + + BluetoothConnectionThread thread = connections.get(address); + if (thread != null) { + thread.close(); + connections.remove(address); + } + + configurations.remove(address); + } + + private void startConnection(ConnectionConfiguration config) { + String address = config.address; + if (connections.containsKey(address)) { + Log.d(TAG, "Connection already active for " + address); + return; + } + + Log.d(TAG, "Starting Bluetooth connection for " + address); + BluetoothConnectionThread thread = new BluetoothConnectionThread(context, config, btAdapter, wearableImpl); + connections.put(address, thread); + thread.start(); + } + + private void onAclConnected(BluetoothDevice device) { + String address = device.getAddress(); + ConnectionConfiguration config = configurations.get(address); + if (config != null) { + Log.d(TAG, "ACL_CONNECTED for configured device " + address + ", attempting reconnection"); + retryConnection(config, false); + } + } + + private void onBtAdapterStateChaged(int state) { + Log.d(TAG, "Bluetooth adapter state changed to " + state); + + if (state == BluetoothAdapter.STATE_ON) { + for (ConnectionConfiguration config: configurations.values()) { + String address = config.address; + if (!connections.containsKey(address)) + startConnection(config); + } + } else if (state == BluetoothAdapter.STATE_OFF) { + for (BluetoothConnectionThread thread : connections.values()) { + thread.close(); + } + connections.clear(); + } + } + + public void retryConnection(ConnectionConfiguration config, boolean immediate) { + validateConfig(config); + + String address = config.address; + if (!configurations.containsKey(address)) { + Log.w(TAG, "Configuration not found for " + address); + return; + } + + BluetoothConnectionThread thread = connections.get(address); + if (thread != null && btAdapter != null && btAdapter.isEnabled()) { + if (immediate) + thread.retryConnection(); + else + thread.scheduleRetry(); + } + } + + private static void validateConfig(ConnectionConfiguration config){ + if (config == null || config.address == null) + throw new IllegalArgumentException("Invalid configuration: config or address is null"); + + int type = config.type; + if ( type != WearableImpl.TYPE_BLUETOOTH_RFCOMM && type != 5) + throw new IllegalArgumentException("Invalid connection type: " + type); + + if (config.role != WearableImpl.ROLE_CLIENT) + throw new IllegalArgumentException("Role is not client: " + config.role); + } + + @Override + public void close() { + try { + context.unregisterReceiver(btStateReceiver); + } catch (Exception e) { + Log.w(TAG, "close BT: Error"); + } + + try { + context.unregisterReceiver(aclConnReceiver); + } catch (Exception e) { + Log.w(TAG, "close ACL: Error"); + } + + for (BluetoothConnectionThread thread: connections.values()) { + thread.close(); + } + + connections.clear(); + configurations.clear(); + } +} diff --git a/play-services-wearable/core/src/main/java/org/microg/gms/wearable/bluetooth/BluetoothConnectionThread.java b/play-services-wearable/core/src/main/java/org/microg/gms/wearable/bluetooth/BluetoothConnectionThread.java new file mode 100644 index 0000000000..3016154bb7 --- /dev/null +++ b/play-services-wearable/core/src/main/java/org/microg/gms/wearable/bluetooth/BluetoothConnectionThread.java @@ -0,0 +1,208 @@ +package org.microg.gms.wearable.bluetooth; + +import android.Manifest; +import android.bluetooth.BluetoothAdapter; +import android.bluetooth.BluetoothDevice; +import android.bluetooth.BluetoothSocket; +import android.content.Context; +import android.os.Build; +import android.os.Handler; +import android.os.Looper; +import android.util.Log; + +import androidx.annotation.RequiresPermission; + +import com.google.android.gms.wearable.ConnectionConfiguration; + +import org.microg.gms.wearable.WearableImpl; +import org.microg.wearable.SocketWearableConnection; +import org.microg.wearable.WearableConnection; +import org.microg.wearable.proto.Connect; + +import java.io.Closeable; +import java.io.IOException; +import java.util.UUID; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.atomic.AtomicInteger; + +public class BluetoothConnectionThread extends Thread implements Closeable { + private static final String TAG = "GmsWearBtConnThread"; + + private static final UUID WEAR_BT_UUID = UUID.fromString("5e8945b0-9525-11e3-a5e2-0800200c9a66"); + + private static final int MAX_RETRY_DELAY_MS = 60000; + private static final int MIN_RETRY_DELAY_MS = 1000; + private static final int BACKOFF_MULTIPLIEER = 2; + + private final Context context; + private final ConnectionConfiguration config; + private final BluetoothAdapter btAdapter; + private final Handler retryHandler; + + private final AtomicBoolean running = new AtomicBoolean(true); + private final AtomicInteger retryCount = new AtomicInteger(0); + + private BluetoothSocket socket; + private WearableConnection wearableConnection; + + private final WearableImpl wearableImpl; + + public BluetoothConnectionThread(Context context, ConnectionConfiguration config, BluetoothAdapter btAdapter, WearableImpl wearableImpl) { + super("BtThread-" + config.address); + this.context = context; + this.config = config; + this.btAdapter = btAdapter; + this.wearableImpl = wearableImpl; + this.retryHandler = new Handler(Looper.getMainLooper()); + } + + @RequiresPermission(allOf = {Manifest.permission.BLUETOOTH_CONNECT, Manifest.permission.BLUETOOTH_SCAN, Manifest.permission.BLUETOOTH_CONNECT}) + @Override + public void run(){ + Log.d(TAG, "Bluetooth connection thread started for " + config.address); + + while (running.get() && !isInterrupted()) { + try { + connect(); + } catch (IOException e) { + Log.w(TAG, "Connection failed for " + config.address + ": " + e.getMessage()); + closeSocket(); + + if (running.get()) { + try { + waitForRetry(); + } catch (InterruptedException ex) { + throw new RuntimeException(ex); + } + } + } catch (InterruptedException e) { + Log.d(TAG, "Connection thread interrupted"); + break; + } + } + + closeSocket(); + Log.d(TAG, "Bluetooth connection thread stopped for " + config.address); + } + + @RequiresPermission(allOf = {Manifest.permission.BLUETOOTH_CONNECT, Manifest.permission.BLUETOOTH_SCAN}) + private void connect() throws IOException, InterruptedException { + if (!running.get() || btAdapter == null || !btAdapter.isEnabled()) { + throw new IOException("Bluetooth not available"); + } + + BluetoothDevice device = btAdapter.getRemoteDevice(config.address); + if (device == null) throw new IOException("Could not get remote device"); + + Log.d(TAG, "Connecting to " + config.address + " via " + getConnectionTypeName()); + + if (config.type != WearableImpl.TYPE_BLUETOOTH_RFCOMM && config.type != 5 ) { + return; + } + + socket = device.createRfcommSocketToServiceRecord(WEAR_BT_UUID); + + if (btAdapter.isDiscovering()) btAdapter.cancelDiscovery(); + + socket.connect(); + Log.d(TAG, "Socket connected to " + config.address); + + retryCount.set(0); + + wearableConnection = new BluetoothWearableConnection(socket, config.nodeId, new ConnectionListener(context, config, wearableImpl)); + wearableConnection.run(); + } + + private void waitForRetry() throws InterruptedException { + int count = retryCount.incrementAndGet(); + int delay = calcRetryDelay(count); + Log.d(TAG, "Waiting " + delay + "ms before retry #" + count + " for " + config.address); + Thread.sleep(delay); + } + + private int calcRetryDelay(int retryCount) { + int delay = MIN_RETRY_DELAY_MS * (int)Math.pow(BACKOFF_MULTIPLIEER, Math.min(retryCount - 1, 6)); + return Math.min(delay, MAX_RETRY_DELAY_MS); + } + + public void retryConnection(){ + Log.d(TAG, "Immediate retry requested for " + config.address); + interrupt(); + } + + public void scheduleRetry() { + retryHandler.post(() -> { + Log.d(TAG, "Scheduled retry triggered for " + config.address); + interrupt(); + }); + } + + private void closeSocket() { + if (socket != null) { + try { + socket.close(); + } catch (IOException e) { + Log.w(TAG, "Error closing socket", e); + } + socket = null; + } + wearableConnection = null; + } + + private String getConnectionTypeName() { + switch (config.type) { + case 5: return "RFCOMM maybe, idk"; + case WearableImpl.TYPE_BLUETOOTH_RFCOMM: return "RFCOMM"; + default: return "Unknown"; + } + } + + @Override + public void close(){ + Log.d(TAG, "Closing Bluetooth connection for " + config.address); + running.set(false); + interrupt(); + closeSocket(); + } + + private static class ConnectionListener implements WearableConnection.Listener { + private final Context context; + private final ConnectionConfiguration config; + private final WearableImpl wearableImpl; + private Connect peerConnect; + private WearableConnection connection; + + public ConnectionListener(Context context, ConnectionConfiguration config, WearableImpl wearableImpl) { + this.context = context; + this.config = config; + this.wearableImpl = wearableImpl; + } + + @Override + public void onConnected(WearableConnection connection) { + Log.d(TAG, "Wearable connection established for " + config.address); + + this.connection = connection; + + BluetoothWearableConnection btConnection = (BluetoothWearableConnection) connection; + this.peerConnect = btConnection.getPeerConnect(); + + + wearableImpl.onConnectReceived(connection, config.nodeId, peerConnect); + } + + @Override + public void onMessage(WearableConnection connection, org.microg.wearable.proto.RootMessage message) { + Log.d(TAG, "Message received from " + config.address); + + } + + @Override + public void onDisconnected() { + Log.d(TAG, "Wearable connection disconnected for " + config.address); + if (connection != null && peerConnect != null) { + wearableImpl.onDisconnectReceived(connection, peerConnect); + } + } + } +} diff --git a/play-services-wearable/core/src/main/java/org/microg/gms/wearable/bluetooth/BluetoothServer.java b/play-services-wearable/core/src/main/java/org/microg/gms/wearable/bluetooth/BluetoothServer.java new file mode 100644 index 0000000000..0bc7b7a20f --- /dev/null +++ b/play-services-wearable/core/src/main/java/org/microg/gms/wearable/bluetooth/BluetoothServer.java @@ -0,0 +1,339 @@ +/* + * SPDX-FileCopyrightText: 2015, microG Project Team + * SPDX-License-Identifier: Apache-2.0 + */ + +package org.microg.gms.wearable.bluetooth; + +import android.Manifest; +import android.bluetooth.BluetoothAdapter; +import android.bluetooth.BluetoothServerSocket; +import android.bluetooth.BluetoothSocket; +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.Intent; +import android.content.IntentFilter; +import android.os.Build; +import android.util.Log; + +import androidx.annotation.RequiresPermission; + +import com.google.android.gms.wearable.ConnectionConfiguration; + +import org.microg.gms.wearable.WearableImpl; +import org.microg.wearable.WearableConnection; + +import java.io.Closeable; +import java.io.IOException; +import java.util.HashMap; +import java.util.Map; +import java.util.UUID; + +public class BluetoothServer implements Closeable { + private static final String TAG = "GmsWearBtServer"; + + // Use the standard Wear OS UUID + private static final UUID WEAR_UUID = UUID.fromString("5e8945b0-9525-11e3-a5e2-0800200c9a66"); + + private final Context context; + private final BluetoothAdapter bluetoothAdapter; + private final Map servers = new HashMap<>(); + private final BroadcastReceiver bluetoothStateReceiver; + + public BluetoothServer(Context context) { + this.context = context; + this.bluetoothAdapter = BluetoothAdapter.getDefaultAdapter(); + + this.bluetoothStateReceiver = new BroadcastReceiver() { + @Override + public void onReceive(Context context, Intent intent) { + if (BluetoothAdapter.ACTION_STATE_CHANGED.equals(intent.getAction())) { + int state = intent.getIntExtra(BluetoothAdapter.EXTRA_STATE, BluetoothAdapter.STATE_OFF); + onBluetoothAdapterStateChanged(state); + } + } + }; + + context.registerReceiver(bluetoothStateReceiver, + new IntentFilter(BluetoothAdapter.ACTION_STATE_CHANGED)); + + Log.d(TAG, "BluetoothServerManager initialized"); + } + + /** + * Add a Bluetooth server configuration + */ + public void addConfiguration(ConnectionConfiguration config) { + validateConfiguration(config); + + String name = config.name != null ? config.name : "WearServer"; + Log.d(TAG, "Adding Bluetooth server configuration: " + name); + + if (servers.containsKey(name)) { + Log.d(TAG, "Server already exists: " + name); + return; + } + + if (bluetoothAdapter == null || !bluetoothAdapter.isEnabled()) { + Log.w(TAG, "Bluetooth not available, deferring server start"); + return; + } + + startServer(config); + } + + /** + * Remove a Bluetooth server configuration + */ + public void removeConfiguration(ConnectionConfiguration config) { + validateConfiguration(config); + + String name = config.name != null ? config.name : "WearServer"; + Log.d(TAG, "Removing Bluetooth server configuration: " + name); + + BluetoothServerThread server = servers.get(name); + if (server != null) { + server.close(); + servers.remove(name); + } + } + + private void startServer(ConnectionConfiguration config) { + String name = config.name != null ? config.name : "WearServer"; + + if (servers.containsKey(name)) { + Log.d(TAG, "Server already running: " + name); + return; + } + + Log.d(TAG, "Starting Bluetooth server: " + name); + BluetoothServerThread server = new BluetoothServerThread(context, config, bluetoothAdapter); + servers.put(name, server); + server.start(); + } + + private void onBluetoothAdapterStateChanged(int state) { + Log.d(TAG, "Bluetooth adapter state changed to " + state); + + if (state == BluetoothAdapter.STATE_OFF) { + // Bluetooth turned off, close all servers + Log.d(TAG, "Closing all Bluetooth servers due to adapter off"); + for (BluetoothServerThread server : servers.values()) { + server.close(); + } + servers.clear(); + } + // Note: We don't auto-restart servers on STATE_ON + // The user/system must explicitly re-enable the configuration + } + + private static void validateConfiguration(ConnectionConfiguration config) { + if (config == null) { + throw new IllegalArgumentException("Invalid configuration: null"); + } + + int type = config.type; + if (type != WearableImpl.TYPE_BLUETOOTH_RFCOMM && type != 5) { + throw new IllegalArgumentException("Invalid connection type for Bluetooth server: " + type); + } + + if (config.role != WearableImpl.ROLE_SERVER) { + throw new IllegalArgumentException("Invalid role for server: " + config.role); + } + } + + @Override + public void close() { + Log.d(TAG, "Closing BluetoothServerManager"); + + try { + context.unregisterReceiver(bluetoothStateReceiver); + } catch (Exception e) { + Log.w(TAG, "Error unregistering receiver", e); + } + + for (BluetoothServerThread server : servers.values()) { + server.close(); + } + servers.clear(); + } + + /** + * Individual server thread that accepts incoming connections + */ + private static class BluetoothServerThread extends Thread implements Closeable { + private static final String TAG = "GmsWearBtSrvThread"; + private static final int MAX_RETRY_COUNT = 3; + private static final int RETRY_DELAY_MS = 5000; + + private final Context context; + private final ConnectionConfiguration config; + private final BluetoothAdapter bluetoothAdapter; + private volatile boolean running = true; + private BluetoothServerSocket serverSocket; + private int retryCount = 0; + + public BluetoothServerThread(Context context, ConnectionConfiguration config, BluetoothAdapter adapter) { + super("BtServerThread-" + (config.name != null ? config.name : "default")); + this.context = context; + this.config = config; + this.bluetoothAdapter = adapter; + } + + @RequiresPermission(Manifest.permission.BLUETOOTH_CONNECT) + @Override + public void run() { + String name = config.name != null ? config.name : "WearServer"; + Log.d(TAG, "Bluetooth server thread started: " + name); + + while (running && !isInterrupted()) { + try { + // Create server socket + if (serverSocket == null) { + createServerSocket(); + } + + if (serverSocket != null) { + acceptConnection(); + retryCount = 0; // Reset on successful accept + } + + } catch (IOException e) { + Log.w(TAG, "Server socket error: " + e.getMessage()); + closeServerSocket(); + + if (running && retryCount < MAX_RETRY_COUNT) { + retryCount++; + Log.d(TAG, "Retrying server socket creation (attempt " + retryCount + "/" + MAX_RETRY_COUNT + ")"); + try { + Thread.sleep(RETRY_DELAY_MS); + } catch (InterruptedException ie) { + break; + } + } else if (retryCount >= MAX_RETRY_COUNT) { + Log.e(TAG, "Max retry count reached, stopping server"); + break; + } + } + } + + closeServerSocket(); + Log.d(TAG, "Bluetooth server thread stopped: " + name); + } + + @RequiresPermission(Manifest.permission.BLUETOOTH_CONNECT) + private void createServerSocket() throws IOException { + String name = config.name != null ? config.name : "WearServer"; + + if (!running || bluetoothAdapter == null || !bluetoothAdapter.isEnabled()) { + throw new IOException("Bluetooth not available"); + } + + Log.d(TAG, "Creating server socket for " + name + " via " + getConnectionTypeName()); + + if (config.type != WearableImpl.TYPE_BLUETOOTH_RFCOMM && config.type != 5) { + return; + } + serverSocket = bluetoothAdapter.listenUsingRfcommWithServiceRecord(name, WEAR_UUID); + Log.d(TAG, "RFCOMM server socket created on UUID: " + WEAR_UUID); + + } + + private void acceptConnection() throws IOException { + if (serverSocket == null) { + throw new IOException("Server socket is null"); + } + + Log.d(TAG, "Waiting for incoming connection..."); + + // This blocks until a connection is made + BluetoothSocket clientSocket = serverSocket.accept(); + + if (clientSocket != null) { + Log.d(TAG, "Client connected from: " + clientSocket.getRemoteDevice().getAddress()); + handleConnection(clientSocket); + } + } + + private void handleConnection(BluetoothSocket clientSocket) { + // Spawn a new thread to handle this connection + // so we can go back to accepting new connections + new Thread(() -> { + try { + Log.d(TAG, "Handling connection from " + clientSocket.getRemoteDevice().getAddress()); + + BluetoothWearableConnection connection = new BluetoothWearableConnection( + clientSocket, config.nodeId, new ServerConnectionListener(context, config, clientSocket)); + connection.run(); // Blocks until connection closes + + } catch (IOException e) { + Log.w(TAG, "Error handling connection: " + e.getMessage()); + } finally { + try { + clientSocket.close(); + } catch (IOException e) { + Log.w(TAG, "Error closing client socket", e); + } + } + }, "BtServerConn-" + clientSocket.getRemoteDevice().getAddress()).start(); + } + + private void closeServerSocket() { + if (serverSocket != null) { + try { + serverSocket.close(); + } catch (IOException e) { + Log.w(TAG, "Error closing server socket", e); + } + serverSocket = null; + } + } + + private String getConnectionTypeName() { + if (config.type == WearableImpl.TYPE_BLUETOOTH_RFCOMM) { + return "RFCOMM"; + } else if (config.type == 5) { + return "RFCOMM maybe"; + } + return "Unknown"; + } + + @Override + public void close() { + Log.d(TAG, "Closing Bluetooth server"); + running = false; + interrupt(); + closeServerSocket(); + } + + private static class ServerConnectionListener implements WearableConnection.Listener { + private final Context context; + private final ConnectionConfiguration config; + private final BluetoothSocket socket; + + public ServerConnectionListener(Context context, ConnectionConfiguration config, BluetoothSocket socket) { + this.context = context; + this.config = config; + this.socket = socket; + } + + @Override + public void onConnected(WearableConnection connection) { + Log.d(TAG, "Server connection established with " + socket.getRemoteDevice().getAddress()); + // TODO: Notify WearableImpl about connection + } + + @Override + public void onMessage(WearableConnection connection, org.microg.wearable.proto.RootMessage message) { + Log.d(TAG, "Server received message from " + socket.getRemoteDevice().getAddress()); + // TODO: Handle incoming messages + } + + @Override + public void onDisconnected() { + Log.d(TAG, "Server connection disconnected from " + socket.getRemoteDevice().getAddress()); + // TODO: Notify WearableImpl about disconnection + } + } + } +} \ No newline at end of file diff --git a/play-services-wearable/core/src/main/java/org/microg/gms/wearable/bluetooth/BluetoothWearableConnection.java b/play-services-wearable/core/src/main/java/org/microg/gms/wearable/bluetooth/BluetoothWearableConnection.java new file mode 100644 index 0000000000..c899ebd5a5 --- /dev/null +++ b/play-services-wearable/core/src/main/java/org/microg/gms/wearable/bluetooth/BluetoothWearableConnection.java @@ -0,0 +1,164 @@ +/* + * SPDX-FileCopyrightText: 2015, microG Project Team + * SPDX-License-Identifier: Apache-2.0 + */ + +package org.microg.gms.wearable.bluetooth; + +import android.bluetooth.BluetoothSocket; +import android.util.Log; + +import org.microg.gms.profile.Build; +import org.microg.wearable.WearableConnection; +import org.microg.wearable.proto.Connect; +import org.microg.wearable.proto.MessagePiece; +import org.microg.wearable.proto.RootMessage; + +import java.io.DataInputStream; +import java.io.DataOutputStream; +import java.io.IOException; + +public class BluetoothWearableConnection extends WearableConnection { + private static final String TAG = "BtWearableConnection"; + private final int MAX_PIECE_SIZE = 20 * 1024 * 1024; + private final BluetoothSocket socket; + private final DataInputStream is; + private final DataOutputStream os; + private final Listener listener; + + private final String localNodeId; + private String peerNodeId; + private boolean handshakeComplete = false; + private Connect peerConnect; + + public BluetoothWearableConnection(BluetoothSocket socket, String localNodeId, Listener listener) throws IOException { + super(listener); + this.socket = socket; + this.is = new DataInputStream(socket.getInputStream()); + this.os = new DataOutputStream(socket.getOutputStream()); + this.localNodeId = localNodeId; + this.listener = listener; + + if (localNodeId == null) { + throw new IllegalArgumentException("localNodeId cannot be null"); + } + } + + private boolean handshake() { + try { + Log.d(TAG, "Starting handshake, local node ID: " + localNodeId); + + Connect connectMessage = new Connect.Builder() + .id(localNodeId) + .name(Build.MODEL) + .peerVersion(2) + .peerMinimumVersion(0) + .build(); + + RootMessage outgoingMessage = new RootMessage.Builder() + .connect(connectMessage) + .build(); + + writeMessage(outgoingMessage); + Log.d(TAG, "Sent Connect message with node ID: " + localNodeId); + + RootMessage incomingMessage = readMessage(); + Log.d(TAG, "Received message type: " + incomingMessage); + + if (incomingMessage.connect == null) { + Log.e(TAG, "Expected Connect message but received: " + incomingMessage); + return false; + } + + this.peerConnect = incomingMessage.connect; + this.peerNodeId = peerConnect.id; + + if (peerNodeId == null || peerNodeId.isEmpty()) { + Log.e(TAG, "Received invalid peer node ID"); + return false; + } + + Log.d(TAG, "Handshake successful! Peer node ID: " + peerNodeId); + Log.d(TAG, "Connect message details: " + incomingMessage.connect); + + handshakeComplete = true; + listener.onConnected(this); + return true; + } catch (IOException e) { + Log.e(TAG, "Handshake failed", e); + return false; + } + } + + public String getPeerNodeId() { + return peerNodeId; + } + + public String getLocalNodeId() { + return localNodeId; + } + + public boolean isHandshakeComplete() { + return handshakeComplete; + } + + protected void writeMessagePiece(MessagePiece piece) throws IOException { +// byte[] bytes = piece.toByteArray(); + byte[] bytes = MessagePiece.ADAPTER.encode(piece); + os.writeInt(bytes.length); + os.write(bytes); + os.flush(); + } + + @Override + public void run() { + try { + // Perform handshake first + if (!handshake()) { + Log.e(TAG, "Handshake failed, closing connection"); + try { + close(); + } catch (IOException e) { + Log.e(TAG, "Error closing connection after handshake failure", e); + } + return; + } + + super.run(); + + } catch (Exception e) { + Log.e(TAG, "Error in connection run loop", e); + } + } + + protected MessagePiece readMessagePiece() throws IOException { + int len = is.readInt(); + if (len > MAX_PIECE_SIZE) { + throw new IOException("Piece size " + len + " exceeded limit of " + MAX_PIECE_SIZE + " bytes."); + } + System.out.println("Reading piece of length " + len); + byte[] bytes = new byte[len]; + is.readFully(bytes); +// return wire.parseFrom(bytes, MessagePiece.class); + return MessagePiece.ADAPTER.decode(bytes); + } + + @Override + public void close() throws IOException { + try { + if (is != null) is.close(); + } catch (IOException e) { + // Ignore + } + try { + if (os != null) os.close(); + } catch (IOException e) { + // Ignore + } + socket.close(); + } + + public Connect getPeerConnect() { + return peerConnect; + } +} \ No newline at end of file diff --git a/play-services-wearable/core/src/main/java/org/microg/gms/wearable/bluetooth/NetworkConnectionManager.java b/play-services-wearable/core/src/main/java/org/microg/gms/wearable/bluetooth/NetworkConnectionManager.java new file mode 100644 index 0000000000..2b89962950 --- /dev/null +++ b/play-services-wearable/core/src/main/java/org/microg/gms/wearable/bluetooth/NetworkConnectionManager.java @@ -0,0 +1,4 @@ +package org.microg.gms.wearable.bluetooth; + +public class NetworkConnectionManager { +} diff --git a/play-services-wearable/core/src/main/java/org/microg/gms/wearable/bluetooth/NetworkConnectionThread.java b/play-services-wearable/core/src/main/java/org/microg/gms/wearable/bluetooth/NetworkConnectionThread.java new file mode 100644 index 0000000000..6f14ca6476 --- /dev/null +++ b/play-services-wearable/core/src/main/java/org/microg/gms/wearable/bluetooth/NetworkConnectionThread.java @@ -0,0 +1,4 @@ +package org.microg.gms.wearable.bluetooth; + +public class NetworkConnectionThread { +} From af855ec356d7c0389c656df829f47460fd79e384 Mon Sep 17 00:00:00 2001 From: deadYokai Date: Sun, 4 Jan 2026 04:59:46 +0200 Subject: [PATCH 08/29] Some channel manager things still failed to pair --- .../gms/wearable/CapabilityManager.java | 2 +- .../microg/gms/wearable/DataItemRecord.java | 3 +- .../org/microg/gms/wearable/WearableImpl.java | 133 +++- .../gms/wearable/WearableServiceImpl.java | 367 +++++++++- .../wearable/channel/ChannelCallbacks.java | 8 + .../gms/wearable/channel/ChannelManager.java | 634 ++++++++++++++++++ .../wearable/channel/ChannelStateMachine.java | 340 ++++++++++ .../wearable/channel/ChannelStatusCodes.java | 31 + .../gms/wearable/channel/ChannelToken.java | 136 ++++ .../channel/InvalidChannelTokenException.java | 7 + .../wearable/channel/OpenChannelCallback.java | 5 + .../internal/IChannelStreamCallbacks.aidl | 1 + .../wearable/internal/IWearableService.aidl | 7 + .../internal/ChannelReceiveFileResponse.java | 9 + .../internal/ChannelSendFileResponse.java | 11 +- .../internal/CloseChannelResponse.java | 9 + .../GetChannelInputStreamResponse.java | 14 + .../GetChannelOutputStreamResponse.java | 13 + .../internal/OpenChannelResponse.java | 13 + 19 files changed, 1708 insertions(+), 35 deletions(-) create mode 100644 play-services-wearable/core/src/main/java/org/microg/gms/wearable/channel/ChannelCallbacks.java create mode 100644 play-services-wearable/core/src/main/java/org/microg/gms/wearable/channel/ChannelManager.java create mode 100644 play-services-wearable/core/src/main/java/org/microg/gms/wearable/channel/ChannelStateMachine.java create mode 100644 play-services-wearable/core/src/main/java/org/microg/gms/wearable/channel/ChannelStatusCodes.java create mode 100644 play-services-wearable/core/src/main/java/org/microg/gms/wearable/channel/ChannelToken.java create mode 100644 play-services-wearable/core/src/main/java/org/microg/gms/wearable/channel/InvalidChannelTokenException.java create mode 100644 play-services-wearable/core/src/main/java/org/microg/gms/wearable/channel/OpenChannelCallback.java diff --git a/play-services-wearable/core/src/main/java/org/microg/gms/wearable/CapabilityManager.java b/play-services-wearable/core/src/main/java/org/microg/gms/wearable/CapabilityManager.java index 8a10674968..b127f903f5 100644 --- a/play-services-wearable/core/src/main/java/org/microg/gms/wearable/CapabilityManager.java +++ b/play-services-wearable/core/src/main/java/org/microg/gms/wearable/CapabilityManager.java @@ -42,7 +42,7 @@ public class CapabilityManager { private final Object lock = new Object(); - private Set capabilities = new HashSet(); + private final Set capabilities = new HashSet(); public CapabilityManager(Context context, WearableImpl wearable, String packageName) { this.context = context; diff --git a/play-services-wearable/core/src/main/java/org/microg/gms/wearable/DataItemRecord.java b/play-services-wearable/core/src/main/java/org/microg/gms/wearable/DataItemRecord.java index 358b8b7507..6ed311facc 100644 --- a/play-services-wearable/core/src/main/java/org/microg/gms/wearable/DataItemRecord.java +++ b/play-services-wearable/core/src/main/java/org/microg/gms/wearable/DataItemRecord.java @@ -37,7 +37,7 @@ import okio.ByteString; public class DataItemRecord { - private static String[] EVENT_DATA_HOLDER_FIELDS = new String[] { "event_type", "path", "data", "tags", "asset_key", "asset_id" }; + private static final String[] EVENT_DATA_HOLDER_FIELDS = new String[] { "event_type", "path", "data", "tags", "asset_key", "asset_id" }; public DataItemInternal dataItem; public String source; @@ -164,7 +164,6 @@ public static DataItemRecord fromSetDataItem(SetDataItem setDataItem) { record.seqId = setDataItem.seqId; record.v1SeqId = -1; record.lastModified = setDataItem.lastModified; -// record.deleted = setDataItem.deleted == null ? false : setDataItem.deleted; record.deleted = setDataItem.deleted; record.packageName = setDataItem.packageName; record.signatureDigest = setDataItem.signatureDigest; diff --git a/play-services-wearable/core/src/main/java/org/microg/gms/wearable/WearableImpl.java b/play-services-wearable/core/src/main/java/org/microg/gms/wearable/WearableImpl.java index 4184db0a2f..56b8489e0f 100644 --- a/play-services-wearable/core/src/main/java/org/microg/gms/wearable/WearableImpl.java +++ b/play-services-wearable/core/src/main/java/org/microg/gms/wearable/WearableImpl.java @@ -17,6 +17,8 @@ package org.microg.gms.wearable; import android.Manifest; +import android.bluetooth.BluetoothAdapter; +import android.bluetooth.BluetoothDevice; import android.content.Context; import android.content.Intent; import android.content.IntentFilter; @@ -48,6 +50,9 @@ import org.microg.gms.wearable.bluetooth.BleClientManager; import org.microg.gms.wearable.bluetooth.BluetoothClient; import org.microg.gms.wearable.bluetooth.BluetoothServer; +import org.microg.gms.wearable.channel.ChannelCallbacks; +import org.microg.gms.wearable.channel.ChannelManager; +import org.microg.gms.wearable.channel.ChannelToken; import org.microg.wearable.SocketConnectionThread; import org.microg.wearable.WearableConnection; import org.microg.wearable.proto.AckAsset; @@ -113,6 +118,7 @@ public class WearableImpl { public static final int ROLE_CLIENT = 1; public static final int ROLE_SERVER = 2; + private ChannelManager channelManager; public WearableImpl(Context context, NodeDatabaseHelper nodeDatabase, ConfigurationDatabaseHelper configDatabase) { this.context = context; @@ -126,8 +132,63 @@ public WearableImpl(Context context, NodeDatabaseHelper nodeDatabase, Configurat networkHandlerLock.countDown(); Looper.loop(); }).start(); + + new Thread(() -> { + try { + networkHandlerLock.await(); + channelManager = new ChannelManager(networkHandler, this, getLocalNodeId()); + channelManager.setChannelCallbacks(new WearableChannelCallbacks()); + channelManager.start(); + } catch (InterruptedException e) { + Log.w(TAG, "Failed to initialize ChannelManager", e); + } + }).start(); + } + + public ChannelManager getChannelManager() { + return channelManager; + } + + public AppKey getAppKey(String packageName) { + String signatureDigest = PackageUtils.firstSignatureDigest(context, packageName); + return new AppKey(packageName, signatureDigest); + } + + private class WearableChannelCallbacks implements ChannelCallbacks { + @Override + public void onChannelOpened(ChannelToken token, String path) { + Log.d(TAG, "onChannelOpened: " + token + ", path=" + path); + invokeListeners(null, listener -> { + // todo + }); + } + + @Override + public void onChannelClosed(ChannelToken token, String path, int closeReason, int errorCode) { + Log.d(TAG, "onChannelClosed: " + token + ", reason=" + closeReason); + invokeListeners(null, listener -> { + // todo + }); + } + + @Override + public void onChannelInputClosed(ChannelToken token, String path, int closeReason, int errorCode) { + Log.d(TAG, "onChannelInputClosed: " + token); + invokeListeners(null, listener -> { + // todo + }); + } + + @Override + public void onChannelOutputClosed(ChannelToken token, String path, int closeReason, int errorCode) { + Log.d(TAG, "onChannelOutputClosed: " + token); + invokeListeners(null, listener -> { + // todo + }); + } } + public String getLocalNodeId() { return clockworkNodePreferences.getLocalNodeId(); } @@ -229,6 +290,18 @@ public synchronized ConnectionConfiguration getConfigurationByName(String name) return null; } + public synchronized ConnectionConfiguration getConfigurationByNodeId(String nodeId) { + if (configurations == null) { + configurations = configDatabase.getAllConfigurations(); + } + + for (ConnectionConfiguration configuration : configurations) { + if (configuration.nodeId.equals(nodeId)) return configuration; + } + + return null; + } + public synchronized ConnectionConfiguration getConfigurationByAddress(String address) { if (configurations == null) { configurations = configDatabase.getAllConfigurations(); @@ -250,7 +323,8 @@ public synchronized ConnectionConfiguration[] getConfigurations() { ConnectionConfiguration[] newConfigurations = configDatabase.getAllConfigurations(); for (ConnectionConfiguration configuration : configurations) { for (ConnectionConfiguration newConfiguration : newConfigurations) { - if (newConfiguration.name.equals(configuration.name)) { + if (newConfiguration.address != null && + newConfiguration.address.equalsIgnoreCase(configuration.address)) { newConfiguration.connected = configuration.connected; newConfiguration.peerNodeId = configuration.peerNodeId; newConfiguration.nodeId = configuration.nodeId; @@ -260,6 +334,29 @@ public synchronized ConnectionConfiguration[] getConfigurations() { } configurations = newConfigurations; } + + // companion app crash if name is null + // name can be null in failed pair (maybe), + // or maybe i something broke, + // or not setting name properly somewhere + for (int i = 0; i < configurations.length; i++) { + ConnectionConfiguration c = configurations[i]; + if (c.name == null || c.name.isEmpty() || "null".equals(c.name)) { + String fallbackName = (c.address != null) ? c.address : "Unknown"; + configurations[i] = new ConnectionConfiguration( + fallbackName, + c.address, + c.type, + c.role, + c.enabled, + c.nodeId + ); + configurations[i].connected = c.connected; + configurations[i].peerNodeId = c.peerNodeId; + } + } + + Log.d(TAG, "Configurations reported: " + Arrays.toString(configurations)); return configurations; } @@ -301,6 +398,10 @@ void syncRecordToAll(DataItemRecord record) { } } + public Map getActiveConnections() { + return activeConnections; + } + private boolean syncRecordToPeer(String nodeId, DataItemRecord record) { for (Asset asset : record.dataItem.getAssets().values()) { try { @@ -555,14 +656,12 @@ public void removeListener(IWearableListener listener) { @RequiresPermission(Manifest.permission.BLUETOOTH_CONNECT) public void enableConnection(String name) { - configDatabase.setEnabledState(name, true); - configurationsUpdated = true; -// if (name.equals("server") && sct == null) { -// Log.d(TAG, "Starting server on :" + WEAR_TCP_PORT); -// (sct = SocketConnectionThread.serverListen(WEAR_TCP_PORT, new MessageHandler(context, this, configDatabase.getConfiguration(name)))).start(); -// } + Log.d(TAG, "enableConnection: " + name); - ConnectionConfiguration config = configDatabase.getConfiguration(name); + ConnectionConfiguration config = getConfigurationByName(name); + + configDatabase.setEnabledState(config.name, true); + configurationsUpdated = true; switch (config.type) { case TYPE_CLOUD: @@ -586,14 +685,10 @@ public void enableConnection(String name) { @RequiresPermission(Manifest.permission.BLUETOOTH_CONNECT) public void disableConnection(String name) { + Log.d(TAG, "disableConnection: " + name); + configDatabase.setEnabledState(name, false); configurationsUpdated = true; -// if (name.equals("server") && sct != null) { -// activeConnections.remove(sct.getWearableConnection()); -// sct.close(); -// sct.interrupt(); -// sct = null; -// } ConnectionConfiguration config = configDatabase.getConfiguration(name); @@ -620,6 +715,13 @@ public void deleteConnection(String name) { configurationsUpdated = true; } + public void updateConfiguration(ConnectionConfiguration config) { + Log.d(TAG, "updateConfig: " + config); + configDatabase.putConfiguration(config); + configurationsUpdated = true; + } + + @RequiresPermission(Manifest.permission.BLUETOOTH_CONNECT) public void createConnection(ConnectionConfiguration config) { if (config.nodeId == null) config.nodeId = getLocalNodeId(); Log.d(TAG, "putConfig[nyp]: " + config); @@ -811,6 +913,9 @@ public int sendRequest(String packageName, String targetNodeId, String path, byt public void stop() { try { + if (channelManager != null) { + channelManager.stop(); + } this.networkHandlerLock.await(); this.networkHandler.getLooper().quit(); } catch (InterruptedException e) { diff --git a/play-services-wearable/core/src/main/java/org/microg/gms/wearable/WearableServiceImpl.java b/play-services-wearable/core/src/main/java/org/microg/gms/wearable/WearableServiceImpl.java index f384cb0a72..aac11f1f53 100644 --- a/play-services-wearable/core/src/main/java/org/microg/gms/wearable/WearableServiceImpl.java +++ b/play-services-wearable/core/src/main/java/org/microg/gms/wearable/WearableServiceImpl.java @@ -28,6 +28,7 @@ import androidx.annotation.RequiresPermission; +import com.google.android.gms.common.api.CommonStatusCodes; import com.google.android.gms.common.api.Status; import com.google.android.gms.common.data.DataHolder; import com.google.android.gms.wearable.Asset; @@ -35,7 +36,16 @@ import com.google.android.gms.wearable.MessageOptions; import com.google.android.gms.wearable.internal.*; +import org.microg.gms.wearable.channel.ChannelManager; +import org.microg.gms.wearable.channel.ChannelStateMachine; +import org.microg.gms.wearable.channel.ChannelStatusCodes; +import org.microg.gms.wearable.channel.ChannelToken; +import org.microg.gms.wearable.channel.InvalidChannelTokenException; +import org.microg.gms.wearable.channel.OpenChannelCallback; +import org.microg.wearable.proto.AppKey; + import java.io.FileNotFoundException; +import java.io.IOException; import java.util.ArrayList; import java.util.HashMap; import java.util.HashSet; @@ -60,6 +70,14 @@ public WearableServiceImpl(Context context, WearableImpl wearable, String packag this.mainHandler = new Handler(context.getMainLooper()); } + private AppKey getAppKey() { + return wearable.getAppKey(packageName); + } + + private ChannelManager getChannelManager() { + return wearable.getChannelManager(); + } + private void postMain(IWearableCallbacks callbacks, RemoteExceptionRunnable runnable) { mainHandler.post(new CallbackRunnable(callbacks) { @Override @@ -82,6 +100,7 @@ public void run(IWearableCallbacks callbacks) throws RemoteException { * Config */ + @RequiresPermission(Manifest.permission.BLUETOOTH_CONNECT) @Override public void putConfig(IWearableCallbacks callbacks, final ConnectionConfiguration config) throws RemoteException { postMain(callbacks, () -> { @@ -131,6 +150,68 @@ public void disableConfig(IWearableCallbacks callbacks, final String name) throw }); } + @Override + public void getRelatedConfigs(IWearableCallbacks callbacks) throws RemoteException { + Log.d(TAG, "getRelatedConfigs"); + postMain(callbacks, () -> { + try { + ConnectionConfiguration[] allConfigs = wearable.getConfigurations(); + List relatedConfigs = new ArrayList<>(); + for (ConnectionConfiguration config : allConfigs) { + if (config.packageName == null || config.packageName.equals(packageName)) { + relatedConfigs.add(config); + } + } + + callbacks.onGetConfigsResponse(new GetConfigsResponse(0, + relatedConfigs.toArray(new ConnectionConfiguration[0]))); + } catch (Exception e) { + Log.e(TAG, "getRelatedConfigs failed", e); + callbacks.onGetConfigsResponse(new GetConfigsResponse(8, new ConnectionConfiguration[0])); + } + }); + + } + + @Override + public void updateConfig(IWearableCallbacks callbacks, ConnectionConfiguration config) throws RemoteException { + Log.d(TAG, "updateConfig: " + config); + + postMain(callbacks, () -> { + try { + if (config == null || config.address == null) { + Log.w(TAG, "updateConfig: invalid config"); + callbacks.onStatus(new Status(CommonStatusCodes.ERROR)); + return; + } + + ConnectionConfiguration existing = wearable.getConfigurationByAddress(config.address); + + if (existing == null) { + Log.w(TAG, "updateConfig: no existing config for address " + config.address); + callbacks.onStatus(new Status(CommonStatusCodes.ERROR)); + return; + } + + ConnectionConfiguration toUpdate = config; + if (existing.dataItemSyncEnabled && !config.dataItemSyncEnabled) { + Log.w(TAG, "updateConfig: disabling dataItemSync not allowed, keeping existing value"); + } + + wearable.updateConfiguration(toUpdate); + callbacks.onStatus(Status.SUCCESS); + + } catch (Exception e) { + Log.e(TAG, "updateConfig: exception during processing", e); + try { + callbacks.onStatus(new Status(CommonStatusCodes.ERROR)); + } catch (RemoteException re) { + Log.w(TAG, "Failed to send error status", re); + } + } + }); + } + /* * DataItems */ @@ -267,6 +348,16 @@ public void getCompanionPackageForNode(IWearableCallbacks callbacks, String node } + @Override + public void setCloudSyncSettingByNode(IWearableCallbacks callbacks, String s, boolean b) throws RemoteException { + Log.d(TAG, "unimplemented Method setCloudSyncSettingByNode"); + + // dummy + postMain(callbacks, () -> { + callbacks.onStatus(Status.SUCCESS); + }); + } + @Override public void getFdForAsset(IWearableCallbacks callbacks, final Asset asset) throws RemoteException { Log.d(TAG, "getFdForAsset " + asset); @@ -282,6 +373,9 @@ public void getFdForAsset(IWearableCallbacks callbacks, final Asset asset) throw @Override public void optInCloudSync(IWearableCallbacks callbacks, boolean enable) throws RemoteException { + Log.d(TAG, "unimplemented Method optInCloudSync"); + + // dummy callbacks.onStatus(Status.SUCCESS); } @@ -297,7 +391,7 @@ public void setCloudSyncSetting(IWearableCallbacks callbacks, boolean enable) th Log.d(TAG, "unimplemented Method: setCloudSyncSetting"); postMain(callbacks, () -> { - // dummy stuff + // dummy callbacks.onStatus(new Status(0)); }); } @@ -663,44 +757,283 @@ public void doAncsNegativeAction(IWearableCallbacks callbacks, int i) throws Rem Log.d(TAG, "unimplemented Method: doAncsNegativeAction: " + i); } - @Override - public void openChannel(IWearableCallbacks callbacks, String s1, String s2) throws RemoteException { - Log.d(TAG, "unimplemented Method: openChannel; " + s1 + ", " + s2); - } - /* * Channels */ + @Override + public void openChannel(IWearableCallbacks callbacks, String nodeId, String path) throws RemoteException { + Log.d(TAG, "openChannel; " + nodeId + ", " + path); + + ChannelManager channelManager = getChannelManager(); + if (channelManager == null) { + Log.w(TAG, "openChannel: ChannelManager not initialized"); + callbacks.onOpenChannelResponse(new OpenChannelResponse(ChannelStatusCodes.INTERNAL_ERROR, null)); + return; + } + + try { + if (nodeId == null || nodeId.isEmpty()) { + Log.w(TAG, "openChannel: nodeId is null or empty"); + callbacks.onOpenChannelResponse(new OpenChannelResponse(ChannelStatusCodes.INVALID_ARGUMENT, null)); + return; + } + + if (path == null || path.isEmpty()) { + Log.w(TAG, "openChannel: path is null or empty"); + callbacks.onOpenChannelResponse(new OpenChannelResponse(ChannelStatusCodes.INVALID_ARGUMENT, null)); + return; + } + + AppKey appKey = getAppKey(); + + OpenChannelCallback openCallback = (statusCode, token, path1) -> { + try { + if (statusCode == ChannelStatusCodes.SUCCESS && token != null) { + callbacks.onOpenChannelResponse(new OpenChannelResponse(statusCode, token.toParcelable(path1))); + } else { + callbacks.onOpenChannelResponse(new OpenChannelResponse(statusCode, null)); + } + } catch (RemoteException e) { + Log.w(TAG, "Failed to send openChannel result", e); + } + }; + + channelManager.openChannel(appKey, nodeId, path, openCallback); + } catch (Exception e) { + Log.w(TAG, "openChannel: exception during processing", e); + callbacks.onOpenChannelResponse(new OpenChannelResponse(ChannelStatusCodes.INTERNAL_ERROR, null)); + } + } + @Override public void closeChannel(IWearableCallbacks callbacks, String s) throws RemoteException { - Log.d(TAG, "unimplemented Method: closeChannel: " + s); + closeChannelWithError(callbacks, s, 0); } @Override - public void closeChannelWithError(IWearableCallbacks callbacks, String s, int errorCode) throws RemoteException { - Log.d(TAG, "unimplemented Method: closeChannelWithError:" + s + ", " + errorCode); + public void closeChannelWithError(IWearableCallbacks callbacks, String channelToken, int errorCode) throws RemoteException { + Log.d(TAG, "closeChannelWithError:" + channelToken + ", " + errorCode); + + ChannelManager channelManager = getChannelManager(); + if (channelManager == null) { + callbacks.onCloseChannelResponse(new CloseChannelResponse(ChannelStatusCodes.INTERNAL_ERROR)); + return; + } + + try { + ChannelToken token = ChannelToken.fromString(getAppKey(), channelToken); + ChannelStateMachine channel = channelManager.getChannel(token); + + if (channel == null) { + callbacks.onCloseChannelResponse(new CloseChannelResponse(ChannelStatusCodes.CHANNEL_NOT_FOUND)); + return; + } + + channelManager.closeChannel(token, errorCode); + callbacks.onCloseChannelResponse(new CloseChannelResponse(ChannelStatusCodes.SUCCESS)); + + } catch (InvalidChannelTokenException e) { + Log.w(TAG, "closeChannelWithError: invalid token", e); + callbacks.onCloseChannelResponse(new CloseChannelResponse(ChannelStatusCodes.INVALID_ARGUMENT)); + } catch (Exception e) { + Log.w(TAG, "closeChannelWithError: exception", e); + callbacks.onCloseChannelResponse(new CloseChannelResponse(ChannelStatusCodes.INTERNAL_ERROR)); + } } @Override - public void getChannelInputStream(IWearableCallbacks callbacks, IChannelStreamCallbacks channelCallbacks, String s) throws RemoteException { - Log.d(TAG, "unimplemented Method: getChannelInputStream: " + s); + public void getChannelInputStream(IWearableCallbacks callbacks, IChannelStreamCallbacks channelCallbacks, String channelToken) throws RemoteException { + Log.d(TAG, "getChannelInputStream: " + channelToken); + + ChannelManager channelManager = getChannelManager(); + if (channelManager == null) { + ChannelManager.getInputStreamError(callbacks, ChannelStatusCodes.INTERNAL_ERROR); + return; + } + + try { + ChannelToken token = ChannelToken.fromString(getAppKey(), channelToken); + ChannelStateMachine channel = channelManager.getChannel(token); + + if (channel == null) { + ChannelManager.getInputStreamError(callbacks, ChannelStatusCodes.CHANNEL_NOT_FOUND); + return; + } + + if (channel.hasInputStream()) { + ChannelManager.getInputStreamError(callbacks, ChannelStatusCodes.ALREADY_IN_PROGRESS); + return; + } + + ParcelFileDescriptor[] pipe = ParcelFileDescriptor.createPipe(); + ParcelFileDescriptor readEnd = pipe[0]; + ParcelFileDescriptor writeEnd = pipe[1]; + + channel.setInputStream(writeEnd, channelCallbacks); + + callbacks.onGetChannelInputStreamResponse( + new GetChannelInputStreamResponse(ChannelStatusCodes.SUCCESS, readEnd)); + + } catch (InvalidChannelTokenException e) { + Log.w(TAG, "getChannelInputStream: invalid token", e); + ChannelManager.getInputStreamError(callbacks, ChannelStatusCodes.INVALID_ARGUMENT); + } catch (IOException e) { + Log.w(TAG, "getChannelInputStream: IO exception", e); + ChannelManager.getInputStreamError(callbacks, ChannelStatusCodes.INTERNAL_ERROR); + } catch (Exception e) { + Log.w(TAG, "getChannelInputStream: exception", e); + ChannelManager.getInputStreamError(callbacks, ChannelStatusCodes.INTERNAL_ERROR); + } } @Override - public void getChannelOutputStream(IWearableCallbacks callbacks, IChannelStreamCallbacks channelCallbacks, String s) throws RemoteException { - Log.d(TAG, "unimplemented Method: getChannelOutputStream: " + s); + public void getChannelOutputStream(IWearableCallbacks callbacks, IChannelStreamCallbacks channelCallbacks, String channelToken) throws RemoteException { + Log.d(TAG, "getChannelOutputStream: " + channelToken); + + ChannelManager channelManager = getChannelManager(); + if (channelManager == null) { + ChannelManager.getOutputStreamError(callbacks, ChannelStatusCodes.INTERNAL_ERROR); + return; + } + + try { + ChannelToken token = ChannelToken.fromString(getAppKey(), channelToken); + ChannelStateMachine channel = channelManager.getChannel(token); + + if (channel == null) { + ChannelManager.getOutputStreamError(callbacks, ChannelStatusCodes.CHANNEL_NOT_FOUND); + return; + } + + if (channel.hasOutputStream()) { + ChannelManager.getOutputStreamError(callbacks, ChannelStatusCodes.ALREADY_IN_PROGRESS); + return; + } + + ParcelFileDescriptor[] pipe = ParcelFileDescriptor.createPipe(); + ParcelFileDescriptor readEnd = pipe[0]; + ParcelFileDescriptor writeEnd = pipe[1]; + + channel.setOutputStream(readEnd, channelCallbacks, 0, -1); + + callbacks.onGetChannelOutputStreamResponse( + new GetChannelOutputStreamResponse(ChannelStatusCodes.SUCCESS, writeEnd)); + + } catch (InvalidChannelTokenException e) { + Log.w(TAG, "getChannelOutputStream: invalid token", e); + ChannelManager.getOutputStreamError(callbacks, ChannelStatusCodes.INVALID_ARGUMENT); + } catch (IOException e) { + Log.w(TAG, "getChannelOutputStream: IO exception", e); + ChannelManager.getOutputStreamError(callbacks, ChannelStatusCodes.INTERNAL_ERROR); + } catch (Exception e) { + Log.w(TAG, "getChannelOutputStream: exception", e); + ChannelManager.getOutputStreamError(callbacks, ChannelStatusCodes.INTERNAL_ERROR); + } + } @Override - public void writeChannelInputToFd(IWearableCallbacks callbacks, String s, ParcelFileDescriptor fd) throws RemoteException { - Log.d(TAG, "unimplemented Method: writeChannelInputToFd: " + s); + public void writeChannelInputToFd(IWearableCallbacks callbacks, String channelToken, ParcelFileDescriptor fd) throws RemoteException { + Log.d(TAG, "writeChannelInputToFd: " + channelToken); + + ChannelManager channelManager = getChannelManager(); + if (channelManager == null) { + ChannelManager.receiveFileResult(callbacks, ChannelStatusCodes.INTERNAL_ERROR); + return; + } + + try { + ChannelToken token = ChannelToken.fromString(getAppKey(), channelToken); + ChannelStateMachine channel = channelManager.getChannel(token); + + if (channel == null) { + ChannelManager.receiveFileResult(callbacks, ChannelStatusCodes.CHANNEL_NOT_FOUND); + return; + } + + if (channel.hasInputStream()) { + ChannelManager.receiveFileResult(callbacks, ChannelStatusCodes.ALREADY_IN_PROGRESS); + return; + } + + channel.setInputStream(fd, new ReceiveFileStreamCallback(callbacks)); + + } catch (InvalidChannelTokenException e) { + Log.w(TAG, "writeChannelInputToFd: invalid token", e); + ChannelManager.receiveFileResult(callbacks, ChannelStatusCodes.INVALID_ARGUMENT); + } catch (Exception e) { + Log.w(TAG, "writeChannelInputToFd: exception", e); + ChannelManager.receiveFileResult(callbacks, ChannelStatusCodes.INTERNAL_ERROR); + } + } @Override - public void readChannelOutputFromFd(IWearableCallbacks callbacks, String s, ParcelFileDescriptor fd, long l1, long l2) throws RemoteException { - Log.d(TAG, "unimplemented Method: readChannelOutputFromFd: " + s + ", " + l1 + ", " + l2); + public void readChannelOutputFromFd(IWearableCallbacks callbacks, String channelToken, ParcelFileDescriptor fd, long startOffset, long length) throws RemoteException { + Log.d(TAG, "unimplemented Method: readChannelOutputFromFd: " + channelToken + ", " + startOffset + ", " + length); + + ChannelManager channelManager = getChannelManager(); + if (channelManager == null) { + ChannelManager.sendFileResult(callbacks, ChannelStatusCodes.INTERNAL_ERROR); + return; + } + + try { + ChannelToken token = ChannelToken.fromString(getAppKey(), channelToken); + ChannelStateMachine channel = channelManager.getChannel(token); + + if (channel == null) { + ChannelManager.sendFileResult(callbacks, ChannelStatusCodes.CHANNEL_NOT_FOUND); + return; + } + + if (channel.hasOutputStream()) { + ChannelManager.sendFileResult(callbacks, ChannelStatusCodes.ALREADY_IN_PROGRESS); + return; + } + + channel.setOutputStream(fd, new SendFileStreamCallback(callbacks), startOffset, length); + + } catch (InvalidChannelTokenException e) { + Log.w(TAG, "readChannelOutputFromFd: invalid token", e); + ChannelManager.sendFileResult(callbacks, ChannelStatusCodes.INVALID_ARGUMENT); + } catch (Exception e) { + Log.w(TAG, "readChannelOutputFromFd: exception", e); + ChannelManager.sendFileResult(callbacks, ChannelStatusCodes.INTERNAL_ERROR); + } + + } + + private static class ReceiveFileStreamCallback extends IChannelStreamCallbacks.Stub { + private final IWearableCallbacks callbacks; + + ReceiveFileStreamCallback(IWearableCallbacks callbacks) { + this.callbacks = callbacks; + } + + @Override + public void onChannelClosed(int closeReason, int errorCode) throws RemoteException { + int statusCode = (closeReason == ChannelStatusCodes.CLOSE_REASON_NORMAL) + ? ChannelStatusCodes.SUCCESS : closeReason; + ChannelManager.receiveFileResult(callbacks, statusCode); + } + } + + private static class SendFileStreamCallback extends IChannelStreamCallbacks.Stub { + private final IWearableCallbacks callbacks; + + SendFileStreamCallback(IWearableCallbacks callbacks) { + this.callbacks = callbacks; + } + + @Override + public void onChannelClosed(int closeReason, int errorCode) throws RemoteException { + int statusCode = (closeReason == ChannelStatusCodes.CLOSE_REASON_NORMAL) + ? ChannelStatusCodes.SUCCESS : closeReason; + ChannelManager.sendFileResult(callbacks, statusCode); + } } @Override diff --git a/play-services-wearable/core/src/main/java/org/microg/gms/wearable/channel/ChannelCallbacks.java b/play-services-wearable/core/src/main/java/org/microg/gms/wearable/channel/ChannelCallbacks.java new file mode 100644 index 0000000000..05d44d6e7d --- /dev/null +++ b/play-services-wearable/core/src/main/java/org/microg/gms/wearable/channel/ChannelCallbacks.java @@ -0,0 +1,8 @@ +package org.microg.gms.wearable.channel; + +public interface ChannelCallbacks { + void onChannelOpened(ChannelToken token, String path); + void onChannelClosed(ChannelToken token, String path, int closeReason, int errorCode); + void onChannelInputClosed(ChannelToken token, String path, int closeReason, int errorCode); + void onChannelOutputClosed(ChannelToken token, String path, int closeReason, int errorCode); +} \ No newline at end of file diff --git a/play-services-wearable/core/src/main/java/org/microg/gms/wearable/channel/ChannelManager.java b/play-services-wearable/core/src/main/java/org/microg/gms/wearable/channel/ChannelManager.java new file mode 100644 index 0000000000..3269605879 --- /dev/null +++ b/play-services-wearable/core/src/main/java/org/microg/gms/wearable/channel/ChannelManager.java @@ -0,0 +1,634 @@ +package org.microg.gms.wearable.channel; + +import android.os.Handler; +import android.os.IBinder; +import android.os.RemoteException; +import android.util.Log; + +import com.google.android.gms.wearable.internal.ChannelReceiveFileResponse; +import com.google.android.gms.wearable.internal.ChannelSendFileResponse; +import com.google.android.gms.wearable.internal.GetChannelInputStreamResponse; +import com.google.android.gms.wearable.internal.GetChannelOutputStreamResponse; +import com.google.android.gms.wearable.internal.IWearableCallbacks; + +import org.microg.gms.wearable.WearableImpl; +import org.microg.wearable.WearableConnection; +import org.microg.wearable.proto.AppKey; +import org.microg.wearable.proto.ChannelControlRequest; +import org.microg.wearable.proto.ChannelDataAckRequest; +import org.microg.wearable.proto.ChannelDataHeader; +import org.microg.wearable.proto.ChannelDataRequest; +import org.microg.wearable.proto.ChannelRequest; +import org.microg.wearable.proto.Request; +import org.microg.wearable.proto.RootMessage; + +import java.io.IOException; +import java.util.Map; +import java.util.Random; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.atomic.AtomicInteger; + +import okio.ByteString; + +public class ChannelManager { + private static final String TAG = "ChannelManager"; + + public static final int CHANNEL_CONTROL_TYPE_OPEN = 1; + public static final int CHANNEL_CONTROL_TYPE_OPEN_ACK = 2; + public static final int CHANNEL_CONTROL_TYPE_CLOSE = 3; + + private final Handler handler; + private final WearableImpl wearable; + private final String localNodeId; + private final Random random; + + private final Object lock = new Object(); + private final Map channels = new ConcurrentHashMap<>(); + private final Map channelIdToToken = new ConcurrentHashMap<>(); + private final AtomicBoolean isRunning = new AtomicBoolean(false); + + private final AtomicInteger requestIdCounter = new AtomicInteger(1); + private final AtomicInteger generationCounter = new AtomicInteger(1); + + private ChannelCallbacks channelCallbacks; + + public ChannelManager(Handler handler, WearableImpl wearable, String localNodeId) { + this.handler = handler; + this.wearable = wearable; + this.localNodeId = localNodeId; + this.random = new Random(); + } + + public void start() { + isRunning.set(true); + Log.d(TAG, "ChannelManager started, localNodeId=" + localNodeId); + } + + public void stop() { + isRunning.set(false); + synchronized (lock) { + for (ChannelStateMachine channel : channels.values()) { + try { + channel.close(); + } catch (Exception e) { + Log.w(TAG, "Error closing channel on stop", e); + } + } + channels.clear(); + channelIdToToken.clear(); + } + Log.d(TAG, "ChannelManager stopped"); + } + + public void setChannelCallbacks(ChannelCallbacks callbacks) { + this.channelCallbacks = callbacks; + } + + public void openChannel(AppKey appKey, String nodeId, String path, OpenChannelCallback callback) { + Log.d(TAG, String.format("openChannel(%s, %s, %s)", appKey.packageName, nodeId, path)); + + if (!isRunning.get()) { + Log.w(TAG, "openChannel called while not running"); + callback.onResult(ChannelStatusCodes.INTERNAL_ERROR, null, path); + return; + } + + handler.post(() -> doOpenChannel(appKey, nodeId, path, callback)); + } + + private void doOpenChannel(AppKey appKey, String nodeId, String path, OpenChannelCallback callback) { + try { + WearableConnection connection = wearable.getActiveConnections().get(nodeId); + if (connection == null) { + Log.w(TAG, "Target node not connected: " + nodeId); + callback.onResult(ChannelStatusCodes.CHANNEL_NOT_CONNECTED, null, path); + return; + } + + long channelId = generateChannelId(); + + ChannelToken token = new ChannelToken(nodeId, appKey, channelId, true); + String tokenString = token.toTokenString(); + + IBinder.DeathRecipient deathRecipient = () -> onBinderDied(token); + + ChannelStateMachine channel = new ChannelStateMachine( + token, this, channelCallbacks, true, deathRecipient + ); + channel.setPath(path); + channel.setOpenCallback(callback); + + synchronized (lock) { + channels.put(tokenString, channel); + channelIdToToken.put(channelId, tokenString); + } + + channel.setConnectionState(ChannelStateMachine.CONNECTION_STATE_OPEN_SENT); + + ChannelControlRequest controlRequest = new ChannelControlRequest.Builder() + .type(CHANNEL_CONTROL_TYPE_OPEN) + .channelId(channelId) + .fromChannelOperator(true) + .packageName(appKey.packageName) + .signatureDigest(appKey.signatureDigest) + .path(path) + .build(); + + ChannelRequest channelRequest = new ChannelRequest.Builder() + .channelControlRequest(controlRequest) + .version(1) + .origin(0) + .build(); + + int requestId = requestIdCounter.getAndIncrement(); + int generation = generationCounter.get(); + + Request request = new Request.Builder() + .targetNodeId(nodeId) + .sourceNodeId(localNodeId) + .packageName(appKey.packageName) + .signatureDigest(appKey.signatureDigest) + .path(path) + .request(channelRequest) + .requestId(requestId) + .generation(generation) + .build(); + + RootMessage message = new RootMessage.Builder() + .channelRequest(request) + .build(); + + try { + connection.writeMessage(message); + Log.d(TAG, "Sent open channel request: " + channel); + } catch (IOException e) { + Log.e(TAG, "Failed to send channel open request", e); + synchronized (lock) { + channels.remove(tokenString); + channelIdToToken.remove(channelId); + } + callback.onResult(ChannelStatusCodes.CHANNEL_NOT_CONNECTED, null, path); + } + + } catch (Exception e) { + Log.e(TAG, "Failed to open channel", e); + callback.onResult(ChannelStatusCodes.INTERNAL_ERROR, null, path); + } + } + + private long generateChannelId() { + return System.currentTimeMillis() ^ (random.nextLong() & 0xFFFFFFFFL); + } + + public ChannelStateMachine getChannel(String tokenString) { + synchronized (lock) { + return channels.get(tokenString); + } + } + + public ChannelStateMachine getChannel(ChannelToken token) { + return getChannel(token.toTokenString()); + } + + public void closeChannel(ChannelToken token, int errorCode) { + ChannelStateMachine channel = getChannel(token); + if (channel == null) { + Log.w(TAG, "closeChannel: channel not found"); + return; + } + + handler.post(() -> doCloseChannel(channel, errorCode)); + } + + private void doCloseChannel(ChannelStateMachine channel, int errorCode) { + try { + WearableConnection connection = wearable.getActiveConnections().get(channel.token.nodeId); + if (connection != null) { + ChannelControlRequest controlRequest = new ChannelControlRequest.Builder() + .type(CHANNEL_CONTROL_TYPE_CLOSE) + .channelId(channel.token.channelId) + .fromChannelOperator(channel.token.thisNodeWasOpener) + .packageName(channel.token.appKey.packageName) + .signatureDigest(channel.token.appKey.signatureDigest) + .closeErrorCode(errorCode) + .build(); + + ChannelRequest channelRequest = new ChannelRequest.Builder() + .channelControlRequest(controlRequest) + .version(1) + .origin(0) + .build(); + + int requestId = requestIdCounter.getAndIncrement(); + + Request request = new Request.Builder() + .requestId(requestId) + .packageName(channel.token.appKey.packageName) + .signatureDigest(channel.token.appKey.signatureDigest) + .targetNodeId(channel.token.nodeId) + .sourceNodeId(localNodeId) + .request(channelRequest) + .generation(generationCounter.get()) + .build(); + + try { + connection.writeMessage(new RootMessage.Builder() + .channelRequest(request) + .build()); + } catch (IOException e) { + Log.e(TAG, "Failed to send close request", e); + } + } + + channel.close(); + } catch (Exception e) { + Log.e(TAG, "Error closing channel", e); + } finally { + synchronized (lock) { + channels.remove(channel.token.toTokenString()); + channelIdToToken.remove(channel.token.channelId); + } + } + } + + public void onChannelRequestReceived(WearableConnection connection, String sourceNodeId, Request request) { + if (request.request == null) { + Log.w(TAG, "Received channel request with null ChannelRequest"); + return; + } + + ChannelRequest channelRequest = request.request; + + if (channelRequest.channelControlRequest != null) { + onChannelControlReceived(connection, sourceNodeId, request, channelRequest.channelControlRequest); + } else if (channelRequest.channelDataRequest != null) { + onChannelDataReceived(channelRequest.channelDataRequest); + } else if (channelRequest.channelDataAckRequest != null) { + onChannelDataAckReceived(channelRequest.channelDataAckRequest); + } + } + + + private void onChannelControlReceived(WearableConnection connection, String sourceNodeId, Request request, ChannelControlRequest control) { + int type = control.type; + Log.d(TAG, "onChannelControlReceived: type=" + type + ", channelId=" + control.channelId); + + switch (type) { + case CHANNEL_CONTROL_TYPE_OPEN: + onChannelOpenReceived(connection, sourceNodeId, request, control); + break; + case CHANNEL_CONTROL_TYPE_OPEN_ACK: + onChannelOpenAckReceived(control); + break; + case CHANNEL_CONTROL_TYPE_CLOSE: + onChannelCloseReceived(control); + break; + default: + Log.w(TAG, "Unknown channel control type: " + type); + } + } + + private void onChannelOpenReceived(WearableConnection connection, String sourceNodeId, Request request, ChannelControlRequest control) { + Log.d(TAG, "onChannelOpenReceived: channelId=" + control.channelId + + ", path=" + control.path + ", from=" + sourceNodeId); + + handler.post(() -> { + try { + AppKey appKey = new AppKey(control.packageName, control.signatureDigest); + + ChannelToken token = new ChannelToken( + sourceNodeId, appKey, control.channelId, false + ); + String tokenString = token.toTokenString(); + + IBinder.DeathRecipient deathRecipient = () -> onBinderDied(token); + + ChannelStateMachine channel = new ChannelStateMachine( + token, this, channelCallbacks, false, deathRecipient + ); + channel.setPath(control.path); + channel.setConnectionState(ChannelStateMachine.CONNECTION_STATE_ESTABLISHED); + + synchronized (lock) { + channels.put(tokenString, channel); + channelIdToToken.put(control.channelId, tokenString); + } + + sendOpenAck(connection, sourceNodeId, control, appKey); + + Log.d(TAG, "Channel opened by remote: " + channel); + + if (channelCallbacks != null) { + channelCallbacks.onChannelOpened(token, control.path); + } + + } catch (Exception e) { + Log.e(TAG, "Error handling channel open request", e); + } + }); + } + + private void sendOpenAck(WearableConnection connection, String targetNodeId, + ChannelControlRequest originalRequest, AppKey appKey) { + ChannelControlRequest ackControl = new ChannelControlRequest.Builder() + .type(CHANNEL_CONTROL_TYPE_OPEN_ACK) + .channelId(originalRequest.channelId) + .fromChannelOperator(false) + .packageName(appKey.packageName) + .signatureDigest(appKey.signatureDigest) + .path(originalRequest.path) + .build(); + + ChannelRequest channelRequest = new ChannelRequest.Builder() + .channelControlRequest(ackControl) + .version(1) + .origin(0) + .build(); + + int requestId = requestIdCounter.getAndIncrement(); + + Request request = new Request.Builder() + .requestId(requestId) + .packageName(appKey.packageName) + .signatureDigest(appKey.signatureDigest) + .targetNodeId(targetNodeId) + .sourceNodeId(localNodeId) + .request(channelRequest) + .generation(generationCounter.get()) + .build(); + + try { + connection.writeMessage(new RootMessage.Builder() + .channelRequest(request) + .build()); + Log.d(TAG, "Sent channel open ack for channelId=" + originalRequest.channelId); + } catch (IOException e) { + Log.e(TAG, "Failed to send open ack", e); + } + } + + private void onChannelOpenAckReceived(ChannelControlRequest control) { + Log.d(TAG, "onChannelOpenAckReceived: channelId=" + control.channelId); + + handler.post(() -> { + String tokenString; + synchronized (lock) { + tokenString = channelIdToToken.get(control.channelId); + } + + if (tokenString == null) { + Log.w(TAG, "Received open ack for unknown channelId: " + control.channelId); + return; + } + + ChannelStateMachine channel = getChannel(tokenString); + if (channel == null) { + Log.w(TAG, "Channel not found for token: " + tokenString); + return; + } + + channel.onChannelEstablished(); + Log.d(TAG, "Channel established: " + channel); + }); + } + + void onChannelCloseReceived(ChannelControlRequest control) { + Log.d(TAG, "onChannelCloseReceived: channelId=" + control.channelId); + + handler.post(() -> { + String tokenString; + synchronized (lock) { + tokenString = channelIdToToken.get(control.channelId); + } + + if (tokenString == null) { + Log.w(TAG, "Received close for unknown channelId: " + control.channelId); + return; + } + + ChannelStateMachine channel = getChannel(tokenString); + if (channel == null) { + Log.w(TAG, "Channel not found for token: " + tokenString); + return; + } + + try { + int errorCode = control.closeErrorCode; + channel.onRemoteClose(errorCode); + } catch (Exception e) { + Log.e(TAG, "Error handling channel close", e); + } finally { + synchronized (lock) { + channels.remove(tokenString); + channelIdToToken.remove(control.channelId); + } + } + }); + } + + private void onChannelDataReceived(ChannelDataRequest dataRequest) { + if (dataRequest.header == null) { + Log.w(TAG, "Received data request with null header"); + return; + } + + ChannelDataHeader header = dataRequest.header; + Log.d(TAG, "onChannelDataReceived: channelId=" + header.channelId + + ", size=" + (dataRequest.payload != null ? dataRequest.payload.size() : 0)); + + handler.post(() -> { + String tokenString; + synchronized (lock) { + tokenString = channelIdToToken.get(header.channelId); + } + + if (tokenString == null) { + Log.w(TAG, "Received data for unknown channelId: " + header.channelId); + return; + } + + ChannelStateMachine channel = getChannel(tokenString); + if (channel == null) { + Log.w(TAG, "Channel not found for token: " + tokenString); + return; + } + + try { + byte[] data = dataRequest.payload != null ? dataRequest.payload.toByteArray() : new byte[0]; + boolean isFinal = dataRequest.finalMessage; + long requestId = header.requestId; + + channel.onDataReceived(data, isFinal, requestId); + + sendDataAck(channel, requestId, isFinal); + + } catch (Exception e) { + Log.e(TAG, "Error handling channel data", e); + } + }); + } + + private void onChannelDataAckReceived(ChannelDataAckRequest ackRequest) { + if (ackRequest.header == null) { + Log.w(TAG, "Received data ack with null header"); + return; + } + + ChannelDataHeader header = ackRequest.header; + Log.d(TAG, "onChannelDataAckReceived: channelId=" + header.channelId); + + handler.post(() -> { + String tokenString; + synchronized (lock) { + tokenString = channelIdToToken.get(header.channelId); + } + + if (tokenString == null) { + Log.w(TAG, "Received ack for unknown channelId: " + header.channelId); + return; + } + + ChannelStateMachine channel = getChannel(tokenString); + if (channel == null) { + Log.w(TAG, "Channel not found for token: " + tokenString); + return; + } + + try { + long requestId = header.requestId; + boolean isFinal = ackRequest.finalMessage; + channel.onDataAckReceived(requestId, isFinal); + } catch (Exception e) { + Log.e(TAG, "Error handling data ack", e); + } + }); + } + + private void sendDataAck(ChannelStateMachine channel, long requestId, boolean isFinal) { + WearableConnection connection = wearable.getActiveConnections().get(channel.token.nodeId); + if (connection == null) { + Log.w(TAG, "Cannot send ack - connection not found"); + return; + } + + try { + ChannelDataHeader header = new ChannelDataHeader.Builder() + .channelId(channel.token.channelId) + .fromChannelOperator(channel.token.thisNodeWasOpener) + .requestId(requestId) + .build(); + + ChannelDataAckRequest ackRequest = new ChannelDataAckRequest.Builder() + .header(header) + .finalMessage(isFinal) + .build(); + + ChannelRequest channelRequest = new ChannelRequest.Builder() + .channelDataAckRequest(ackRequest) + .version(1) + .origin(0) + .build(); + + Request request = new Request.Builder() + .requestId(requestIdCounter.getAndIncrement()) + .targetNodeId(channel.token.nodeId) + .sourceNodeId(localNodeId) + .packageName(channel.token.appKey.packageName) + .signatureDigest(channel.token.appKey.signatureDigest) + .request(channelRequest) + .generation(generationCounter.get()) + .build(); + + connection.writeMessage(new RootMessage.Builder() + .channelRequest(request) + .build()); + } catch (IOException e) { + Log.e(TAG, "Failed to send data ack", e); + } + } + + public boolean sendData(ChannelStateMachine channel, byte[] data, boolean isFinal, long requestId) { + WearableConnection connection = wearable.getActiveConnections().get(channel.token.nodeId); + if (connection == null) { + Log.w(TAG, "Cannot send data - connection not found"); + return false; + } + + try { + ChannelDataHeader header = new ChannelDataHeader.Builder() + .channelId(channel.token.channelId) + .fromChannelOperator(channel.token.thisNodeWasOpener) + .requestId(requestId) + .build(); + + ChannelDataRequest dataRequest = new ChannelDataRequest.Builder() + .header(header) + .payload(ByteString.of(data)) + .finalMessage(isFinal) + .build(); + + ChannelRequest channelRequest = new ChannelRequest.Builder() + .channelDataRequest(dataRequest) + .version(1) + .origin(0) + .build(); + + Request request = new Request.Builder() + .requestId(requestIdCounter.getAndIncrement()) + .targetNodeId(channel.token.nodeId) + .sourceNodeId(localNodeId) + .packageName(channel.token.appKey.packageName) + .signatureDigest(channel.token.appKey.signatureDigest) + .request(channelRequest) + .generation(generationCounter.get()) + .build(); + + connection.writeMessage(new RootMessage.Builder() + .channelRequest(request) + .build()); + return true; + } catch (IOException e) { + Log.e(TAG, "Failed to send channel data", e); + return false; + } + } + + + private void onBinderDied(ChannelToken token) { + Log.w(TAG, "Client died for channel: " + token); + closeChannel(token, ChannelStatusCodes.CLOSE_REASON_LOCAL_CLOSE); + } + + public static void sendFileResult(IWearableCallbacks callbacks, int statusCode) { + try { + callbacks.onChannelSendFileResponse(new ChannelSendFileResponse(statusCode)); + } catch (RemoteException e) { + Log.w(TAG, "Failed to send sendFile result", e); + } + } + + public static void receiveFileResult(IWearableCallbacks callbacks, int statusCode) { + try { + callbacks.onChannelReceiveFileResponse(new ChannelReceiveFileResponse(statusCode)); + } catch (RemoteException e) { + Log.w(TAG, "Failed to send receiveFile result", e); + } + } + + public static void getInputStreamError(IWearableCallbacks callbacks, int statusCode) { + try { + callbacks.onGetChannelInputStreamResponse(new GetChannelInputStreamResponse(statusCode, null)); + } catch (RemoteException e) { + Log.w(TAG, "Failed to send getInputStream error", e); + } + } + + public static void getOutputStreamError(IWearableCallbacks callbacks, int statusCode) { + try { + callbacks.onGetChannelOutputStreamResponse(new GetChannelOutputStreamResponse(statusCode, null)); + } catch (RemoteException e) { + Log.w(TAG, "Failed to send getOutputStream error", e); + } + } +} \ No newline at end of file diff --git a/play-services-wearable/core/src/main/java/org/microg/gms/wearable/channel/ChannelStateMachine.java b/play-services-wearable/core/src/main/java/org/microg/gms/wearable/channel/ChannelStateMachine.java new file mode 100644 index 0000000000..145d372694 --- /dev/null +++ b/play-services-wearable/core/src/main/java/org/microg/gms/wearable/channel/ChannelStateMachine.java @@ -0,0 +1,340 @@ +package org.microg.gms.wearable.channel; + +import android.os.IBinder; +import android.os.ParcelFileDescriptor; +import android.os.RemoteException; +import android.util.Log; + +import com.google.android.gms.wearable.internal.IChannelStreamCallbacks; + +import java.io.FileOutputStream; +import java.io.IOException; +import java.nio.ByteBuffer; + +public class ChannelStateMachine { + private static final String TAG = "ChannelStateMachine"; + + public static final int CONNECTION_STATE_NOT_STARTED = 0; + public static final int CONNECTION_STATE_OPEN_SENT = 1; + public static final int CONNECTION_STATE_ESTABLISHED = 2; + public static final int CONNECTION_STATE_CLOSING = 3; + public static final int CONNECTION_STATE_CLOSED = 4; + public static final int SENDING_STATE_NOT_STARTED = 5; + public static final int SENDING_STATE_WAITING_TO_READ = 6; + public static final int SENDING_STATE_WAITING_FOR_ACK = 7; + public static final int SENDING_STATE_CLOSED = 8; + public static final int RECEIVING_STATE_WAITING_FOR_DATA = 9; + public static final int RECEIVING_STATE_WAITING_TO_WRITE = 10; + public static final int RECEIVING_STATE_CLOSED = 11; + + public final ChannelToken token; + public final boolean isLocalOpener; + + private final ChannelManager channelManager; + private final ChannelCallbacks callbacks; + private final IBinder.DeathRecipient deathRecipient; + + private int connectionState = CONNECTION_STATE_NOT_STARTED; + private int sendingState = SENDING_STATE_NOT_STARTED; + private int receivingState = RECEIVING_STATE_WAITING_FOR_DATA; + + private String path; + private long sequenceNumberIn; + private long sequenceNumberOut; + + private ParcelFileDescriptor inputFd; + private IChannelStreamCallbacks inputCallbacks; + private ByteBuffer receiveBuffer; + + private ParcelFileDescriptor outputFd; + private IChannelStreamCallbacks outputCallbacks; + private ByteBuffer sendBuffer; + private long sendOffset; + private long sendMaxLength; + private int pendingCloseErrorCode; + + private OpenChannelCallback openCallback; + + public ChannelStateMachine( + ChannelToken token, + ChannelManager channelManager, + ChannelCallbacks callbacks, + boolean isLocalOpener, + IBinder.DeathRecipient deathRecipient) { + + this.token = token; + this.channelManager = channelManager; + this.callbacks = callbacks; + this.isLocalOpener = isLocalOpener; + this.deathRecipient = deathRecipient; + } + + public int getConnectionState() { return connectionState; } + public int getSendingState() { return sendingState; } + public int getReceivingState() { return receivingState; } + public String getPath() { return path; } + public void setPath(String path) { this.path = path; } + + public void setConnectionState(int newState) { + if (Log.isLoggable(TAG, Log.VERBOSE)) { + Log.v(TAG, String.format("Channel(%s): %s -> %s", + token, getConnectionStateString(connectionState), getConnectionStateString(newState))); + } + this.connectionState = newState; + } + + public void setSendingState(int newState) { + if (Log.isLoggable(TAG, Log.VERBOSE)) { + Log.v(TAG, String.format("Channel(%s): Sender %s -> %s", + token, getSendingStateString(sendingState), getSendingStateString(newState))); + } + this.sendingState = newState; + } + + public void setReceivingState(int newState) { + if (Log.isLoggable(TAG, Log.VERBOSE)) { + Log.v(TAG, String.format("Channel(%s): Receiver %s -> %s", + token, getReceivingStateString(receivingState), getReceivingStateString(newState))); + } + this.receivingState = newState; + } + + public boolean hasInputStream() { return inputFd != null; } + public boolean hasOutputStream() { return outputFd != null; } + public boolean isInputClosed() { return receivingState == RECEIVING_STATE_CLOSED; } + public boolean isOutputClosed() { return sendingState == SENDING_STATE_CLOSED; } + + public void setOpenCallback(OpenChannelCallback callback) { + this.openCallback = callback; + } + + public void onChannelEstablished() { + setConnectionState(CONNECTION_STATE_ESTABLISHED); + if (openCallback != null) { + openCallback.onResult(ChannelStatusCodes.SUCCESS, token, path); + openCallback = null; + } + } + + public void onOpenFailed(int errorCode) { + if (openCallback != null) { + Log.w(TAG, "openChannel failed with error: " + errorCode); + openCallback.onResult(errorCode, null, path); + openCallback = null; + } + setConnectionState(CONNECTION_STATE_CLOSED); + } + + public void onDataReceived(byte[] data, boolean isFinal, long requestId) throws IOException { + if (inputFd == null) { + Log.w(TAG, "Received data but no input FD set"); + return; + } + + try { + FileOutputStream fos = new FileOutputStream(inputFd.getFileDescriptor()); + fos.write(data); + } catch (IOException e) { + Log.e(TAG, "Failed to write received data", e); + throw e; + } + + if (isFinal) { + closeInputStream(ChannelStatusCodes.CLOSE_REASON_NORMAL, 0); + } + } + + public void onDataAckReceived(long requestId, boolean isFinal) { + if (sendingState == SENDING_STATE_WAITING_FOR_ACK) { + if (isFinal) { + setSendingState(SENDING_STATE_CLOSED); + } else { + setSendingState(SENDING_STATE_WAITING_TO_READ); + } + } + } + + public void onRemoteClose(int errorCode) throws IOException { + closeInputStream(ChannelStatusCodes.CLOSE_REASON_REMOTE_CLOSE, errorCode); + closeOutputStream(ChannelStatusCodes.CLOSE_REASON_REMOTE_CLOSE, errorCode); + setConnectionState(CONNECTION_STATE_CLOSED); + + if (callbacks != null) { + callbacks.onChannelClosed(token, path, + ChannelStatusCodes.CLOSE_REASON_REMOTE_CLOSE, errorCode); + } + } + + public void close() throws IOException { + setConnectionState(CONNECTION_STATE_CLOSED); + + if (openCallback != null) { + openCallback.onResult(ChannelStatusCodes.CHANNEL_CLOSED, null, path); + openCallback = null; + } + + closeInputStream(ChannelStatusCodes.CLOSE_REASON_LOCAL_CLOSE, 0); + closeOutputStream(ChannelStatusCodes.CLOSE_REASON_LOCAL_CLOSE, 0); + + receiveBuffer = null; + sendBuffer = null; + } + + public void closeInputStream(int closeReason, int errorCode) throws IOException { + if (inputFd == null) return; + + if (inputCallbacks != null) { + unlinkToDeath(inputCallbacks.asBinder()); + if (closeReason != ChannelStatusCodes.CLOSE_REASON_NORMAL) { + try { + inputCallbacks.onChannelClosed(closeReason, errorCode); + } catch (RemoteException e) { + Log.w(TAG, "Failed to notify InputStream of close", e); + } + } + } + + try { + inputFd.close(); + } catch (IOException e) { + Log.w(TAG, "Failed to close receiving FD", e); + } + + inputFd = null; + inputCallbacks = null; + setReceivingState(RECEIVING_STATE_CLOSED); + + if (callbacks != null) { + callbacks.onChannelInputClosed(token, path, closeReason, errorCode); + } + } + + public void closeOutputStream(int closeReason, int errorCode) throws IOException { + if (outputFd == null) return; + + if (outputCallbacks != null) { + unlinkToDeath(outputCallbacks.asBinder()); + if (closeReason != ChannelStatusCodes.CLOSE_REASON_NORMAL) { + try { + outputCallbacks.onChannelClosed(closeReason, errorCode); + } catch (RemoteException e) { + Log.w(TAG, "Failed to notify OutputStream of close", e); + } + } + } + + try { + outputFd.close(); + } catch (IOException e) { + Log.w(TAG, "Failed to close sending FD", e); + } + + outputFd = null; + outputCallbacks = null; + setSendingState(SENDING_STATE_CLOSED); + + if (callbacks != null) { + callbacks.onChannelOutputClosed(token, path, closeReason, errorCode); + } + } + + public void setInputStream(ParcelFileDescriptor fd, IChannelStreamCallbacks callbacks) + throws RemoteException { + if (receivingState == RECEIVING_STATE_CLOSED) { + throw new IllegalStateException("Cannot set input FD after closing"); + } + if (inputFd != null) { + throw new IllegalStateException("Input FD already set"); + } + if (fd == null) { + throw new NullPointerException("fd is null"); + } + + this.inputFd = fd; + this.inputCallbacks = callbacks; + + linkToDeath(callbacks); + } + + public void setOutputStream(ParcelFileDescriptor fd, IChannelStreamCallbacks callbacks, + long startOffset, long length) throws RemoteException { + if (startOffset < 0) { + throw new IllegalArgumentException("invalid startOffset " + startOffset); + } + if (length != -1 && length < 0) { + throw new IllegalArgumentException("invalid length " + length); + } + if (sendingState != SENDING_STATE_NOT_STARTED) { + throw new IllegalStateException("Output FD already set"); + } + if (outputFd != null) { + throw new IllegalStateException("Output FD already set"); + } + if (fd == null) { + throw new NullPointerException("fd is null"); + } + + this.outputFd = fd; + this.outputCallbacks = callbacks; + this.sendOffset = startOffset; + this.sendMaxLength = length; + + setSendingState(SENDING_STATE_WAITING_TO_READ); + linkToDeath(callbacks); + } + + private void linkToDeath(IChannelStreamCallbacks callbacks) throws RemoteException { + if (callbacks == null || deathRecipient == null) return; + try { + callbacks.asBinder().linkToDeath(deathRecipient, 0); + } catch (RemoteException e) { + deathRecipient.binderDied(); + } + } + + private void unlinkToDeath(IBinder binder) { + if (binder == null || deathRecipient == null) return; + try { + binder.unlinkToDeath(deathRecipient, 0); + } catch (Exception ignored) {} + } + + public static String getConnectionStateString(int state) { + switch (state) { + case CONNECTION_STATE_NOT_STARTED: return "NOT_STARTED"; + case CONNECTION_STATE_OPEN_SENT: return "OPEN_SENT"; + case CONNECTION_STATE_ESTABLISHED: return "ESTABLISHED"; + case CONNECTION_STATE_CLOSING: return "CLOSING"; + case CONNECTION_STATE_CLOSED: return "CLOSED"; + default: return "UNKNOWN(" + state + ")"; + } + } + + public static String getSendingStateString(int state) { + switch (state) { + case SENDING_STATE_NOT_STARTED: return "NOT_STARTED"; + case SENDING_STATE_WAITING_TO_READ: return "WAITING_TO_READ"; + case SENDING_STATE_WAITING_FOR_ACK: return "WAITING_FOR_ACK"; + case SENDING_STATE_CLOSED: return "CLOSED"; + default: return "UNKNOWN(" + state + ")"; + } + } + + public static String getReceivingStateString(int state) { + switch (state) { + case RECEIVING_STATE_WAITING_FOR_DATA: return "WAITING_FOR_DATA"; + case RECEIVING_STATE_WAITING_TO_WRITE: return "WAITING_TO_WRITE"; + case RECEIVING_STATE_CLOSED: return "CLOSED"; + default: return "UNKNOWN(" + state + ")"; + } + } + + @Override + public String toString() { + return "ChannelStateMachine{token=" + token + + ", path='" + path + "'" + + ", connection=" + getConnectionStateString(connectionState) + + ", sending=" + getSendingStateString(sendingState) + + ", receiving=" + getReceivingStateString(receivingState) + "}"; + } +} \ No newline at end of file diff --git a/play-services-wearable/core/src/main/java/org/microg/gms/wearable/channel/ChannelStatusCodes.java b/play-services-wearable/core/src/main/java/org/microg/gms/wearable/channel/ChannelStatusCodes.java new file mode 100644 index 0000000000..04c0483333 --- /dev/null +++ b/play-services-wearable/core/src/main/java/org/microg/gms/wearable/channel/ChannelStatusCodes.java @@ -0,0 +1,31 @@ +package org.microg.gms.wearable.channel; + +public class ChannelStatusCodes { + public static final int SUCCESS = 0; + public static final int CLOSE_REASON_NORMAL = 0; + public static final int CLOSE_REASON_LOCAL_CLOSE = 1; + public static final int CLOSE_REASON_REMOTE_CLOSE = 3; + public static final int INTERNAL_ERROR = 8; + public static final int CHANNEL_NOT_CONNECTED = 13; + public static final int CHANNEL_CLOSED = 16; + + // Custom codes + public static final int INVALID_ARGUMENT = 10003; + public static final int CHANNEL_NOT_FOUND = 10004; + public static final int ALREADY_IN_PROGRESS = 10005; + + public static String getStatusName(int status) { + switch (status) { + case SUCCESS: return "SUCCESS"; + case CLOSE_REASON_LOCAL_CLOSE: return "LOCAL_CLOSE"; + case CLOSE_REASON_REMOTE_CLOSE: return "REMOTE_CLOSE"; + case INTERNAL_ERROR: return "INTERNAL_ERROR"; + case CHANNEL_NOT_CONNECTED: return "NOT_CONNECTED"; + case CHANNEL_CLOSED: return "CLOSED"; + case INVALID_ARGUMENT: return "INVALID_ARGUMENT"; + case CHANNEL_NOT_FOUND: return "NOT_FOUND"; + case ALREADY_IN_PROGRESS: return "ALREADY_IN_PROGRESS"; + default: return "UNKNOWN(" + status + ")"; + } + } +} \ No newline at end of file diff --git a/play-services-wearable/core/src/main/java/org/microg/gms/wearable/channel/ChannelToken.java b/play-services-wearable/core/src/main/java/org/microg/gms/wearable/channel/ChannelToken.java new file mode 100644 index 0000000000..9f531776f7 --- /dev/null +++ b/play-services-wearable/core/src/main/java/org/microg/gms/wearable/channel/ChannelToken.java @@ -0,0 +1,136 @@ +package org.microg.gms.wearable.channel; + +import android.util.Base64; + +import com.google.android.gms.wearable.internal.ChannelParcelable; + +import org.microg.wearable.proto.AppKey; + +public final class ChannelToken { + private static final String TAG = "ChannelToken"; + private static final String TOKEN_PREFIX = "chl-"; + + public final String nodeId; + public final AppKey appKey; + public final long channelId; + public final boolean thisNodeWasOpener; + + public ChannelToken(String nodeId, AppKey appKey, long channelId, boolean thisNodeWasOpener) { + if (nodeId == null) throw new NullPointerException("nodeId is null"); + if (appKey == null) throw new NullPointerException("appKey is null"); + if (channelId < 0) throw new IllegalArgumentException("Negative channelId: " + channelId); + + this.nodeId = nodeId; + this.appKey = appKey; + this.channelId = channelId; + this.thisNodeWasOpener = thisNodeWasOpener; + } + + public static ChannelToken fromString(AppKey expectedAppKey, String tokenString) + throws InvalidChannelTokenException { + if (expectedAppKey == null) throw new NullPointerException("expectedAppKey is null"); + if (tokenString == null || !tokenString.startsWith(TOKEN_PREFIX)) { + throw new InvalidChannelTokenException("Invalid token prefix"); + } + + try { + byte[] data = Base64.decode(tokenString.substring(TOKEN_PREFIX.length()), Base64.DEFAULT); + ChannelTokenProto proto = ChannelTokenProto.parseFrom(data); + + if (proto.nodeId == null || proto.packageName == null || + proto.signatureDigest == null || proto.channelId < 0) { + throw new InvalidChannelTokenException("Missing required fields"); + } + + AppKey tokenAppKey = new AppKey(proto.packageName, proto.signatureDigest); + if (!expectedAppKey.equals(tokenAppKey)) { + throw new InvalidChannelTokenException("AppKey mismatch"); + } + + return new ChannelToken( + proto.nodeId, + tokenAppKey, + proto.channelId, + proto.thisNodeWasOpener + ); + } catch (InvalidChannelTokenException e) { + throw e; + } catch (Exception e) { + throw new InvalidChannelTokenException("Failed to parse token", e); + } + } + + public String toTokenString() { + ChannelTokenProto proto = new ChannelTokenProto(); + proto.nodeId = nodeId; + proto.packageName = appKey.packageName; + proto.signatureDigest = appKey.signatureDigest; + proto.channelId = channelId; + proto.thisNodeWasOpener = thisNodeWasOpener; + + return TOKEN_PREFIX + Base64.encodeToString(proto.toByteArray(), Base64.NO_WRAP); + } + + public ChannelParcelable toParcelable(String path) { + return new ChannelParcelable(toTokenString(), nodeId, path); + } + + @Override + public boolean equals(Object obj) { + if (obj == this) return true; + if (!(obj instanceof ChannelToken)) return false; + ChannelToken other = (ChannelToken) obj; + return channelId == other.channelId + && thisNodeWasOpener == other.thisNodeWasOpener + && appKey.equals(other.appKey) + && nodeId.equals(other.nodeId); + } + + @Override + public int hashCode() { + int result = ((nodeId.hashCode() + 527) * 31) + appKey.hashCode(); + result = (result * 31) + Long.hashCode(channelId); + return (result * 31) + (thisNodeWasOpener ? 1 : 0); + } + + @Override + public String toString() { + return "ChannelToken[nodeId='" + nodeId + "', appKey=" + appKey + + ", channelId=" + channelId + ", thisNodeWasOpener=" + thisNodeWasOpener + "]"; + } + + static class ChannelTokenProto { + String nodeId; + String packageName; + String signatureDigest; + long channelId; + boolean thisNodeWasOpener; + + static ChannelTokenProto parseFrom(byte[] data) throws Exception { + ChannelTokenProto proto = new ChannelTokenProto(); + java.io.DataInputStream dis = new java.io.DataInputStream( + new java.io.ByteArrayInputStream(data)); + proto.nodeId = dis.readUTF(); + proto.packageName = dis.readUTF(); + proto.signatureDigest = dis.readUTF(); + proto.channelId = dis.readLong(); + proto.thisNodeWasOpener = dis.readBoolean(); + return proto; + } + + byte[] toByteArray() { + try { + java.io.ByteArrayOutputStream baos = new java.io.ByteArrayOutputStream(); + java.io.DataOutputStream dos = new java.io.DataOutputStream(baos); + dos.writeUTF(nodeId); + dos.writeUTF(packageName); + dos.writeUTF(signatureDigest); + dos.writeLong(channelId); + dos.writeBoolean(thisNodeWasOpener); + return baos.toByteArray(); + } catch (Exception e) { + throw new RuntimeException(e); + } + } + } +} \ No newline at end of file diff --git a/play-services-wearable/core/src/main/java/org/microg/gms/wearable/channel/InvalidChannelTokenException.java b/play-services-wearable/core/src/main/java/org/microg/gms/wearable/channel/InvalidChannelTokenException.java new file mode 100644 index 0000000000..2c2259f53f --- /dev/null +++ b/play-services-wearable/core/src/main/java/org/microg/gms/wearable/channel/InvalidChannelTokenException.java @@ -0,0 +1,7 @@ +package org.microg.gms.wearable.channel; + +public class InvalidChannelTokenException extends Exception { + public InvalidChannelTokenException() { super(); } + public InvalidChannelTokenException(String message) { super(message); } + public InvalidChannelTokenException(String message, Throwable cause) { super(message, cause); } +} \ No newline at end of file diff --git a/play-services-wearable/core/src/main/java/org/microg/gms/wearable/channel/OpenChannelCallback.java b/play-services-wearable/core/src/main/java/org/microg/gms/wearable/channel/OpenChannelCallback.java new file mode 100644 index 0000000000..00c12a45ae --- /dev/null +++ b/play-services-wearable/core/src/main/java/org/microg/gms/wearable/channel/OpenChannelCallback.java @@ -0,0 +1,5 @@ +package org.microg.gms.wearable.channel; + +public interface OpenChannelCallback { + void onResult(int statusCode, ChannelToken token, String path); +} \ No newline at end of file diff --git a/play-services-wearable/src/main/aidl/com/google/android/gms/wearable/internal/IChannelStreamCallbacks.aidl b/play-services-wearable/src/main/aidl/com/google/android/gms/wearable/internal/IChannelStreamCallbacks.aidl index 1f08e37338..183fafdf4a 100644 --- a/play-services-wearable/src/main/aidl/com/google/android/gms/wearable/internal/IChannelStreamCallbacks.aidl +++ b/play-services-wearable/src/main/aidl/com/google/android/gms/wearable/internal/IChannelStreamCallbacks.aidl @@ -1,4 +1,5 @@ package com.google.android.gms.wearable.internal; interface IChannelStreamCallbacks { + void onChannelClosed(int closeReason, int appSpecificErrorCode) = 2; } diff --git a/play-services-wearable/src/main/aidl/com/google/android/gms/wearable/internal/IWearableService.aidl b/play-services-wearable/src/main/aidl/com/google/android/gms/wearable/internal/IWearableService.aidl index 536b273c19..e04142fbe8 100644 --- a/play-services-wearable/src/main/aidl/com/google/android/gms/wearable/internal/IWearableService.aidl +++ b/play-services-wearable/src/main/aidl/com/google/android/gms/wearable/internal/IWearableService.aidl @@ -26,6 +26,9 @@ interface IWearableService { void enableConfig(IWearableCallbacks callbacks, String name) = 22; // aka enableConnection void disableConfig(IWearableCallbacks callbacks, String name) = 23; + void getRelatedConfigs(IWearableCallbacks callbacks) = 72; + void updateConfig(IWearableCallbacks iWearableCallbacks, in ConnectionConfiguration config) = 73; + // DataItems void putData(IWearableCallbacks callbacks, in PutDataRequest request) = 5; void getDataItem(IWearableCallbacks callbacks, in Uri uri) = 6; @@ -90,10 +93,14 @@ interface IWearableService { void getConsentStatus(IWearableCallbacks callbacks) = 64; void addAccountToConsent(IWearableCallbacks callbacks, in AddAccountToConsentRequest request) = 65; +// void privacyRecordOptinRequest(IWearableCallbacks callbacks, in PrivacyRecordOptinRequest request) = 70; + void someBoolUnknown(IWearableCallbacks callbacks) = 84; // cannot figure out name void getCompanionPackageForNode(IWearableCallbacks callbacks, String nodeId) = 62; + void setCloudSyncSettingByNode(IWearableCallbacks callbacks, String s, boolean b) = 74; + void logCounter(IWearableCallbacks callbacks, in LogCounterRequest request) = 105; void logEvent(IWearableCallbacks callbacks, in LogEventRequest request) = 106; void logTimer(IWearableCallbacks callbacks, in LogTimerRequest request) = 107; diff --git a/play-services-wearable/src/main/java/com/google/android/gms/wearable/internal/ChannelReceiveFileResponse.java b/play-services-wearable/src/main/java/com/google/android/gms/wearable/internal/ChannelReceiveFileResponse.java index 9a6a05fe4b..7582c35fa2 100644 --- a/play-services-wearable/src/main/java/com/google/android/gms/wearable/internal/ChannelReceiveFileResponse.java +++ b/play-services-wearable/src/main/java/com/google/android/gms/wearable/internal/ChannelReceiveFileResponse.java @@ -22,5 +22,14 @@ public class ChannelReceiveFileResponse extends AutoSafeParcelable { @SafeParceled(1) private int versionCode = 1; + @SafeParceled(2) + public int status = 1; + + private ChannelReceiveFileResponse() {} + + public ChannelReceiveFileResponse(int status) { + this.status = status; + } + public static final Creator CREATOR = new AutoCreator(ChannelReceiveFileResponse.class); } diff --git a/play-services-wearable/src/main/java/com/google/android/gms/wearable/internal/ChannelSendFileResponse.java b/play-services-wearable/src/main/java/com/google/android/gms/wearable/internal/ChannelSendFileResponse.java index 09a2cb19a6..718bb8d168 100644 --- a/play-services-wearable/src/main/java/com/google/android/gms/wearable/internal/ChannelSendFileResponse.java +++ b/play-services-wearable/src/main/java/com/google/android/gms/wearable/internal/ChannelSendFileResponse.java @@ -1,6 +1,6 @@ /* * Copyright (C) 2019 microG Project Team - * + *ChannelReceiveFileResponse * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at @@ -22,5 +22,14 @@ public class ChannelSendFileResponse extends AutoSafeParcelable { @SafeParceled(1) private int versionCode = 1; + @SafeParceled(2) + public int status = 1; + + private ChannelSendFileResponse() {} + + public ChannelSendFileResponse(int status) { + this.status = status; + } + public static final Creator CREATOR = new AutoCreator(ChannelSendFileResponse.class); } diff --git a/play-services-wearable/src/main/java/com/google/android/gms/wearable/internal/CloseChannelResponse.java b/play-services-wearable/src/main/java/com/google/android/gms/wearable/internal/CloseChannelResponse.java index 3520593b35..cd0c77caaf 100644 --- a/play-services-wearable/src/main/java/com/google/android/gms/wearable/internal/CloseChannelResponse.java +++ b/play-services-wearable/src/main/java/com/google/android/gms/wearable/internal/CloseChannelResponse.java @@ -22,5 +22,14 @@ public class CloseChannelResponse extends AutoSafeParcelable { @SafeParceled(1) private int versionCode = 1; + @SafeParceled(2) + public int status = 1; + + private CloseChannelResponse() {} + + public CloseChannelResponse(int status) { + this.status = status; + } + public static final Creator CREATOR = new AutoCreator(CloseChannelResponse.class); } diff --git a/play-services-wearable/src/main/java/com/google/android/gms/wearable/internal/GetChannelInputStreamResponse.java b/play-services-wearable/src/main/java/com/google/android/gms/wearable/internal/GetChannelInputStreamResponse.java index b5460a4373..2bff0caddb 100644 --- a/play-services-wearable/src/main/java/com/google/android/gms/wearable/internal/GetChannelInputStreamResponse.java +++ b/play-services-wearable/src/main/java/com/google/android/gms/wearable/internal/GetChannelInputStreamResponse.java @@ -16,11 +16,25 @@ package com.google.android.gms.wearable.internal; +import android.os.ParcelFileDescriptor; + import org.microg.safeparcel.AutoSafeParcelable; import org.microg.safeparcel.SafeParceled; public class GetChannelInputStreamResponse extends AutoSafeParcelable { @SafeParceled(1) private int versionCode = 1; + @SafeParceled(2) + public int statusCode; + @SafeParceled(3) + private ParcelFileDescriptor descriptor; + + private GetChannelInputStreamResponse() {} + + public GetChannelInputStreamResponse(int statusCode, ParcelFileDescriptor descriptor) { + this.statusCode = statusCode; + this.descriptor = descriptor; + } + public static final Creator CREATOR = new AutoCreator(GetChannelInputStreamResponse.class); } diff --git a/play-services-wearable/src/main/java/com/google/android/gms/wearable/internal/GetChannelOutputStreamResponse.java b/play-services-wearable/src/main/java/com/google/android/gms/wearable/internal/GetChannelOutputStreamResponse.java index 71e024e20b..60a1d77e66 100644 --- a/play-services-wearable/src/main/java/com/google/android/gms/wearable/internal/GetChannelOutputStreamResponse.java +++ b/play-services-wearable/src/main/java/com/google/android/gms/wearable/internal/GetChannelOutputStreamResponse.java @@ -16,11 +16,24 @@ package com.google.android.gms.wearable.internal; +import android.os.ParcelFileDescriptor; + import org.microg.safeparcel.AutoSafeParcelable; import org.microg.safeparcel.SafeParceled; public class GetChannelOutputStreamResponse extends AutoSafeParcelable { @SafeParceled(1) private int versionCode = 1; + @SafeParceled(2) + public int statusCode; + @SafeParceled(3) + private ParcelFileDescriptor descriptor; + + private GetChannelOutputStreamResponse() {} + + public GetChannelOutputStreamResponse(int statusCode, ParcelFileDescriptor descriptor) { + this.statusCode = statusCode; + this.descriptor = descriptor; + } public static final Creator CREATOR = new AutoCreator(GetChannelOutputStreamResponse.class); } diff --git a/play-services-wearable/src/main/java/com/google/android/gms/wearable/internal/OpenChannelResponse.java b/play-services-wearable/src/main/java/com/google/android/gms/wearable/internal/OpenChannelResponse.java index fcd97a228e..b8539b4d02 100644 --- a/play-services-wearable/src/main/java/com/google/android/gms/wearable/internal/OpenChannelResponse.java +++ b/play-services-wearable/src/main/java/com/google/android/gms/wearable/internal/OpenChannelResponse.java @@ -16,11 +16,24 @@ package com.google.android.gms.wearable.internal; +import org.microg.gms.wearable.ChannelImpl; import org.microg.safeparcel.AutoSafeParcelable; import org.microg.safeparcel.SafeParceled; public class OpenChannelResponse extends AutoSafeParcelable { @SafeParceled(1) private int versionCode = 1; + @SafeParceled(2) + public int statusCode; + @SafeParceled(3) + public ChannelParcelable channel; + + private OpenChannelResponse() {} + + public OpenChannelResponse (int statusCode, ChannelParcelable channel) { + this.statusCode = statusCode; + this.channel = channel; + } + public static final Creator CREATOR = new AutoCreator(OpenChannelResponse.class); } From 63a77a0ee8a0b8d349c63f6483f9c0204d031d9e Mon Sep 17 00:00:00 2001 From: deadYokai Date: Mon, 5 Jan 2026 11:12:11 +0200 Subject: [PATCH 09/29] Wearable lib relocation and some bluetooth changes --- play-services-wearable/core/build.gradle | 9 +- .../microg/gms/wearable/DataItemRecord.java | 6 +- .../microg/gms/wearable/MessageHandler.java | 23 ++- .../microg/gms/wearable/MessageListener.java | 82 +++++++++ .../gms/wearable/ServerMessageListener.java | 45 +++++ .../gms/wearable/SocketConnectionThread.java | 100 +++++++++++ .../wearable/SocketWearableConnection.java | 49 ++++++ .../gms/wearable/WearableConnection.java | 133 +++++++++++++++ .../org/microg/gms/wearable/WearableImpl.java | 78 +++++++-- .../gms/wearable/WearableServiceImpl.java | 11 +- .../wearable/bluetooth/BluetoothClient.java | 61 ++++++- .../bluetooth/BluetoothConnectionThread.java | 136 +++++++++++---- .../wearable/bluetooth/BluetoothServer.java | 5 +- .../BluetoothWearableConnection.java | 9 +- .../gms/wearable/channel/ChannelManager.java | 18 +- .../gms/wearable/channel/ChannelToken.java | 2 +- .../core/src/main/proto/wearable.proto | 161 ++++++++++++++++++ .../gms/wearable/internal/NodeParcelable.java | 9 +- 18 files changed, 849 insertions(+), 88 deletions(-) create mode 100644 play-services-wearable/core/src/main/java/org/microg/gms/wearable/MessageListener.java create mode 100644 play-services-wearable/core/src/main/java/org/microg/gms/wearable/ServerMessageListener.java create mode 100644 play-services-wearable/core/src/main/java/org/microg/gms/wearable/SocketConnectionThread.java create mode 100644 play-services-wearable/core/src/main/java/org/microg/gms/wearable/SocketWearableConnection.java create mode 100644 play-services-wearable/core/src/main/java/org/microg/gms/wearable/WearableConnection.java create mode 100644 play-services-wearable/core/src/main/proto/wearable.proto diff --git a/play-services-wearable/core/build.gradle b/play-services-wearable/core/build.gradle index 5675f7e736..6858188061 100644 --- a/play-services-wearable/core/build.gradle +++ b/play-services-wearable/core/build.gradle @@ -7,14 +7,13 @@ apply plugin: 'com.android.library' apply plugin: 'kotlin-android' apply plugin: 'maven-publish' apply plugin: 'signing' +apply plugin: 'com.squareup.wire' dependencies { implementation project(':play-services-base-core') implementation project(':play-services-location') implementation project(':play-services-wearable') - - implementation "org.microg:wearable:$wearableVersion" } android { @@ -51,6 +50,12 @@ android { } } +wire { + java { + + } +} + apply from: '../../gradle/publish-android.gradle' description = 'microG service implementation for play-services-wearable' diff --git a/play-services-wearable/core/src/main/java/org/microg/gms/wearable/DataItemRecord.java b/play-services-wearable/core/src/main/java/org/microg/gms/wearable/DataItemRecord.java index 6ed311facc..7ff7738b56 100644 --- a/play-services-wearable/core/src/main/java/org/microg/gms/wearable/DataItemRecord.java +++ b/play-services-wearable/core/src/main/java/org/microg/gms/wearable/DataItemRecord.java @@ -26,8 +26,8 @@ import com.google.android.gms.wearable.internal.DataItemAssetParcelable; import com.google.android.gms.wearable.internal.DataItemParcelable; -import org.microg.wearable.proto.AssetEntry; -import org.microg.wearable.proto.SetDataItem; +import org.microg.gms.wearable.proto.AssetEntry; +import org.microg.gms.wearable.proto.SetDataItem; import java.util.ArrayList; import java.util.HashMap; @@ -120,7 +120,7 @@ public SetDataItem toSetDataItem() { protoAssets.add(new AssetEntry.Builder() .key(key) .unknown3(4) - .value(new org.microg.wearable.proto.Asset.Builder() + .value(new org.microg.gms.wearable.proto.Asset.Builder() .digest(assets.get(key).getDigest()) .build()).build()); } diff --git a/play-services-wearable/core/src/main/java/org/microg/gms/wearable/MessageHandler.java b/play-services-wearable/core/src/main/java/org/microg/gms/wearable/MessageHandler.java index 0f12d92edd..3953ee9af5 100644 --- a/play-services-wearable/core/src/main/java/org/microg/gms/wearable/MessageHandler.java +++ b/play-services-wearable/core/src/main/java/org/microg/gms/wearable/MessageHandler.java @@ -26,18 +26,17 @@ import org.microg.gms.profile.Build; import org.microg.gms.settings.SettingsContract; -import org.microg.wearable.ServerMessageListener; -import org.microg.wearable.proto.AckAsset; -import org.microg.wearable.proto.Connect; -import org.microg.wearable.proto.FetchAsset; -import org.microg.wearable.proto.FilePiece; -import org.microg.wearable.proto.Heartbeat; -import org.microg.wearable.proto.Request; -import org.microg.wearable.proto.RootMessage; -import org.microg.wearable.proto.SetAsset; -import org.microg.wearable.proto.SetDataItem; -import org.microg.wearable.proto.SyncStart; -import org.microg.wearable.proto.SyncTableEntry; +import org.microg.gms.wearable.proto.AckAsset; +import org.microg.gms.wearable.proto.Connect; +import org.microg.gms.wearable.proto.FetchAsset; +import org.microg.gms.wearable.proto.FilePiece; +import org.microg.gms.wearable.proto.Heartbeat; +import org.microg.gms.wearable.proto.Request; +import org.microg.gms.wearable.proto.RootMessage; +import org.microg.gms.wearable.proto.SetAsset; +import org.microg.gms.wearable.proto.SetDataItem; +import org.microg.gms.wearable.proto.SyncStart; +import org.microg.gms.wearable.proto.SyncTableEntry; import java.io.IOException; import java.util.Arrays; diff --git a/play-services-wearable/core/src/main/java/org/microg/gms/wearable/MessageListener.java b/play-services-wearable/core/src/main/java/org/microg/gms/wearable/MessageListener.java new file mode 100644 index 0000000000..fdfd79f920 --- /dev/null +++ b/play-services-wearable/core/src/main/java/org/microg/gms/wearable/MessageListener.java @@ -0,0 +1,82 @@ +/* + * SPDX-FileCopyrightText: 2015, microG Project Team + * SPDX-License-Identifier: Apache-2.0 + */ + +package org.microg.gms.wearable; + +import org.microg.gms.wearable.proto.AckAsset; +import org.microg.gms.wearable.proto.Connect; +import org.microg.gms.wearable.proto.FetchAsset; +import org.microg.gms.wearable.proto.FilePiece; +import org.microg.gms.wearable.proto.Heartbeat; +import org.microg.gms.wearable.proto.Request; +import org.microg.gms.wearable.proto.RootMessage; +import org.microg.gms.wearable.proto.SetAsset; +import org.microg.gms.wearable.proto.SetDataItem; +import org.microg.gms.wearable.proto.SyncStart; + +public abstract class MessageListener implements WearableConnection.Listener { + private WearableConnection connection; + + @Override + public void onConnected(WearableConnection connection) { + this.connection = connection; + } + + @Override + public void onDisconnected() { + this.connection = null; + } + + public WearableConnection getConnection() { + return connection; + } + + @Override + public void onMessage(WearableConnection connection, RootMessage message) { + if (message.setAsset != null) { + onSetAsset(message.setAsset); + } else if (message.ackAsset != null) { + onAckAsset(message.ackAsset); + } else if (message.fetchAsset != null) { + onFetchAsset(message.fetchAsset); + } else if (message.connect != null) { + onConnect(message.connect); + } else if (message.syncStart != null) { + onSyncStart(message.syncStart); + } else if (message.setDataItem != null) { + onSetDataItem(message.setDataItem); + } else if (message.rpcRequest != null) { + onRpcRequest(message.rpcRequest); + } else if (message.heartbeat != null) { + onHeartbeat(message.heartbeat); + } else if (message.filePiece != null) { + onFilePiece(message.filePiece); + } else if (message.channelRequest != null) { + onChannelRequest(message.channelRequest); + } else { + System.err.println("Unknown message: " + message); + } + } + + public abstract void onSetAsset(SetAsset setAsset); + + public abstract void onAckAsset(AckAsset ackAsset); + + public abstract void onFetchAsset(FetchAsset fetchAsset); + + public abstract void onConnect(Connect connect); + + public abstract void onSyncStart(SyncStart syncStart); + + public abstract void onSetDataItem(SetDataItem setDataItem); + + public abstract void onRpcRequest(Request rpcRequest); + + public abstract void onHeartbeat(Heartbeat heartbeat); + + public abstract void onFilePiece(FilePiece filePiece); + + public abstract void onChannelRequest(Request channelRequest); +} diff --git a/play-services-wearable/core/src/main/java/org/microg/gms/wearable/ServerMessageListener.java b/play-services-wearable/core/src/main/java/org/microg/gms/wearable/ServerMessageListener.java new file mode 100644 index 0000000000..72c522c93f --- /dev/null +++ b/play-services-wearable/core/src/main/java/org/microg/gms/wearable/ServerMessageListener.java @@ -0,0 +1,45 @@ +/* + * SPDX-FileCopyrightText: 2015, microG Project Team + * SPDX-License-Identifier: Apache-2.0 + */ + +package org.microg.gms.wearable; + +import org.microg.gms.wearable.proto.Connect; +import org.microg.gms.wearable.proto.RootMessage; + +import java.io.IOException; + +public abstract class ServerMessageListener extends MessageListener { + private Connect localConnect; + private Connect remoteConnect; + + public ServerMessageListener(Connect localConnect) { + this.localConnect = localConnect; + } + + @Override + public void onConnected(WearableConnection connection) { + super.onConnected(connection); + try { + connection.writeMessage(new RootMessage.Builder().connect(localConnect).build()); + } catch (IOException ignored) { + // Will disconnect soon + } + } + + @Override + public void onDisconnected() { + super.onDisconnected(); + remoteConnect = null; + } + + @Override + public void onConnect(Connect connect) { + this.remoteConnect = connect; + } + + public Connect getRemoteConnect() { + return remoteConnect; + } +} diff --git a/play-services-wearable/core/src/main/java/org/microg/gms/wearable/SocketConnectionThread.java b/play-services-wearable/core/src/main/java/org/microg/gms/wearable/SocketConnectionThread.java new file mode 100644 index 0000000000..52b2664548 --- /dev/null +++ b/play-services-wearable/core/src/main/java/org/microg/gms/wearable/SocketConnectionThread.java @@ -0,0 +1,100 @@ +/* + * SPDX-FileCopyrightText: 2015, microG Project Team + * SPDX-License-Identifier: Apache-2.0 + */ + +package org.microg.gms.wearable; + +import java.io.IOException; +import java.net.ServerSocket; +import java.net.Socket; + +public abstract class SocketConnectionThread extends Thread { + + private SocketWearableConnection wearableConnection; + + private SocketConnectionThread() { + super(); + } + + protected void setWearableConnection(org.microg.gms.wearable.SocketWearableConnection wearableConnection) { + this.wearableConnection = wearableConnection; + } + + public SocketWearableConnection getWearableConnection() { + return wearableConnection; + } + + public abstract void close(); + + public static SocketConnectionThread serverListen(final int port, final WearableConnection.Listener listener) { + return new SocketConnectionThread() { + private ServerSocket serverSocket = null; + + @Override + public void close() { + if (serverSocket != null) { + try { + serverSocket.close(); + } catch (IOException ignored) { + } + serverSocket = null; + } + } + + @Override + public void run() { + try { + serverSocket = new ServerSocket(port); + Socket socket; + while ((socket = serverSocket.accept()) != null && !Thread.interrupted()) { + SocketWearableConnection connection = new SocketWearableConnection(socket, listener); + setWearableConnection(connection); + connection.run(); + } + } catch (IOException e) { + // quit + } finally { + try { + if (serverSocket != null) serverSocket.close(); + } catch (IOException e) { + } + } + } + }; + } + + public static SocketConnectionThread clientConnect(final int port, final WearableConnection.Listener listener) { + return new SocketConnectionThread() { + private Socket socket; + + @Override + public void close() { + if (socket != null) { + try { + socket.close(); + } catch (IOException ignored) { + } + socket = null; + } + } + + @Override + public void run() { + try { + socket = new Socket("127.0.0.1", port); + SocketWearableConnection connection = new SocketWearableConnection(socket, listener); + setWearableConnection(connection); + connection.run(); + } catch (IOException e) { + // quit + } finally { + try { + if (socket != null) socket.close(); + } catch (IOException e) { + } + } + } + }; + } +} diff --git a/play-services-wearable/core/src/main/java/org/microg/gms/wearable/SocketWearableConnection.java b/play-services-wearable/core/src/main/java/org/microg/gms/wearable/SocketWearableConnection.java new file mode 100644 index 0000000000..7a570f9c05 --- /dev/null +++ b/play-services-wearable/core/src/main/java/org/microg/gms/wearable/SocketWearableConnection.java @@ -0,0 +1,49 @@ +/* + * SPDX-FileCopyrightText: 2015, microG Project Team + * SPDX-License-Identifier: Apache-2.0 + */ + +package org.microg.gms.wearable; + +import org.microg.gms.wearable.proto.MessagePiece; + +import java.io.DataInputStream; +import java.io.DataOutputStream; +import java.io.IOException; +import java.net.Socket; + +public class SocketWearableConnection extends WearableConnection { + private final int MAX_PIECE_SIZE = 20 * 1024 * 1024; + private final Socket socket; + private final DataInputStream is; + private final DataOutputStream os; + + public SocketWearableConnection(Socket socket, Listener listener) throws IOException { + super(listener); + this.socket = socket; + this.is = new DataInputStream(socket.getInputStream()); + this.os = new DataOutputStream(socket.getOutputStream()); + } + + protected void writeMessagePiece(MessagePiece piece) throws IOException { + byte[] bytes = MessagePiece.ADAPTER.encode(piece); + os.writeInt(bytes.length); + os.write(bytes); + } + + protected MessagePiece readMessagePiece() throws IOException { + int len = is.readInt(); + if (len > MAX_PIECE_SIZE) { + throw new IOException("Piece size " + len + " exceeded limit of " + MAX_PIECE_SIZE + " bytes."); + } + System.out.println("Reading piece of length " + len); + byte[] bytes = new byte[len]; + is.readFully(bytes); + return MessagePiece.ADAPTER.decode(bytes); + } + + @Override + public void close() throws IOException { + socket.close(); + } +} diff --git a/play-services-wearable/core/src/main/java/org/microg/gms/wearable/WearableConnection.java b/play-services-wearable/core/src/main/java/org/microg/gms/wearable/WearableConnection.java new file mode 100644 index 0000000000..4d264deef0 --- /dev/null +++ b/play-services-wearable/core/src/main/java/org/microg/gms/wearable/WearableConnection.java @@ -0,0 +1,133 @@ +/* + * SPDX-FileCopyrightText: 2015, microG Project Team + * SPDX-License-Identifier: Apache-2.0 + */ + +package org.microg.gms.wearable; + +import org.microg.gms.wearable.proto.MessagePiece; +import org.microg.gms.wearable.proto.RootMessage; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; + +import okio.ByteString; + +public abstract class WearableConnection implements Runnable { + private static String B64ALPHABET = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_"; + + private HashMap> piecesQueues = new HashMap>(); + private final Listener listener; + + public WearableConnection(Listener listener) { + this.listener = listener; + } + + public static String base64encode(byte[] bytes) { + int paddingCount = (3 - (bytes.length % 3)) % 3; + byte[] padded = new byte[bytes.length + paddingCount]; + System.arraycopy(bytes, 0, padded, 0, bytes.length); + StringBuilder sb = new StringBuilder(); + for (int i = 0; i < bytes.length; i += 3) { + int j = ((padded[i] & 0xff) << 16) + ((padded[i + 1] & 0xff) << 8) + (padded[i + 2] & 0xff); + sb.append(B64ALPHABET.charAt((j >> 18) & 0x3f)).append(B64ALPHABET.charAt((j >> 12) & 0x3f)) + .append(B64ALPHABET.charAt((j >> 6) & 0x3f)).append(B64ALPHABET.charAt(j & 0x3f)); + } + return sb.substring(0, sb.length() - paddingCount); + } + + public static String calculateDigest(byte[] bytes) { + try { + return base64encode(MessageDigest.getInstance("SHA1").digest(bytes)); + } catch (NoSuchAlgorithmException e) { + throw new RuntimeException("SHA1 not supported => platform not supported"); + } + } + + public void writeMessage(RootMessage message) throws IOException { + byte[] bytes = RootMessage.ADAPTER.encode(message); + // TODO: cut in pieces + writeMessagePiece(new MessagePiece.Builder() + .data(ByteString.of(bytes)) + .digest(calculateDigest(bytes)) + .thisPiece(1) + .totalPieces(1).build()); + } + + protected abstract void writeMessagePiece(MessagePiece piece) throws IOException; + + protected RootMessage readMessage() throws IOException { + while (true) { + System.out.println("Waiting for new message..."); + MessagePiece piece = readMessagePiece(); + if (piece.totalPieces == 1) { + return RootMessage.ADAPTER.decode(piece.data); + } else { + if (piece.thisPiece == 1) { + List queue = piecesQueues.get(piece.queueId); + String oldDigest = null; + if (queue != null) { + oldDigest = queue.get(0).digest; + } + queue = new ArrayList(piece.totalPieces); + queue.add(piece); + piecesQueues.put(piece.queueId, queue); + if (oldDigest != null) { + throw new IOException("Could not finish message of digest " + oldDigest + ", queue is used for newer messagee"); + } + } else { + List queue = piecesQueues.get(piece.queueId); + if (queue == null || !queue.get(0).digest.equals(piece.digest)) { + throw new IOException("Received " + piece.thisPiece + " before first piece."); + } + if (queue.size() + 1 != piece.thisPiece) { + throw new IOException("Received " + piece.thisPiece + " but expected piece" + queue.size() + 1); + } + queue.add(piece); + if (piece.thisPiece == piece.totalPieces) { + piecesQueues.remove(piece.queueId); + ByteArrayOutputStream bos = new ByteArrayOutputStream(); + for (MessagePiece messagePiece : queue) { + messagePiece.data.write(bos); + } + byte[] bytes = bos.toByteArray(); + if (!calculateDigest(bytes).equals(piece.digest)) { + throw new IOException("Merged pieces have digest " + calculateDigest(bytes) + ", but should be " + piece.digest); + } + return RootMessage.ADAPTER.decode(bytes); + } + } + } + } + } + + protected abstract MessagePiece readMessagePiece() throws IOException; + + public abstract void close() throws IOException; + + @Override + public void run() { + try { + listener.onConnected(this); + RootMessage message; + while ((message = readMessage()) != null) { + listener.onMessage(this, message); + } + } catch (IOException e) { + // quit + } + System.out.println("WearableConnection closed"); + listener.onDisconnected(); + } + + public interface Listener { + void onConnected(WearableConnection connection); + void onMessage(WearableConnection connection, RootMessage message); + void onDisconnected(); + } +} diff --git a/play-services-wearable/core/src/main/java/org/microg/gms/wearable/WearableImpl.java b/play-services-wearable/core/src/main/java/org/microg/gms/wearable/WearableImpl.java index 56b8489e0f..9c8ba3f6b5 100644 --- a/play-services-wearable/core/src/main/java/org/microg/gms/wearable/WearableImpl.java +++ b/play-services-wearable/core/src/main/java/org/microg/gms/wearable/WearableImpl.java @@ -24,6 +24,7 @@ import android.content.IntentFilter; import android.database.Cursor; import android.net.Uri; +import android.os.Build; import android.os.Handler; import android.os.Looper; import android.os.RemoteException; @@ -53,18 +54,16 @@ import org.microg.gms.wearable.channel.ChannelCallbacks; import org.microg.gms.wearable.channel.ChannelManager; import org.microg.gms.wearable.channel.ChannelToken; -import org.microg.wearable.SocketConnectionThread; -import org.microg.wearable.WearableConnection; -import org.microg.wearable.proto.AckAsset; -import org.microg.wearable.proto.AppKey; -import org.microg.wearable.proto.AppKeys; -import org.microg.wearable.proto.Connect; -import org.microg.wearable.proto.FetchAsset; -import org.microg.wearable.proto.FilePiece; -import org.microg.wearable.proto.Request; -import org.microg.wearable.proto.RootMessage; -import org.microg.wearable.proto.SetAsset; -import org.microg.wearable.proto.SetDataItem; +import org.microg.gms.wearable.proto.AckAsset; +import org.microg.gms.wearable.proto.AppKey; +import org.microg.gms.wearable.proto.AppKeys; +import org.microg.gms.wearable.proto.Connect; +import org.microg.gms.wearable.proto.FetchAsset; +import org.microg.gms.wearable.proto.FilePiece; +import org.microg.gms.wearable.proto.Request; +import org.microg.gms.wearable.proto.RootMessage; +import org.microg.gms.wearable.proto.SetAsset; +import org.microg.gms.wearable.proto.SetDataItem; import java.io.File; import java.io.FileInputStream; @@ -83,6 +82,7 @@ import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentMap; import java.util.concurrent.CountDownLatch; +import java.util.concurrent.atomic.AtomicBoolean; import okio.ByteString; @@ -724,6 +724,19 @@ public void updateConfiguration(ConnectionConfiguration config) { @RequiresPermission(Manifest.permission.BLUETOOTH_CONNECT) public void createConnection(ConnectionConfiguration config) { if (config.nodeId == null) config.nodeId = getLocalNodeId(); + + ConnectionConfiguration existing = getConfigurationByAddress(config.address); + if (existing != null) { + Log.d(TAG, "Config already exists for address " + config.address + ", updating"); + if (config.name != null && !config.name.isEmpty() && !"null".equals(config.name)) { + existing.name = config.name; + } + existing.enabled = config.enabled; + configDatabase.putConfiguration(existing); + configurationsUpdated = true; + return; + } + Log.d(TAG, "putConfig[nyp]: " + config); configDatabase.putConfiguration(config); configurationsUpdated = true; @@ -736,9 +749,45 @@ public void createConnection(ConnectionConfiguration config) { } } + public static class BluetoothConnectionLock { + private static final String TAG = "BtConnLock"; + private static final Map locks = new ConcurrentHashMap<>(); + + public static synchronized boolean tryAcquire(String address, String owner) { + AtomicBoolean lock = locks.get(address); + if (lock == null) { + lock = new AtomicBoolean(false); + locks.put(address, lock); + } + + boolean acquired = lock.compareAndSet(false, true); + if (acquired) { + Log.d(TAG, owner + " acquired lock for " + address); + } else { + Log.d(TAG, owner + " failed to acquire lock for " + address + " (already held)"); + } + return acquired; + } + + public static synchronized void release(String address, String owner) { + AtomicBoolean lock = locks.get(address); + if (lock != null) { + lock.set(false); + Log.d(TAG, owner + " released lock for " + address); + } + } + + public static boolean isHeld(String address) { + AtomicBoolean lock = locks.get(address); + return lock != null && lock.get(); + } + + } + @RequiresPermission(Manifest.permission.BLUETOOTH_CONNECT) private void handleBle(ConnectionConfiguration config, boolean enabled) { + Log.d(TAG, "BLE not implemented"); if (config.role == ROLE_CLIENT) { if (enabled) { try { @@ -771,6 +820,7 @@ private void handleBle(ConnectionConfiguration config, boolean enabled) { } private void handleNetwork(ConnectionConfiguration config, boolean enabled) { + Log.d(TAG, "Network not implemented"); if (enabled) { // initialize new network service } else { @@ -872,12 +922,14 @@ private void closeConnection(String nodeId) { sct = null; } activeConnections.remove(nodeId); + String name = "Wear device"; for (ConnectionConfiguration config : getConfigurations()) { if (nodeId.equals(config.nodeId) || nodeId.equals(config.peerNodeId)) { config.connected = false; + name = config.name; } } - onPeerDisconnected(new NodeParcelable(nodeId, "Wear device")); + onPeerDisconnected(new NodeParcelable(nodeId, name)); Log.d(TAG, "Closed connection to " + nodeId + " on error"); } diff --git a/play-services-wearable/core/src/main/java/org/microg/gms/wearable/WearableServiceImpl.java b/play-services-wearable/core/src/main/java/org/microg/gms/wearable/WearableServiceImpl.java index aac11f1f53..9f9beacb97 100644 --- a/play-services-wearable/core/src/main/java/org/microg/gms/wearable/WearableServiceImpl.java +++ b/play-services-wearable/core/src/main/java/org/microg/gms/wearable/WearableServiceImpl.java @@ -42,7 +42,7 @@ import org.microg.gms.wearable.channel.ChannelToken; import org.microg.gms.wearable.channel.InvalidChannelTokenException; import org.microg.gms.wearable.channel.OpenChannelCallback; -import org.microg.wearable.proto.AppKey; +import org.microg.gms.wearable.proto.AppKey; import java.io.FileNotFoundException; import java.io.IOException; @@ -509,9 +509,10 @@ public void clearLogs(IWearableCallbacks callbacks) throws RemoteException { @Override public void getLocalNode(IWearableCallbacks callbacks) throws RemoteException { + ConnectionConfiguration config = wearable.getConfigurationByNodeId(wearable.getLocalNodeId()); postMain(callbacks, () -> { try { - callbacks.onGetLocalNodeResponse(new GetLocalNodeResponse(0, new NodeParcelable(wearable.getLocalNodeId(), wearable.getLocalNodeId()))); + callbacks.onGetLocalNodeResponse(new GetLocalNodeResponse(0, new NodeParcelable(config.nodeId, config.name))); } catch (Exception e) { callbacks.onGetLocalNodeResponse(new GetLocalNodeResponse(8, null)); } @@ -563,7 +564,8 @@ public void getConnectedCapability(IWearableCallbacks callbacks, String capabili for (String nodeId : nodeIds) { if (shouldIncludeNode(nodeId, nodeFilter)) { - nodes.add(new NodeParcelable(nodeId, nodeId)); + String dispName = wearable.getConfigurationByNodeId(nodeId).name; + nodes.add(new NodeParcelable(nodeId, dispName)); } } @@ -607,7 +609,8 @@ public void getAllCapabilities(IWearableCallbacks callbacks, int nodeFilter) thr for (String nodeId: nodeIds) { if (shouldIncludeNode(nodeId, nodeFilter)){ - nodes.add(new NodeParcelable(nodeId, nodeId)); + String dispName = wearable.getConfigurationByNodeId(nodeId).name; + nodes.add(new NodeParcelable(nodeId, dispName)); } } diff --git a/play-services-wearable/core/src/main/java/org/microg/gms/wearable/bluetooth/BluetoothClient.java b/play-services-wearable/core/src/main/java/org/microg/gms/wearable/bluetooth/BluetoothClient.java index d3f7fc58ed..bb522f2c9e 100644 --- a/play-services-wearable/core/src/main/java/org/microg/gms/wearable/bluetooth/BluetoothClient.java +++ b/play-services-wearable/core/src/main/java/org/microg/gms/wearable/bluetooth/BluetoothClient.java @@ -39,9 +39,23 @@ public BluetoothClient(Context context, WearableImpl wearableImpl) { this.btStateReceiver = new BroadcastReceiver() { @Override public void onReceive(Context context, Intent intent) { - if (BluetoothAdapter.ACTION_CONNECTION_STATE_CHANGED.equals(intent.getAction())) { - int state = intent.getIntExtra(BluetoothAdapter.EXTRA_STATE, BluetoothAdapter.STATE_OFF); - onBtAdapterStateChaged(state); + String action = intent.getAction(); + + if (BluetoothAdapter.ACTION_STATE_CHANGED.equals(action)) { + int state = intent.getIntExtra(BluetoothAdapter.EXTRA_STATE, BluetoothAdapter.ERROR); + onAdapterStateChanged(state); + + } else if (BluetoothDevice.ACTION_ACL_CONNECTED.equals(action)) { + BluetoothDevice device = intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE); + if (device != null) { + onAclConnected(device); + } + + } else if (BluetoothDevice.ACTION_ACL_DISCONNECTED.equals(action)) { + BluetoothDevice device = intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE); + if (device != null) { + onAclDisconnected(device.getAddress()); + } } } }; @@ -56,18 +70,44 @@ public void onReceive(Context context, Intent intent) { } }; - context.registerReceiver(btStateReceiver, new IntentFilter(BluetoothAdapter.ACTION_CONNECTION_STATE_CHANGED)); + context.registerReceiver(btStateReceiver, new IntentFilter(BluetoothAdapter.ACTION_STATE_CHANGED)); context.registerReceiver(aclConnReceiver, new IntentFilter(BluetoothDevice.ACTION_ACL_CONNECTED)); } + private void onAdapterStateChanged(int state) { + Log.d(TAG, "Bluetooth adapter state changed to " + state); + + if (state == BluetoothAdapter.STATE_ON) { + for (BluetoothConnectionThread thread : connections.values()) { + thread.resetBackoff(); + thread.scheduleRetry(); + } + } else if (state == BluetoothAdapter.STATE_OFF) { + if (btAdapter != null && btAdapter.isEnabled()) { + Log.d(TAG, "Ignoring STATE_OFF - adapter still enabled (stale broadcast)"); + return; + } + for (BluetoothConnectionThread thread : connections.values()) { + thread.close(); + } + } + } + + public void addConfig(ConnectionConfiguration config) { validateConfig(config); + if (!btAdapter.isEnabled()) { + Log.w(TAG, "Bluetooth not enabled, skipping connection"); + return; + } + + String address = config.address; if (configurations.containsKey(address)) { Log.d(TAG, "Configuration already exists for " + address + ", reconnecting"); BluetoothConnectionThread thread = connections.get(address); - if (thread != null && btAdapter != null && btAdapter.isEnabled()) { + if (thread != null && btAdapter.isEnabled()) { thread.retryConnection(); } return; @@ -75,7 +115,7 @@ public void addConfig(ConnectionConfiguration config) { configurations.put(address, config); - if (btAdapter == null || !btAdapter.isEnabled()) { + if (!btAdapter.isEnabled()) { Log.w(TAG, "Bluetooth adapter not available or disabled, deferring connection"); return; } @@ -100,6 +140,7 @@ public void removeConfig(ConnectionConfiguration config) { private void startConnection(ConnectionConfiguration config) { String address = config.address; + if (connections.containsKey(address)) { Log.d(TAG, "Connection already active for " + address); return; @@ -120,6 +161,10 @@ private void onAclConnected(BluetoothDevice device) { } } + private void onAclDisconnected(String address) { + Log.d(TAG, "ACL_DISCONNECTED for " + address); + } + private void onBtAdapterStateChaged(int state) { Log.d(TAG, "Bluetooth adapter state changed to " + state); @@ -130,6 +175,10 @@ private void onBtAdapterStateChaged(int state) { startConnection(config); } } else if (state == BluetoothAdapter.STATE_OFF) { + if (btAdapter != null && btAdapter.isEnabled()) { + Log.d(TAG, "Ignoring STATE_OFF broadcast - adapter is still enabled"); + return; + } for (BluetoothConnectionThread thread : connections.values()) { thread.close(); } diff --git a/play-services-wearable/core/src/main/java/org/microg/gms/wearable/bluetooth/BluetoothConnectionThread.java b/play-services-wearable/core/src/main/java/org/microg/gms/wearable/bluetooth/BluetoothConnectionThread.java index 3016154bb7..d5d569a803 100644 --- a/play-services-wearable/core/src/main/java/org/microg/gms/wearable/bluetooth/BluetoothConnectionThread.java +++ b/play-services-wearable/core/src/main/java/org/microg/gms/wearable/bluetooth/BluetoothConnectionThread.java @@ -14,10 +14,10 @@ import com.google.android.gms.wearable.ConnectionConfiguration; +import org.microg.gms.wearable.WearableConnection; import org.microg.gms.wearable.WearableImpl; -import org.microg.wearable.SocketWearableConnection; -import org.microg.wearable.WearableConnection; -import org.microg.wearable.proto.Connect; +import org.microg.gms.wearable.proto.Connect; +import org.microg.gms.wearable.proto.RootMessage; import java.io.Closeable; import java.io.IOException; @@ -33,6 +33,8 @@ public class BluetoothConnectionThread extends Thread implements Closeable { private static final int MAX_RETRY_DELAY_MS = 60000; private static final int MIN_RETRY_DELAY_MS = 1000; private static final int BACKOFF_MULTIPLIEER = 2; + private static final int MAX_CONSECUTIVE_FAILURES = 5; + private static final long MIN_ATTEMPT_INTERVAL_MS = 3000; private final Context context; private final ConnectionConfiguration config; @@ -41,6 +43,9 @@ public class BluetoothConnectionThread extends Thread implements Closeable { private final AtomicBoolean running = new AtomicBoolean(true); private final AtomicInteger retryCount = new AtomicInteger(0); + private final AtomicBoolean immediateRetry = new AtomicBoolean(false); + + private long lastAttemptTime = 0; private BluetoothSocket socket; private WearableConnection wearableConnection; @@ -62,6 +67,10 @@ public void run(){ Log.d(TAG, "Bluetooth connection thread started for " + config.address); while (running.get() && !isInterrupted()) { + enforceMinInterval(); + + if (!running.get()) break; + try { connect(); } catch (IOException e) { @@ -85,39 +94,98 @@ public void run(){ Log.d(TAG, "Bluetooth connection thread stopped for " + config.address); } + private void enforceMinInterval() { + long now = System.currentTimeMillis(); + long elapsed = now - lastAttemptTime; + + if (elapsed < MIN_ATTEMPT_INTERVAL_MS && lastAttemptTime > 0) { + long sleepTime = MIN_ATTEMPT_INTERVAL_MS - elapsed; + Log.d(TAG, "Enforcing min interval, sleeping " + sleepTime + "ms"); + try { + Thread.sleep(sleepTime); + } catch (InterruptedException e) { + if (!running.get()) return; + } + } + + lastAttemptTime = System.currentTimeMillis(); + } + @RequiresPermission(allOf = {Manifest.permission.BLUETOOTH_CONNECT, Manifest.permission.BLUETOOTH_SCAN}) private void connect() throws IOException, InterruptedException { - if (!running.get() || btAdapter == null || !btAdapter.isEnabled()) { - throw new IOException("Bluetooth not available"); + if (!WearableImpl.BluetoothConnectionLock.tryAcquire(config.address, "BTLock")) { + throw new IOException("Connection lock held by another app"); } - BluetoothDevice device = btAdapter.getRemoteDevice(config.address); - if (device == null) throw new IOException("Could not get remote device"); + try { - Log.d(TAG, "Connecting to " + config.address + " via " + getConnectionTypeName()); + if (!running.get() || btAdapter == null || !btAdapter.isEnabled()) { + throw new IOException("Bluetooth not available"); + } - if (config.type != WearableImpl.TYPE_BLUETOOTH_RFCOMM && config.type != 5 ) { - return; - } + BluetoothDevice device = btAdapter.getRemoteDevice(config.address); + if (device == null) throw new IOException("Could not get remote device"); - socket = device.createRfcommSocketToServiceRecord(WEAR_BT_UUID); + Log.d(TAG, "Connecting to " + config.address + " via " + getConnectionTypeName()); - if (btAdapter.isDiscovering()) btAdapter.cancelDiscovery(); + if (config.type != WearableImpl.TYPE_BLUETOOTH_RFCOMM && config.type != 5) { + return; + } - socket.connect(); - Log.d(TAG, "Socket connected to " + config.address); + socket = device.createRfcommSocketToServiceRecord(WEAR_BT_UUID); - retryCount.set(0); + if (btAdapter.isDiscovering()) btAdapter.cancelDiscovery(); - wearableConnection = new BluetoothWearableConnection(socket, config.nodeId, new ConnectionListener(context, config, wearableImpl)); - wearableConnection.run(); + socket.connect(); + Log.d(TAG, "Socket connected to " + config.address); + + retryCount.set(0); + + wearableConnection = new BluetoothWearableConnection(socket, config.nodeId, new ConnectionListener(context, config, wearableImpl)); + wearableConnection.run(); + } finally { + WearableImpl.BluetoothConnectionLock.release(config.address, "BTLock"); + } } private void waitForRetry() throws InterruptedException { - int count = retryCount.incrementAndGet(); + if (!running.get()) return; + + if (immediateRetry.getAndSet(false)) { + Log.d(TAG, "Immediate retry flag set, skipping delay"); + return; + } + + int count = retryCount.get(); int delay = calcRetryDelay(count); Log.d(TAG, "Waiting " + delay + "ms before retry #" + count + " for " + config.address); - Thread.sleep(delay); + + try { + Thread.sleep(delay); + } catch (InterruptedException e) { + if (!running.get()) { + Log.d(TAG, "Sleep interrupted for close"); + } else { + Log.d(TAG, "Sleep interrupted for retry"); + } + } + } + + private void waitForExternalRetry() { + Log.d(TAG, "Waiting for external retry trigger for " + config.address); + + try { + while (running.get() && !immediateRetry.get()) { + Thread.sleep(5000); + } + immediateRetry.set(false); + retryCount.set(0); + } catch (InterruptedException e) { + if (running.get()) { + Log.d(TAG, "External retry triggered for " + config.address); + retryCount.set(0); + } + } } private int calcRetryDelay(int retryCount) { @@ -127,14 +195,25 @@ private int calcRetryDelay(int retryCount) { public void retryConnection(){ Log.d(TAG, "Immediate retry requested for " + config.address); + retryCount.set(0); + immediateRetry.set(true); interrupt(); } + public void resetBackoff() { + Log.d(TAG, "Resetting backoff for " + config.address); + retryCount.set(0); + lastAttemptTime = 0; + } + public void scheduleRetry() { - retryHandler.post(() -> { - Log.d(TAG, "Scheduled retry triggered for " + config.address); - interrupt(); - }); + retryCount.set(0); + immediateRetry.set(true); + interrupt(); +// retryHandler.post(() -> { +// Log.d(TAG, "Scheduled retry triggered for " + config.address); +// interrupt(); +// }); } private void closeSocket() { @@ -151,7 +230,7 @@ private void closeSocket() { private String getConnectionTypeName() { switch (config.type) { - case 5: return "RFCOMM maybe, idk"; + case 5: return "RFCOMM (type 5)"; case WearableImpl.TYPE_BLUETOOTH_RFCOMM: return "RFCOMM"; default: return "Unknown"; } @@ -161,8 +240,8 @@ private String getConnectionTypeName() { public void close(){ Log.d(TAG, "Closing Bluetooth connection for " + config.address); running.set(false); - interrupt(); closeSocket(); + interrupt(); } private static class ConnectionListener implements WearableConnection.Listener { @@ -187,13 +266,12 @@ public void onConnected(WearableConnection connection) { BluetoothWearableConnection btConnection = (BluetoothWearableConnection) connection; this.peerConnect = btConnection.getPeerConnect(); - wearableImpl.onConnectReceived(connection, config.nodeId, peerConnect); } @Override - public void onMessage(WearableConnection connection, org.microg.wearable.proto.RootMessage message) { - Log.d(TAG, "Message received from " + config.address); + public void onMessage(WearableConnection connection, RootMessage message) { + Log.d(TAG, "Message received from " + config.address + ": " + message.toString()); } diff --git a/play-services-wearable/core/src/main/java/org/microg/gms/wearable/bluetooth/BluetoothServer.java b/play-services-wearable/core/src/main/java/org/microg/gms/wearable/bluetooth/BluetoothServer.java index 0bc7b7a20f..c6b9e342cf 100644 --- a/play-services-wearable/core/src/main/java/org/microg/gms/wearable/bluetooth/BluetoothServer.java +++ b/play-services-wearable/core/src/main/java/org/microg/gms/wearable/bluetooth/BluetoothServer.java @@ -21,7 +21,8 @@ import com.google.android.gms.wearable.ConnectionConfiguration; import org.microg.gms.wearable.WearableImpl; -import org.microg.wearable.WearableConnection; +import org.microg.gms.wearable.WearableConnection; +import org.microg.gms.wearable.proto.RootMessage; import java.io.Closeable; import java.io.IOException; @@ -324,7 +325,7 @@ public void onConnected(WearableConnection connection) { } @Override - public void onMessage(WearableConnection connection, org.microg.wearable.proto.RootMessage message) { + public void onMessage(WearableConnection connection, RootMessage message) { Log.d(TAG, "Server received message from " + socket.getRemoteDevice().getAddress()); // TODO: Handle incoming messages } diff --git a/play-services-wearable/core/src/main/java/org/microg/gms/wearable/bluetooth/BluetoothWearableConnection.java b/play-services-wearable/core/src/main/java/org/microg/gms/wearable/bluetooth/BluetoothWearableConnection.java index c899ebd5a5..aaf0f6eca2 100644 --- a/play-services-wearable/core/src/main/java/org/microg/gms/wearable/bluetooth/BluetoothWearableConnection.java +++ b/play-services-wearable/core/src/main/java/org/microg/gms/wearable/bluetooth/BluetoothWearableConnection.java @@ -9,10 +9,10 @@ import android.util.Log; import org.microg.gms.profile.Build; -import org.microg.wearable.WearableConnection; -import org.microg.wearable.proto.Connect; -import org.microg.wearable.proto.MessagePiece; -import org.microg.wearable.proto.RootMessage; +import org.microg.gms.wearable.WearableConnection; +import org.microg.gms.wearable.proto.Connect; +import org.microg.gms.wearable.proto.MessagePiece; +import org.microg.gms.wearable.proto.RootMessage; import java.io.DataInputStream; import java.io.DataOutputStream; @@ -82,7 +82,6 @@ private boolean handshake() { Log.d(TAG, "Connect message details: " + incomingMessage.connect); handshakeComplete = true; - listener.onConnected(this); return true; } catch (IOException e) { Log.e(TAG, "Handshake failed", e); diff --git a/play-services-wearable/core/src/main/java/org/microg/gms/wearable/channel/ChannelManager.java b/play-services-wearable/core/src/main/java/org/microg/gms/wearable/channel/ChannelManager.java index 3269605879..7d5b7fe532 100644 --- a/play-services-wearable/core/src/main/java/org/microg/gms/wearable/channel/ChannelManager.java +++ b/play-services-wearable/core/src/main/java/org/microg/gms/wearable/channel/ChannelManager.java @@ -11,16 +11,16 @@ import com.google.android.gms.wearable.internal.GetChannelOutputStreamResponse; import com.google.android.gms.wearable.internal.IWearableCallbacks; +import org.microg.gms.wearable.WearableConnection; import org.microg.gms.wearable.WearableImpl; -import org.microg.wearable.WearableConnection; -import org.microg.wearable.proto.AppKey; -import org.microg.wearable.proto.ChannelControlRequest; -import org.microg.wearable.proto.ChannelDataAckRequest; -import org.microg.wearable.proto.ChannelDataHeader; -import org.microg.wearable.proto.ChannelDataRequest; -import org.microg.wearable.proto.ChannelRequest; -import org.microg.wearable.proto.Request; -import org.microg.wearable.proto.RootMessage; +import org.microg.gms.wearable.proto.AppKey; +import org.microg.gms.wearable.proto.ChannelControlRequest; +import org.microg.gms.wearable.proto.ChannelDataAckRequest; +import org.microg.gms.wearable.proto.ChannelDataHeader; +import org.microg.gms.wearable.proto.ChannelDataRequest; +import org.microg.gms.wearable.proto.ChannelRequest; +import org.microg.gms.wearable.proto.Request; +import org.microg.gms.wearable.proto.RootMessage; import java.io.IOException; import java.util.Map; diff --git a/play-services-wearable/core/src/main/java/org/microg/gms/wearable/channel/ChannelToken.java b/play-services-wearable/core/src/main/java/org/microg/gms/wearable/channel/ChannelToken.java index 9f531776f7..98d24f0c02 100644 --- a/play-services-wearable/core/src/main/java/org/microg/gms/wearable/channel/ChannelToken.java +++ b/play-services-wearable/core/src/main/java/org/microg/gms/wearable/channel/ChannelToken.java @@ -4,7 +4,7 @@ import com.google.android.gms.wearable.internal.ChannelParcelable; -import org.microg.wearable.proto.AppKey; +import org.microg.gms.wearable.proto.AppKey; public final class ChannelToken { private static final String TAG = "ChannelToken"; diff --git a/play-services-wearable/core/src/main/proto/wearable.proto b/play-services-wearable/core/src/main/proto/wearable.proto new file mode 100644 index 0000000000..ff04c8def2 --- /dev/null +++ b/play-services-wearable/core/src/main/proto/wearable.proto @@ -0,0 +1,161 @@ +/* + * SPDX-FileCopyrightText: 2015, microG Project Team + * SPDX-License-Identifier: Apache-2.0 + */ + +option java_package = "org.microg.gms.wearable.proto"; +option java_outer_classname = "WearableProto"; + +message AckAsset { + optional string digest = 1; +} + +message AppKey { + optional string packageName = 1; + optional string signatureDigest = 2; +} + +message AppKeys { + repeated AppKey appKeys = 1; +} + +message Asset { + // TODO + optional string digest = 4; +} + +message AssetEntry { + optional string key = 1; + optional Asset value = 2; + optional int32 unknown3 = 3; +} + +message ChannelControlRequest { + optional int32 type = 1; + optional int64 channelId = 2; + optional bool fromChannelOperator = 3; + optional string packageName = 4; + optional string signatureDigest = 5; + optional string path = 6; + optional int32 closeErrorCode = 7; +} + +message ChannelDataAckRequest { + optional ChannelDataHeader header = 1; + optional bool finalMessage = 2; +} + +message ChannelDataHeader { + optional int64 channelId = 1; + optional bool fromChannelOperator = 2; + optional int64 requestId = 3; +} + +message ChannelDataRequest { + optional ChannelDataHeader header = 1; + optional bytes payload = 2; + optional bool finalMessage = 3; +} + +message ChannelRequest { + optional ChannelControlRequest channelControlRequest = 2; + optional ChannelDataRequest channelDataRequest = 3; + optional ChannelDataAckRequest channelDataAckRequest = 4; + optional int32 version = 6; + optional int32 origin = 7; +} + +message Connect { + optional string id = 1; + optional string name = 2; + optional int64 peerAndroidId = 3; + optional int32 unknown4 = 4; + optional int32 peerVersion = 5; + optional int32 peerMinimumVersion = 6; + optional string networkId = 7; +} + +message FetchAsset { + optional string packageName = 1; + optional string assetName = 2; + optional bool permission = 3; + optional string signatureDigest = 4; +} + +message FilePiece { + optional string fileName = 1; + optional bool finalPiece = 2; + optional bytes piece = 3; + optional string digest = 4; +} + +message Heartbeat { + +} + +message MessagePiece { + optional bytes data = 1; + optional string digest = 2; + optional int32 thisPiece = 3; + optional int32 totalPieces = 4; + optional int32 queueId = 5; +} + +message Request { + optional int32 requestId = 1; + optional string packageName = 2; + optional string signatureDigest = 3; + optional string targetNodeId = 4; + optional int32 unknown5 = 5; + optional string path = 6; + optional bytes rawData = 7; + optional string sourceNodeId = 8; + optional ChannelRequest request = 9; + optional int32 generation = 10; +} + +message RootMessage { + optional SetAsset setAsset = 4; + optional AckAsset ackAsset = 5; + optional FetchAsset fetchAsset = 6; + optional Connect connect = 7; + optional SyncStart syncStart = 8; + optional SetDataItem setDataItem = 9; + optional Request rpcRequest = 10; + optional Heartbeat heartbeat = 11; + optional FilePiece filePiece = 12; + optional bool hasAsset = 13; + optional Request channelRequest = 16; +} + +message SetAsset { + optional string digest = 1; + optional bytes data = 2; + optional AppKeys appkeys = 3; +} + +message SetDataItem { + optional string packageName = 1; + optional string uri = 2; + repeated string unknown3 = 3; + optional bytes data = 4; + optional int64 seqId = 5; + optional bool deleted = 6; + optional string source = 7; + repeated AssetEntry assets = 8; + optional string signatureDigest = 9; + optional int64 lastModified = 10; +} + +message SyncStart { + optional int64 receivedSeqId = 1; + repeated SyncTableEntry syncTable = 2; + optional int32 version = 3; +} + +message SyncTableEntry { + optional string key = 1; + optional int64 value = 2; +} + + diff --git a/play-services-wearable/src/main/java/com/google/android/gms/wearable/internal/NodeParcelable.java b/play-services-wearable/src/main/java/com/google/android/gms/wearable/internal/NodeParcelable.java index 1c8af18b66..c9cad07f8a 100644 --- a/play-services-wearable/src/main/java/com/google/android/gms/wearable/internal/NodeParcelable.java +++ b/play-services-wearable/src/main/java/com/google/android/gms/wearable/internal/NodeParcelable.java @@ -49,8 +49,13 @@ public NodeParcelable(String nodeId, String displayName, int hops, boolean isNea this.isNearby = isNearby; } +// public NodeParcelable(String nodeId, String displayName) { +// this(nodeId, displayName, 0, false); +// } + + public NodeParcelable(String nodeId, String displayName) { - this(nodeId, displayName, 0, false); + this(nodeId, displayName, 0, true); } public NodeParcelable(Node node) { @@ -92,7 +97,7 @@ public int hashCode() { @Override public String toString() { - return "NodeParcelable{" + displayName + ", id=" + displayName + ", hops=" + hops + ", isNearby=" + isNearby + "}"; + return "NodeParcelable{" + displayName + ", id=" + nodeId + ", hops=" + hops + ", isNearby=" + isNearby + "}"; } public static final Creator CREATOR = new AutoCreator(NodeParcelable.class); From 4e22cf93cdbbd22f31348df1f0e24a410dc4f404 Mon Sep 17 00:00:00 2001 From: deadYokai Date: Fri, 9 Jan 2026 18:25:47 +0200 Subject: [PATCH 10/29] updated NodeDatabaseHelper some Bluetooth changes moved some functions from WearableImpl to MessageHandler --- .../microg/gms/wearable/MessageHandler.java | 307 +++++++++++++++++- .../gms/wearable/NodeDatabaseHelper.java | 239 ++++++++++++-- .../org/microg/gms/wearable/WearableImpl.java | 184 +++++++---- .../microg/gms/wearable/WearableService.java | 45 +++ .../bluetooth/BluetoothConnectionThread.java | 8 +- .../BluetoothWearableConnection.java | 22 +- .../gms/wearable/channel/ChannelManager.java | 1 + .../wearable/channel/ChannelStateMachine.java | 4 + .../google/android/gms/wearable/Asset.java | 29 +- .../internal/DataItemAssetParcelable.java | 3 +- .../wearable/internal/DataItemParcelable.java | 3 +- .../internal/MessageEventParcelable.java | 9 + .../gms/wearable/internal/NodeParcelable.java | 7 +- .../gms/wearable/internal/PutDataRequest.java | 93 +++++- 14 files changed, 820 insertions(+), 134 deletions(-) diff --git a/play-services-wearable/core/src/main/java/org/microg/gms/wearable/MessageHandler.java b/play-services-wearable/core/src/main/java/org/microg/gms/wearable/MessageHandler.java index 3953ee9af5..8222f68b76 100644 --- a/play-services-wearable/core/src/main/java/org/microg/gms/wearable/MessageHandler.java +++ b/play-services-wearable/core/src/main/java/org/microg/gms/wearable/MessageHandler.java @@ -16,7 +16,12 @@ package org.microg.gms.wearable; +import static org.microg.gms.wearable.WearableConnection.calculateDigest; + import android.content.Context; +import android.content.Intent; +import android.database.Cursor; +import android.net.Uri; import android.text.TextUtils; import android.util.Log; @@ -24,9 +29,12 @@ import com.google.android.gms.wearable.ConnectionConfiguration; import com.google.android.gms.wearable.internal.MessageEventParcelable; +import org.microg.gms.common.Utils; import org.microg.gms.profile.Build; import org.microg.gms.settings.SettingsContract; import org.microg.gms.wearable.proto.AckAsset; +import org.microg.gms.wearable.proto.AppKey; +import org.microg.gms.wearable.proto.AssetEntry; import org.microg.gms.wearable.proto.Connect; import org.microg.gms.wearable.proto.FetchAsset; import org.microg.gms.wearable.proto.FilePiece; @@ -38,8 +46,16 @@ import org.microg.gms.wearable.proto.SyncStart; import org.microg.gms.wearable.proto.SyncTableEntry; +import java.io.File; +import java.io.FileInputStream; +import java.io.FileOutputStream; import java.io.IOException; +import java.nio.file.Files; +import java.util.ArrayList; import java.util.Arrays; +import java.util.List; + +import okio.ByteString; public class MessageHandler extends ServerMessageListener { private static final String TAG = "GmsWearMsgHandler"; @@ -58,7 +74,7 @@ private MessageHandler(WearableImpl wearable, ConnectionConfiguration config, St .networkId(networkId) .peerAndroidId(androidId) .unknown4(3) - .peerVersion(1) + .peerVersion(2) .build()); this.wearable = wearable; this.oldConfigNodeId = config.nodeId; @@ -142,13 +158,14 @@ public void onSetDataItem(SetDataItem setDataItem) { public void onRpcRequest(Request rpcRequest) { Log.d(TAG, "onRpcRequest: " + rpcRequest); if (TextUtils.isEmpty(rpcRequest.targetNodeId) || rpcRequest.targetNodeId.equals(wearable.getLocalNodeId())) { - MessageEventParcelable messageEvent = new MessageEventParcelable(); - messageEvent.data = rpcRequest.rawData != null ? rpcRequest.rawData.toByteArray() : null; - messageEvent.path = rpcRequest.path; - messageEvent.requestId = rpcRequest.requestId + 31 * (rpcRequest.generation + 527); - messageEvent.sourceNodeId = TextUtils.isEmpty(rpcRequest.sourceNodeId) ? peerNodeId : rpcRequest.sourceNodeId; + int requestId = rpcRequest.requestId + 31 * (rpcRequest.generation + 527); + String path = rpcRequest.path; + byte[] data = rpcRequest.rawData != null ? rpcRequest.rawData.toByteArray() : null; + String sourceNodeId = TextUtils.isEmpty(rpcRequest.sourceNodeId) ? peerNodeId : rpcRequest.sourceNodeId; + + MessageEventParcelable messageEvent = new MessageEventParcelable(requestId, path, data, sourceNodeId); - wearable.sendMessageReceived(rpcRequest.packageName, messageEvent); + sendMessageReceived(rpcRequest.packageName, messageEvent); } else if (rpcRequest.targetNodeId.equals(peerNodeId)) { // Drop it } else { @@ -169,11 +186,285 @@ public void onHeartbeat(Heartbeat heartbeat) { @Override public void onFilePiece(FilePiece filePiece) { Log.d(TAG, "onFilePiece: " + filePiece); - wearable.handleFilePiece(getConnection(), filePiece.fileName, filePiece.piece.toByteArray(), filePiece.finalPiece ? filePiece.digest : null); + handleFilePiece(getConnection(), filePiece.fileName, filePiece.piece.toByteArray(), filePiece.finalPiece ? filePiece.digest : null); } @Override public void onChannelRequest(Request channelRequest) { Log.d(TAG, "onChannelRequest:" + channelRequest); } + + public void handleMessage(WearableConnection connection, String sourceNodeId, RootMessage message) { + Log.d(TAG, "handleMessage from " + sourceNodeId); + + if (message.syncStart != null) { + handleSyncStart(connection, sourceNodeId, message.syncStart); + } + + if (message.channelRequest != null && wearable.getChannelManager() != null) { + wearable.getChannelManager().onChannelRequestReceived(connection, sourceNodeId, message.channelRequest); + } + + if (message.rpcRequest != null) { + handleRpcRequest(connection, sourceNodeId, message.rpcRequest); + } + + if (message.setDataItem != null) { + handleSetDataItem(connection, sourceNodeId, message.setDataItem); + } + + if (message.filePiece != null) { + FilePiece piece = message.filePiece; + handleFilePiece(connection, piece.fileName, + piece.piece != null ? piece.piece.toByteArray() : new byte[0], piece.finalPiece ? piece.digest : null); + } + + if (message.ackAsset != null) { + Log.d(TAG, "Asset acknowledged: " + message.ackAsset.digest); + } + + if (message.fetchAsset != null) { + handleFetchAsset(connection, sourceNodeId, message.fetchAsset); + } + + if (message.setAsset != null) { + handleSetAsset(connection, sourceNodeId, message.setAsset, message.hasAsset); + } + } + + private void handleSetAsset(WearableConnection connection, String sourceNodeId, + SetAsset setAsset, Boolean hasAsset) { + Log.d(TAG, "handleSetAsset: digest=" + setAsset.digest + ", hasAsset=" + hasAsset); + + if (setAsset.appkeys != null) { + for (AppKey appKey : setAsset.appkeys.appKeys) { + wearable.getNodeDatabase().allowAssetAccess(setAsset.digest, appKey.packageName, appKey.signatureDigest); + } + } + + if (hasAsset != null && !hasAsset) { + handleFetchAsset(connection, sourceNodeId, new FetchAsset.Builder() + .assetName(setAsset.digest) + .build()); + } + } + + + private void handleSyncStart(WearableConnection connection, String sourceNodeId, + org.microg.gms.wearable.proto.SyncStart syncStart) { + Log.d(TAG, "handleSyncStart from " + sourceNodeId + + ": receivedSeqId=" + syncStart.receivedSeqId + + ", version=" + syncStart.version); + + if (syncStart.syncTable != null) { + for (org.microg.gms.wearable.proto.SyncTableEntry entry : syncStart.syncTable) { + Log.d(TAG, " Watch sync state: key=" + entry.key + ", seqId=" + entry.value); + } + } + + try { + List syncTable = new ArrayList<>(); + syncTable.add(new org.microg.gms.wearable.proto.SyncTableEntry.Builder() + .key(wearable.getLocalNodeId()) + .value(wearable.getClockworkNodePreferences().getNextSeqId() - 1) + .build()); + + RootMessage response = new RootMessage.Builder() + .syncStart(new org.microg.gms.wearable.proto.SyncStart.Builder() + .receivedSeqId(syncStart.receivedSeqId) + .syncTable(syncTable) + .version(2) + .build()) + .build(); + + connection.writeMessage(response); + Log.d(TAG, "Sent SyncStart response"); + + if (syncStart.syncTable != null) { + for (org.microg.gms.wearable.proto.SyncTableEntry entry : syncStart.syncTable) { + String nodeId = entry.key; + long theirSeqId = entry.value; + wearable.syncToPeer(sourceNodeId, nodeId, theirSeqId); + } + } + + } catch (IOException e) { + Log.e(TAG, "Failed to respond to syncStart", e); + } + } + + private void handleRpcRequest(WearableConnection connection, String sourceNodeId, Request request) { + Log.d(TAG, "handleRpcRequest from " + sourceNodeId + ": path=" + request.path); + + if (request.rawData != null) { + MessageEventParcelable messageEvent = new MessageEventParcelable( + request.requestId, + request.path, + request.rawData.toByteArray(), + sourceNodeId + ); + sendMessageReceived(request.packageName, messageEvent); + } + } + + private void handleSetDataItem(WearableConnection connection, String sourceNodeId, + SetDataItem setDataItem) { + Log.d(TAG, "handleSetDataItem from " + sourceNodeId + ": " + setDataItem.uri); + + DataItemRecord record = DataItemRecord.fromSetDataItem(setDataItem); + record.source = sourceNodeId; + + List missingAssets = new ArrayList<>(); + if (setDataItem.assets != null) { + for (AssetEntry assetEntry : setDataItem.assets) { + if (assetEntry.value != null && assetEntry.value.digest != null) { + String digest = assetEntry.value.digest; + if (!wearable.assetFileExists(digest)) { + missingAssets.add(Asset.createFromRef(digest)); + } + } + } + } + + record.assetsAreReady = missingAssets.isEmpty(); + + wearable.putDataItem(record); + + if (!missingAssets.isEmpty()) { + fetchMissingAssets(connection, record, missingAssets); + } + } + + + + private void fetchMissingAssets(WearableConnection connection, DataItemRecord record, + List missingAssets) { + for (Asset asset : missingAssets) { + try { + String digest = asset.getDigest(); + Log.d(TAG, "Fetching missing asset: " + digest); + + FetchAsset fetchAsset = new FetchAsset.Builder() + .assetName(digest) + .packageName(record.packageName) + .signatureDigest(record.signatureDigest) + .permission(false) + .build(); + + connection.writeMessage(new RootMessage.Builder() + .fetchAsset(fetchAsset) + .build()); + + } catch (IOException e) { + Log.w(TAG, "Error fetching asset " + asset.getDigest(), e); + } + } + } + + + private void handleFetchAsset(WearableConnection connection, String sourceNodeId, + FetchAsset fetchAsset) { + Log.d(TAG, "handleFetchAsset: " + fetchAsset.assetName); + + File assetFile = wearable.createAssetFile(fetchAsset.assetName); + if (assetFile.exists()) { + try { + RootMessage announceMessage = new RootMessage.Builder() + .setAsset(new SetAsset.Builder() + .digest(fetchAsset.assetName) + .build()) + .hasAsset(true) + .build(); + connection.writeMessage(announceMessage); + + String fileName = calculateDigest(announceMessage.encode()); + FileInputStream fis = new FileInputStream(assetFile); + byte[] arr = new byte[12215]; + ByteString lastPiece = null; + int c; + while ((c = fis.read(arr)) > 0) { + if (lastPiece != null) { + connection.writeMessage(new RootMessage.Builder() + .filePiece(new FilePiece(fileName, false, lastPiece, null)) + .build()); + } + lastPiece = ByteString.of(arr, 0, c); + } + fis.close(); + connection.writeMessage(new RootMessage.Builder() + .filePiece(new FilePiece(fileName, true, lastPiece, fetchAsset.assetName)) + .build()); + } catch (IOException e) { + Log.e(TAG, "Failed to send asset", e); + } + } else { + Log.w(TAG, "Asset not found: " + fetchAsset.assetName); + } + } + + public void handleFilePiece(WearableConnection connection, String fileName, byte[] bytes, String finalPieceDigest) { + File file = wearable.createAssetReceiveTempFile(fileName); + try { + FileOutputStream fos = new FileOutputStream(file, true); + fos.write(bytes); + fos.close(); + } catch (IOException e) { + Log.w(TAG, e); + } + if (finalPieceDigest != null) { + // This is a final piece. If digest matches we're so happy! + try { + String digest = calculateDigest(Utils.readStreamToEnd(Files.newInputStream(file.toPath()))); + if (digest.equals(finalPieceDigest)) { + if (file.renameTo(wearable.createAssetFile(digest))) { + wearable.getNodeDatabase().markAssetAsPresent(digest); + connection.writeMessage(new RootMessage.Builder().ackAsset(new AckAsset(digest)).build()); + Cursor cursor = wearable.getNodeDatabase().getDataItemsWaitingForAsset(digest); + if (cursor != null) { + try { + while (cursor.moveToNext()) { + DataItemRecord record = DataItemRecord.fromCursor(cursor); + boolean allPresent = true; + for (Asset asset : record.dataItem.getAssets().values()) { + if (!wearable.assetFileExists(asset.getDigest())) { + allPresent = false; + break; + } + } + + if (allPresent && !record.assetsAreReady) { + Log.d(TAG, "All assets now ready for: " + record.dataItem.uri); + record.assetsAreReady = true; + wearable.getNodeDatabase().updateAssetsReady(record.dataItem.uri.toString(), true); + + Intent intent = new Intent("com.google.android.gms.wearable.DATA_CHANGED"); + intent.setPackage(record.packageName); + intent.setData(record.dataItem.uri); + wearable.invokeListeners(intent, listener -> listener.onDataChanged(record.toEventDataHolder())); + } + } + } finally { + cursor.close(); + } + } + + } else { + Log.w(TAG, "Could not rename to target file name. delete=" + file.delete()); + } + } else { + Log.w(TAG, "Received digest does not match. delete=" + file.delete()); + } + } catch (IOException e) { + Log.w(TAG, "Failed working with temp file. delete=" + file.delete(), e); + } + } + } + + public void sendMessageReceived(String packageName, MessageEventParcelable messageEvent) { + Log.d(TAG, "onMessageReceived: " + messageEvent); + Intent intent = new Intent("com.google.android.gms.wearable.MESSAGE_RECEIVED"); + intent.setPackage(packageName); + intent.setData(Uri.parse("wear://" + wearable.getLocalNodeId() + "/" + messageEvent.getPath())); + wearable.invokeListeners(intent, listener -> listener.onMessageReceived(messageEvent)); + } } diff --git a/play-services-wearable/core/src/main/java/org/microg/gms/wearable/NodeDatabaseHelper.java b/play-services-wearable/core/src/main/java/org/microg/gms/wearable/NodeDatabaseHelper.java index 8b86fee197..cee6b0f136 100644 --- a/play-services-wearable/core/src/main/java/org/microg/gms/wearable/NodeDatabaseHelper.java +++ b/play-services-wearable/core/src/main/java/org/microg/gms/wearable/NodeDatabaseHelper.java @@ -21,6 +21,7 @@ import android.database.Cursor; import android.database.sqlite.SQLiteDatabase; import android.database.sqlite.SQLiteOpenHelper; +import android.net.Uri; import android.text.TextUtils; import android.util.Log; @@ -70,25 +71,50 @@ public synchronized Cursor getDataItemsForDataHolder(String packageName, String } public synchronized Cursor getDataItemsForDataHolderByHostAndPath(String packageName, String signatureDigest, String host, String path) { + SQLiteDatabase db = getReadableDatabase(); + String[] params; String selection; + if (path == null) { params = new String[]{packageName, signatureDigest}; - selection = "packageName = ? AND signatureDigest = ?"; + selection = "a.packageName = ? AND a.signatureDigest = ?"; } else if (TextUtils.isEmpty(host)) { if (path.endsWith("/")) path = path + "%"; path = path.replace("*", "%"); params = new String[]{packageName, signatureDigest, path}; - selection = "packageName = ? AND signatureDigest = ? AND path LIKE ?"; + selection = "a.packageName = ? AND a.signatureDigest = ? AND d.path LIKE ?"; } else { if (path.endsWith("/")) path = path + "%"; path = path.replace("*", "%"); host = host.replace("*", "%"); params = new String[]{packageName, signatureDigest, host, path}; - selection = "packageName = ? AND signatureDigest = ? AND host = ? AND path LIKE ?"; + + selection = "a.packageName = ? AND a.signatureDigest = ? " + + "AND d.host LIKE ? AND d.path LIKE ?"; } - selection += " AND deleted=0 AND assetsPresent !=0"; - return getReadableDatabase().rawQuery("SELECT host AS host,path AS path,data AS data,\'\' AS tags,assetname AS asset_key,assets_digest AS asset_id FROM dataItemsAndAssets WHERE " + selection, params); + + selection += " AND d.deleted = 0 AND d.assetsPresent != 0"; + + String query = + "SELECT " + + "d._id AS _id, " + + "d.host AS host, " + + "d.path AS path, " + + "d.data AS data, " + + "'' AS tags, " + + "d.seqId AS seqId, " + + "d.timestampMs AS timestampMs, " + + "d.deleted AS deleted, " + + "d.sourceNode AS sourceNode, " + + "d.assetsPresent AS assetsPresent, " + + "a.packageName AS packageName, " + + "a.signatureDigest AS signatureDigest " + + "FROM dataitems d " + + "JOIN appkeys a ON d.appkeys_id = a._id " + + "WHERE " + selection; + + return db.rawQuery(query, params); } public synchronized Cursor getDataItemsByHostAndPath(String packageName, String signatureDigest, String host, String path) { @@ -145,11 +171,13 @@ public synchronized void putRecord(DataItemRecord record) { // insert key = insertRecord(db, record); } + if (record.assetsAreReady) { ContentValues update = new ContentValues(); update.put("assetsPresent", 1); db.update("dataitems", update, "_id=?", new String[]{key}); } + db.setTransactionSuccessful(); } finally { cursor.close(); @@ -157,19 +185,66 @@ public synchronized void putRecord(DataItemRecord record) { db.endTransaction(); } - private static void updateRecord(SQLiteDatabase db, String key, DataItemRecord record) { - ContentValues cv = record.toContentValues(); - db.update("dataitems", cv, "_id=?", new String[]{key}); - finishRecord(db, key, record); + private static void updateRecord(SQLiteDatabase db, String dataItemId, DataItemRecord record) { + ContentValues cv = new ContentValues(); + cv.put("seqId", record.seqId); + cv.put("deleted", record.deleted ? 1 : 0); + cv.put("sourceNode", record.source); + cv.put("data", record.dataItem.data); + cv.put("timestampMs", System.currentTimeMillis()); + cv.put("assetsPresent", record.assetsAreReady ? 1 : 0); + cv.put("v1SourceNode", record.source); + cv.put("v1SeqId", record.v1SeqId != 0 ? record.v1SeqId : record.seqId); + db.update("dataitems", cv, "_id=?", new String[]{dataItemId}); + db.delete("assetrefs", "dataitems_id=?", new String[]{dataItemId}); + + if (record.dataItem.getAssets() != null && !record.dataItem.getAssets().isEmpty()) { + for (Map.Entry entry : record.dataItem.getAssets().entrySet()) { + ContentValues assetRef = new ContentValues(); + assetRef.put("dataitems_id", Long.parseLong(dataItemId)); + assetRef.put("assetname", entry.getKey()); + assetRef.put("assets_digest", entry.getValue().getDigest()); + + db.insert("assetrefs", null, assetRef); + } + } + } private static String insertRecord(SQLiteDatabase db, DataItemRecord record) { - ContentValues contentValues = record.toContentValues(); - contentValues.put("appkeys_id", getAppKey(db, record.packageName, record.signatureDigest)); - contentValues.put("host", record.dataItem.host); - contentValues.put("path", record.dataItem.path); - String key = Long.toString(db.insertWithOnConflict("dataitems", "host", contentValues, SQLiteDatabase.CONFLICT_REPLACE)); - return finishRecord(db, key, record); + long appKeyId = getAppKey(db, record.packageName, record.signatureDigest); + + ContentValues cv = new ContentValues(); + cv.put("appkeys_id", appKeyId); + cv.put("host", record.dataItem.host); + cv.put("path", record.dataItem.path); + cv.put("seqId", record.seqId); + cv.put("deleted", record.deleted ? 1 : 0); + cv.put("sourceNode", record.source); + cv.put("data", record.dataItem.data); + cv.put("timestampMs", System.currentTimeMillis()); + cv.put("assetsPresent", record.assetsAreReady ? 1 : 0); + cv.put("v1SourceNode", record.source); + cv.put("v1SeqId", record.v1SeqId != 0 ? record.v1SeqId : record.seqId); + + long dataItemId = db.insert("dataitems", null, cv); + + if (record.dataItem.getAssets() != null && !record.dataItem.getAssets().isEmpty()) { + for (Map.Entry entry : record.dataItem.getAssets().entrySet()) { + String assetKey = entry.getKey(); + Asset asset = entry.getValue(); + + ContentValues assetRef = new ContentValues(); + assetRef.put("dataitems_id", dataItemId); + assetRef.put("assetname", assetKey); + assetRef.put("assets_digest", asset.getDigest()); + + db.insert("assetrefs", null, assetRef); + } + } + + + return String.valueOf(dataItemId); } private static String finishRecord(SQLiteDatabase db, String key, DataItemRecord record) { @@ -210,8 +285,34 @@ private static Cursor getDataItemsByHostAndPath(SQLiteDatabase db, String packag } public Cursor getModifiedDataItems(final String nodeId, final long seqId, final boolean excludeDeleted) { - String selection = "sourceNode =? AND seqId >?" + (excludeDeleted ? " AND deleted =0" : ""); - return getReadableDatabase().query("dataItemsAndAssets", GDIBHAP_FIELDS, selection, new String[]{nodeId, Long.toString(seqId)}, null, null, "seqId", null); + SQLiteDatabase db = getReadableDatabase(); + + String selection = "d.sourceNode = ? AND d.seqId > ?"; + if (excludeDeleted) { + selection += " AND d.deleted = 0"; + } + + String query = + "SELECT " + + "d._id AS _id, " + + "a.packageName AS packageName, " + + "a.signatureDigest AS signatureDigest, " + + "d.host AS host, " + + "d.path AS path, " + + "d.seqId AS seqId, " + + "d.deleted AS deleted, " + + "d.sourceNode AS sourceNode, " + + "d.data AS data, " + + "d.timestampMs AS timestampMs, " + + "d.assetsPresent AS assetsPresent, " + + "d.v1SourceNode AS v1SourceNode, " + + "d.v1SeqId AS v1SeqId " + + "FROM dataitems d " + + "JOIN appkeys a ON d.appkeys_id = a._id " + + "WHERE " + selection + " " + + "ORDER BY d.seqId"; + + return db.rawQuery(query, new String[]{nodeId, Long.toString(seqId)}); } public synchronized List deleteDataItems(String packageName, String signatureDigest, String host, String path) { @@ -268,7 +369,32 @@ public synchronized void allowAssetAccess(String digest, String packageName, Str } public Cursor listMissingAssets() { - return getReadableDatabase().query("dataItemsAndAssets", GDIBHAP_FIELDS, "assetsPresent = 0 AND assets_digest NOT NULL", null, null, null, "packageName, signatureDigest, host, path"); + SQLiteDatabase db = getReadableDatabase(); + + String query = + "SELECT " + + "a.packageName AS packageName, " + + "a.signatureDigest AS signatureDigest, " + + "d.host AS host, " + + "d.path AS path, " + + "d.seqId AS seqId, " + + "d.deleted AS deleted, " + + "d.sourceNode AS sourceNode, " + + "d.data AS data, " + + "d.timestampMs AS timestampMs, " + + "d.assetsPresent AS assetsPresent, " + + "d.v1SourceNode AS v1SourceNode, " + + "d.v1SeqId AS v1SeqId, " + + "ar.assetname AS assetname, " + + "ar.assets_digest AS assets_digest " + + "FROM dataitems d " + + "JOIN appkeys a ON d.appkeys_id = a._id " + + "JOIN assetrefs ar ON d._id = ar.dataitems_id " + + "WHERE d.assetsPresent = 0 " + + "AND ar.assets_digest IS NOT NULL " + + "ORDER BY a.packageName, a.signatureDigest, d.host, d.path"; + + return db.rawQuery(query, null); } public boolean hasAsset(Asset asset) { @@ -281,16 +407,85 @@ public boolean hasAsset(Asset asset) { } } + public void markAssetAsMissing(String digest, String packageName, String signatureDigest) { + SQLiteDatabase db = getWritableDatabase(); + + Cursor cursor = db.query("assets", new String[]{"digest"}, + "digest = ?", new String[]{digest}, null, null, null); + + boolean exists = cursor.moveToFirst(); + cursor.close(); + + if (!exists) { + ContentValues values = new ContentValues(); + values.put("digest", digest); + values.put("dataPresent", 0); + values.put("timestampMs", System.currentTimeMillis()); + db.insertWithOnConflict("assets", null, values, SQLiteDatabase.CONFLICT_IGNORE); + } + + allowAssetAccess(digest, packageName, signatureDigest); + } + + public Cursor getDataItemsWaitingForAsset(String digest) { + SQLiteDatabase db = getReadableDatabase(); + + String query = "SELECT " + + "d._id AS _id, " + + "a.packageName AS packageName, " + + "a.signatureDigest AS signatureDigest, " + + "d.host AS host, " + + "d.path AS path, " + + "d.seqId AS seqId, " + + "d.deleted AS deleted, " + + "d.sourceNode AS sourceNode, " + + "d.data AS data, " + + "d.timestampMs AS timestampMs, " + + "d.assetsPresent AS assetsPresent " + + "FROM dataitems d " + + "JOIN appkeys a ON d.appkeys_id = a._id " + + "WHERE d.assetsPresent = 0 " + + "AND d._id IN (" + + " SELECT dataitems_id FROM assetrefs WHERE assets_digest = ?" + + ")"; + + return db.rawQuery(query, new String[]{digest}); + } + + public void updateAssetsReady(String uri, boolean ready) { + SQLiteDatabase db = getWritableDatabase(); + ContentValues values = new ContentValues(); + values.put("assetsPresent", ready ? 1 : 0); + + Uri parsedUri = Uri.parse(uri); + String host = parsedUri.getAuthority(); + String path = parsedUri.getPath(); + + db.update("dataitems", values, + "host = ? AND path = ?", new String[]{host, path}); + } + + public synchronized void markAssetAsPresent(String digest) { ContentValues cv = new ContentValues(); cv.put("dataPresent", 1); + cv.put("timestampMs", System.currentTimeMillis()); SQLiteDatabase db = getWritableDatabase(); - db.update("assets", cv, "digest=?", new String[]{digest}); + int rows = db.update("assets", cv, "digest=?", new String[]{digest}); + if (rows == 0) { + cv.put("digest", digest); + db.insert("assets", null, cv); + } Cursor status = db.query("assetsReadyStatus", null, "nowReady != markedReady", null, null, null, null); while (status.moveToNext()) { - cv = new ContentValues(); - cv.put("assetsPresent", status.getInt(status.getColumnIndexOrThrow("nowReady"))); - db.update("dataitems", cv, "_id=?", new String[]{Integer.toString(status.getInt(status.getColumnIndexOrThrow("dataitems_id")))}); + int nowReady = status.getInt(status.getColumnIndexOrThrow("nowReady")); + int dataItemId = status.getInt(status.getColumnIndexOrThrow("dataitems_id")); + + ContentValues update = new ContentValues(); + update.put("assetsPresent", nowReady); + db.update("dataitems", update, "_id=?", + new String[]{String.valueOf(dataItemId)}); + } status.close(); } diff --git a/play-services-wearable/core/src/main/java/org/microg/gms/wearable/WearableImpl.java b/play-services-wearable/core/src/main/java/org/microg/gms/wearable/WearableImpl.java index 9c8ba3f6b5..0597f300e1 100644 --- a/play-services-wearable/core/src/main/java/org/microg/gms/wearable/WearableImpl.java +++ b/play-services-wearable/core/src/main/java/org/microg/gms/wearable/WearableImpl.java @@ -57,6 +57,7 @@ import org.microg.gms.wearable.proto.AckAsset; import org.microg.gms.wearable.proto.AppKey; import org.microg.gms.wearable.proto.AppKeys; +import org.microg.gms.wearable.proto.AssetEntry; import org.microg.gms.wearable.proto.Connect; import org.microg.gms.wearable.proto.FetchAsset; import org.microg.gms.wearable.proto.FilePiece; @@ -207,18 +208,24 @@ public DataItemRecord putDataItem(String packageName, String signatureDigest, St } public DataItemRecord putDataItem(DataItemRecord record) { - nodeDatabase.putRecord(record); - if (!record.assetsAreReady) { - for (Asset asset : record.dataItem.getAssets().values()) { - if (!nodeDatabase.hasAsset(asset)) { - Log.d(TAG, "Asset is missing: " + asset); - } + boolean allAssetsPresent = true; + for (Asset asset : record.dataItem.getAssets().values()) { + String digest = asset.getDigest(); + if (digest != null && !assetFileExists(digest)) { + Log.d(TAG, "Asset is missing: " + asset); + allAssetsPresent = false; + nodeDatabase.markAssetAsMissing(digest, record.packageName, record.signatureDigest); } } + record.assetsAreReady = allAssetsPresent; + + nodeDatabase.putRecord(record); + Intent intent = new Intent("com.google.android.gms.wearable.DATA_CHANGED"); intent.setPackage(record.packageName); intent.setData(record.dataItem.uri); invokeListeners(intent, listener -> listener.onDataChanged(record.toEventDataHolder())); + return record; } @@ -262,12 +269,6 @@ public File createAssetFile(String digest) { return new File(dir, digest + ".asset"); } - private File createAssetReceiveTempFile(String name) { - File dir = new File(context.getFilesDir(), "piece"); - dir.mkdirs(); - return new File(dir, name); - } - private String calculateDigest(byte[] data) { try { return Base64.encodeToString(MessageDigest.getInstance("SHA1").digest(data), Base64.NO_WRAP | Base64.NO_PADDING | Base64.URL_SAFE); @@ -456,33 +457,16 @@ public long getCurrentSeqId(String nodeId) { return nodeDatabase.getCurrentSeqId(nodeId); } - public void handleFilePiece(WearableConnection connection, String fileName, byte[] bytes, String finalPieceDigest) { - File file = createAssetReceiveTempFile(fileName); - try { - FileOutputStream fos = new FileOutputStream(file, true); - fos.write(bytes); - fos.close(); - } catch (IOException e) { - Log.w(TAG, e); - } - if (finalPieceDigest != null) { - // This is a final piece. If digest matches we're so happy! - try { - String digest = calculateDigest(Utils.readStreamToEnd(new FileInputStream(file))); - if (digest.equals(finalPieceDigest)) { - if (file.renameTo(createAssetFile(digest))) { - nodeDatabase.markAssetAsPresent(digest); - connection.writeMessage(new RootMessage.Builder().ackAsset(new AckAsset(digest)).build()); - } else { - Log.w(TAG, "Could not rename to target file name. delete=" + file.delete()); - } - } else { - Log.w(TAG, "Received digest does not match. delete=" + file.delete()); - } - } catch (IOException e) { - Log.w(TAG, "Failed working with temp file. delete=" + file.delete(), e); - } - } + public boolean assetFileExists(String digest) { + if (digest == null) return false; + File assetFile = createAssetFile(digest); + return assetFile.exists(); + } + + public File createAssetReceiveTempFile(String name) { + File dir = new File(context.getFilesDir(), "piece"); + dir.mkdirs(); + return new File(dir, name); } public void onConnectReceived(WearableConnection connection, String nodeId, Connect connect) { @@ -498,30 +482,86 @@ public void onConnectReceived(WearableConnection connection, String nodeId, Conn } Log.d(TAG, "Adding connection to list of open connections: " + connection + " with connect " + connect); activeConnections.put(connect.id, connection); - onPeerConnected(new NodeParcelable(connect.id, connect.name)); + + onPeerConnected(new NodeParcelable(connect.id, connect.name, 0, true)); + // Fetch missing assets syncToPeer(connect.id, nodeId, getCurrentSeqId(nodeId)); + + try { + networkHandlerLock.await(); + networkHandler.postDelayed(() -> { + if (activeConnections.containsKey(connect.id)) { + fetchMissingAssets(connect.id); + } else { + Log.d(TAG, "Connection closed before asset fetch could start"); + } + }, 1000); + } catch (InterruptedException e) { + Log.w(TAG, "Interrupted while scheduling asset fetch", e); + } + } + + private void fetchMissingAssets(String nodeId) { + WearableConnection connection = activeConnections.get(nodeId); + if (connection == null) { + Log.d(TAG, "Connection no longer active for node: " + nodeId); + return; + } + Cursor cursor = nodeDatabase.listMissingAssets(); if (cursor != null) { - while (cursor.moveToNext()) { - try { - Log.d(TAG, "Fetch for " + cursor.getString(12)); - connection.writeMessage(new RootMessage.Builder() - .fetchAsset(new FetchAsset.Builder() - .assetName(cursor.getString(12)) - .packageName(cursor.getString(1)) - .signatureDigest(cursor.getString(2)) - .permission(false) - .build()).build()); - } catch (IOException e) { - Log.w(TAG, "Error fetching asset", e); - closeConnection(connect.id); + try { + int fetchCount = 0; + while (cursor.moveToNext()) { + // Check if connection is still active before each write attempt + if (!activeConnections.containsKey(nodeId)) { + Log.d(TAG, "Connection closed during asset fetch, stopping (fetched " + fetchCount + " assets)"); + break; + } + + try { + String assetName = cursor.getString(12); + String packageName = cursor.getString(1); + String signatureDigest = cursor.getString(2); + + connection.writeMessage(new RootMessage.Builder() + .fetchAsset(new FetchAsset.Builder() + .assetName(assetName) + .packageName(packageName) + .signatureDigest(signatureDigest) + .permission(false) + .build()).build()); + + fetchCount++; + + // Add small delay between requests to avoid overwhelming the connection + if (fetchCount % 10 == 0) { + try { + Thread.sleep(100); + } catch (InterruptedException e) { + Log.d(TAG, "Asset fetch interrupted"); + break; + } + } + + } catch (IOException e) { + Log.w(TAG, "Error fetching asset (fetched " + fetchCount + " so far): " + e.getMessage()); + closeConnection(nodeId); + break; // Stop fetching on first error + } + } + + if (fetchCount > 0) { + Log.d(TAG, "Fetched " + fetchCount + " missing assets from " + nodeId); } + } finally { + cursor.close(); } - cursor.close(); } } + public void onDisconnectReceived(WearableConnection connection, Connect connect) { for (ConnectionConfiguration config : getConfigurations()) { if (config.nodeId.equals(connect.id)) { @@ -545,7 +585,7 @@ interface ListenerInvoker { void invoke(IWearableListener listener) throws RemoteException; } - private void invokeListeners(@Nullable Intent intent, ListenerInvoker invoker) { + public void invokeListeners(@Nullable Intent intent, ListenerInvoker invoker) { for (String packageName : new ArrayList<>(listeners.keySet())) { List listeners = this.listeners.get(packageName); if (listeners == null) continue; @@ -881,14 +921,6 @@ public int deleteDataItems(Uri uri, String packageName) { return records.size(); } - public void sendMessageReceived(String packageName, MessageEventParcelable messageEvent) { - Log.d(TAG, "onMessageReceived: " + messageEvent); - Intent intent = new Intent("com.google.android.gms.wearable.MESSAGE_RECEIVED"); - intent.setPackage(packageName); - intent.setData(Uri.parse("wear://" + getLocalNodeId() + "/" + messageEvent.getPath())); - invokeListeners(intent, listener -> listener.onMessageReceived(messageEvent)); - } - public DataItemRecord getDataItemByUri(Uri uri, String packageName) { Cursor cursor = nodeDatabase.getDataItemsByHostAndPath(packageName, PackageUtils.firstSignatureDigest(context, packageName), fixHost(uri.getHost(), true), uri.getPath()); DataItemRecord record = null; @@ -912,16 +944,22 @@ private IWearableListener getListener(String packageName, String action, Uri uri private void closeConnection(String nodeId) { WearableConnection connection = activeConnections.get(nodeId); - try { - connection.close(); - } catch (IOException e1) { - Log.w(TAG, e1); + if (connection != null) { + try { + connection.close(); + } catch (IOException e1) { + Log.w(TAG, "Error closing connection", e1); + } } - if (connection == sct.getWearableConnection()) { + + + if (connection == sct.getWearableConnection() && sct != null) { sct.close(); sct = null; } + activeConnections.remove(nodeId); + String name = "Wear device"; for (ConnectionConfiguration config : getConfigurations()) { if (nodeId.equals(config.nodeId) || nodeId.equals(config.peerNodeId)) { @@ -929,6 +967,7 @@ private void closeConnection(String nodeId) { name = config.name; } } + onPeerDisconnected(new NodeParcelable(nodeId, name)); Log.d(TAG, "Closed connection to " + nodeId + " on error"); } @@ -984,4 +1023,13 @@ private ListenerInfo(IWearableListener listener, IntentFilter[] filters) { this.filters = filters; } } + + public NodeDatabaseHelper getNodeDatabase() { + return nodeDatabase; + } + + public ClockworkNodePreferences getClockworkNodePreferences() { + return clockworkNodePreferences; + } + } diff --git a/play-services-wearable/core/src/main/java/org/microg/gms/wearable/WearableService.java b/play-services-wearable/core/src/main/java/org/microg/gms/wearable/WearableService.java index 49b79bafb4..e1ebf54e0f 100644 --- a/play-services-wearable/core/src/main/java/org/microg/gms/wearable/WearableService.java +++ b/play-services-wearable/core/src/main/java/org/microg/gms/wearable/WearableService.java @@ -16,7 +16,14 @@ package org.microg.gms.wearable; +import android.app.Notification; +import android.app.NotificationChannel; +import android.app.NotificationManager; +import android.os.Build; import android.os.RemoteException; +import android.util.Log; + +import androidx.core.app.NotificationCompat; import com.google.android.gms.common.Feature; import com.google.android.gms.common.internal.ConnectionInfo; @@ -26,6 +33,7 @@ import org.microg.gms.BaseService; import org.microg.gms.common.GmsService; import org.microg.gms.common.PackageUtils; +import org.microg.gms.wearable.core.R; public class WearableService extends BaseService { @@ -65,6 +73,10 @@ public class WearableService extends BaseService { new Feature("wear_consents_per_watch", 3L) }; + private boolean isForeground = false; + private static final int NOTIFICATION_ID = 1001; + private static final String CHANNEL_ID = "wearable_service"; + public WearableService() { super("GmsWearSvc", GmsService.WEAR); } @@ -72,11 +84,44 @@ public WearableService() { @Override public void onCreate() { super.onCreate(); + createNotificationChannel(); ConfigurationDatabaseHelper configurationDatabaseHelper = new ConfigurationDatabaseHelper(getApplicationContext()); NodeDatabaseHelper nodeDatabaseHelper = new NodeDatabaseHelper(getApplicationContext()); wearable = new WearableImpl(getApplicationContext(), nodeDatabaseHelper, configurationDatabaseHelper); } + private void createNotificationChannel() { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + NotificationChannel channel = new NotificationChannel( + CHANNEL_ID, + "Wearable Connection", + NotificationManager.IMPORTANCE_LOW + ); + channel.setShowBadge(false); + NotificationManager nm = getSystemService(NotificationManager.class); + nm.createNotificationChannel(channel); + } + } + + public void setConnectionActive(boolean active) { + if (active && !isForeground) { + Notification notification = new NotificationCompat.Builder(this, CHANNEL_ID) + .setSmallIcon(org.microg.gms.base.core.R.drawable.ic_radio_checked) // or whatever icon + .setContentTitle("Connected to watch") + .setOngoing(true) + .setPriority(NotificationCompat.PRIORITY_LOW) + .build(); + + startForeground(NOTIFICATION_ID, notification); + isForeground = true; + Log.d(TAG, "Started foreground service for active connection"); + } else if (!active && isForeground) { + stopForeground(true); + isForeground = false; + Log.d(TAG, "Stopped foreground service"); + } + } + @Override public void onDestroy() { super.onDestroy(); diff --git a/play-services-wearable/core/src/main/java/org/microg/gms/wearable/bluetooth/BluetoothConnectionThread.java b/play-services-wearable/core/src/main/java/org/microg/gms/wearable/bluetooth/BluetoothConnectionThread.java index d5d569a803..3b2c3a9a20 100644 --- a/play-services-wearable/core/src/main/java/org/microg/gms/wearable/bluetooth/BluetoothConnectionThread.java +++ b/play-services-wearable/core/src/main/java/org/microg/gms/wearable/bluetooth/BluetoothConnectionThread.java @@ -14,6 +14,7 @@ import com.google.android.gms.wearable.ConnectionConfiguration; +import org.microg.gms.wearable.MessageHandler; import org.microg.gms.wearable.WearableConnection; import org.microg.gms.wearable.WearableImpl; import org.microg.gms.wearable.proto.Connect; @@ -251,6 +252,8 @@ private static class ConnectionListener implements WearableConnection.Listener { private Connect peerConnect; private WearableConnection connection; + private MessageHandler messageHandler; + public ConnectionListener(Context context, ConnectionConfiguration config, WearableImpl wearableImpl) { this.context = context; this.config = config; @@ -266,13 +269,16 @@ public void onConnected(WearableConnection connection) { BluetoothWearableConnection btConnection = (BluetoothWearableConnection) connection; this.peerConnect = btConnection.getPeerConnect(); + this.messageHandler = new MessageHandler(context, wearableImpl, config); + wearableImpl.onConnectReceived(connection, config.nodeId, peerConnect); } @Override public void onMessage(WearableConnection connection, RootMessage message) { Log.d(TAG, "Message received from " + config.address + ": " + message.toString()); - + if (peerConnect != null && messageHandler != null) + messageHandler.handleMessage(connection, peerConnect.id, message); } @Override diff --git a/play-services-wearable/core/src/main/java/org/microg/gms/wearable/bluetooth/BluetoothWearableConnection.java b/play-services-wearable/core/src/main/java/org/microg/gms/wearable/bluetooth/BluetoothWearableConnection.java index aaf0f6eca2..44bdd7a303 100644 --- a/play-services-wearable/core/src/main/java/org/microg/gms/wearable/bluetooth/BluetoothWearableConnection.java +++ b/play-services-wearable/core/src/main/java/org/microg/gms/wearable/bluetooth/BluetoothWearableConnection.java @@ -102,11 +102,23 @@ public boolean isHandshakeComplete() { } protected void writeMessagePiece(MessagePiece piece) throws IOException { -// byte[] bytes = piece.toByteArray(); - byte[] bytes = MessagePiece.ADAPTER.encode(piece); - os.writeInt(bytes.length); - os.write(bytes); - os.flush(); + if (socket == null) { + throw new IOException("Socket is null"); + } + + if (!socket.isConnected()) { + throw new IOException("Socket is not connected"); + } + + try { + byte[] bytes = MessagePiece.ADAPTER.encode(piece); + os.writeInt(bytes.length); + os.write(bytes); + os.flush(); + } catch (IOException e) { + throw new IOException("Failed to write message piece (size: " + + (piece.data != null ? piece.data.size() : 0) + " bytes): " + e.getMessage(), e); + } } @Override diff --git a/play-services-wearable/core/src/main/java/org/microg/gms/wearable/channel/ChannelManager.java b/play-services-wearable/core/src/main/java/org/microg/gms/wearable/channel/ChannelManager.java index 7d5b7fe532..376ae4ee20 100644 --- a/play-services-wearable/core/src/main/java/org/microg/gms/wearable/channel/ChannelManager.java +++ b/play-services-wearable/core/src/main/java/org/microg/gms/wearable/channel/ChannelManager.java @@ -70,6 +70,7 @@ public void stop() { synchronized (lock) { for (ChannelStateMachine channel : channels.values()) { try { + channel.clearOpenCallback(); channel.close(); } catch (Exception e) { Log.w(TAG, "Error closing channel on stop", e); diff --git a/play-services-wearable/core/src/main/java/org/microg/gms/wearable/channel/ChannelStateMachine.java b/play-services-wearable/core/src/main/java/org/microg/gms/wearable/channel/ChannelStateMachine.java index 145d372694..8ab96e2421 100644 --- a/play-services-wearable/core/src/main/java/org/microg/gms/wearable/channel/ChannelStateMachine.java +++ b/play-services-wearable/core/src/main/java/org/microg/gms/wearable/channel/ChannelStateMachine.java @@ -238,6 +238,10 @@ public void closeOutputStream(int closeReason, int errorCode) throws IOException } } + public void clearOpenCallback() { + this.openCallback = null; + } + public void setInputStream(ParcelFileDescriptor fd, IChannelStreamCallbacks callbacks) throws RemoteException { if (receivingState == RECEIVING_STATE_CLOSED) { diff --git a/play-services-wearable/src/main/java/com/google/android/gms/wearable/Asset.java b/play-services-wearable/src/main/java/com/google/android/gms/wearable/Asset.java index c9e22edf1c..3f19ea60c9 100644 --- a/play-services-wearable/src/main/java/com/google/android/gms/wearable/Asset.java +++ b/play-services-wearable/src/main/java/com/google/android/gms/wearable/Asset.java @@ -26,6 +26,7 @@ import org.microg.safeparcel.SafeParceled; import java.util.Arrays; +import java.util.Objects; /** * An asset is a binary blob shared between data items that is replicated across the wearable @@ -50,8 +51,7 @@ public class Asset extends AutoSafeParcelable { @SafeParceled(5) private Uri uri; - private Asset() { - } + private Asset() {} private Asset(byte[] data, String digest, ParcelFileDescriptor fd, Uri uri) { this.data = data; @@ -64,7 +64,10 @@ private Asset(byte[] data, String digest, ParcelFileDescriptor fd, Uri uri) { * Creates an Asset using a byte array. */ public static Asset createFromBytes(byte[] assetData) { - return null; + if (assetData == null) { + throw new IllegalArgumentException("Asset data cannot be null"); + } + return new Asset(assetData, null, null, null); } /** @@ -93,7 +96,10 @@ public static Asset createFromRef(String digest) { * Uri. */ public static Asset createFromUri(Uri uri) { - return null; + if (uri == null) { + throw new IllegalArgumentException("Asset uri cannot be null"); + } + return new Asset(null, null, null, uri); } /** @@ -118,23 +124,28 @@ public Uri getUri() { return uri; } + public byte[] getData() { + return data; + } + @Override public boolean equals(Object o) { if (this == o) return true; - if (o == null || getClass() != o.getClass()) return false; + if (!(o instanceof Asset)) return false; +// if (o == null || getClass() != o.getClass()) return false; Asset asset = (Asset) o; if (!Arrays.equals(data, asset.data)) return false; - if (digest != null ? !digest.equals(asset.digest) : asset.digest != null) return false; - if (fd != null ? !fd.equals(asset.fd) : asset.fd != null) return false; - return !(uri != null ? !uri.equals(asset.uri) : asset.uri != null); + if (!Objects.equals(digest, asset.digest)) return false; + if (!Objects.equals(fd, asset.fd)) return false; + return Objects.equals(uri, asset.uri); } @Override public int hashCode() { - return Arrays.hashCode(new Object[]{data, digest, fd, uri}); + return Arrays.deepHashCode(new Object[]{data, digest, fd, uri}); } @Override diff --git a/play-services-wearable/src/main/java/com/google/android/gms/wearable/internal/DataItemAssetParcelable.java b/play-services-wearable/src/main/java/com/google/android/gms/wearable/internal/DataItemAssetParcelable.java index 769573602e..f58a0638e4 100644 --- a/play-services-wearable/src/main/java/com/google/android/gms/wearable/internal/DataItemAssetParcelable.java +++ b/play-services-wearable/src/main/java/com/google/android/gms/wearable/internal/DataItemAssetParcelable.java @@ -30,8 +30,7 @@ public class DataItemAssetParcelable extends AutoSafeParcelable implements DataI @SafeParceled(3) private String key; - private DataItemAssetParcelable() { - } + private DataItemAssetParcelable() {} public DataItemAssetParcelable(String id, String key) { this.id = id; diff --git a/play-services-wearable/src/main/java/com/google/android/gms/wearable/internal/DataItemParcelable.java b/play-services-wearable/src/main/java/com/google/android/gms/wearable/internal/DataItemParcelable.java index 837b5fa94d..8d512d87ef 100644 --- a/play-services-wearable/src/main/java/com/google/android/gms/wearable/internal/DataItemParcelable.java +++ b/play-services-wearable/src/main/java/com/google/android/gms/wearable/internal/DataItemParcelable.java @@ -38,8 +38,7 @@ public class DataItemParcelable extends AutoSafeParcelable implements DataItem { @SafeParceled(5) public byte[] data; - private DataItemParcelable() { - } + private DataItemParcelable() {} public DataItemParcelable(Uri uri) { this(uri, new HashMap()); diff --git a/play-services-wearable/src/main/java/com/google/android/gms/wearable/internal/MessageEventParcelable.java b/play-services-wearable/src/main/java/com/google/android/gms/wearable/internal/MessageEventParcelable.java index 1eb95c32a2..8cbaa566df 100644 --- a/play-services-wearable/src/main/java/com/google/android/gms/wearable/internal/MessageEventParcelable.java +++ b/play-services-wearable/src/main/java/com/google/android/gms/wearable/internal/MessageEventParcelable.java @@ -34,6 +34,15 @@ public class MessageEventParcelable extends AutoSafeParcelable implements Messag @SafeParceled(5) public String sourceNodeId; + private MessageEventParcelable() {} + + public MessageEventParcelable(int requestId, String path, byte[] data, String sourceNodeId) { + this.requestId = requestId; + this.path = path; + this.data = data; + this.sourceNodeId = sourceNodeId; + } + @Override public byte[] getData() { return data; diff --git a/play-services-wearable/src/main/java/com/google/android/gms/wearable/internal/NodeParcelable.java b/play-services-wearable/src/main/java/com/google/android/gms/wearable/internal/NodeParcelable.java index c9cad07f8a..6207e3b907 100644 --- a/play-services-wearable/src/main/java/com/google/android/gms/wearable/internal/NodeParcelable.java +++ b/play-services-wearable/src/main/java/com/google/android/gms/wearable/internal/NodeParcelable.java @@ -49,13 +49,8 @@ public NodeParcelable(String nodeId, String displayName, int hops, boolean isNea this.isNearby = isNearby; } -// public NodeParcelable(String nodeId, String displayName) { -// this(nodeId, displayName, 0, false); -// } - - public NodeParcelable(String nodeId, String displayName) { - this(nodeId, displayName, 0, true); + this(nodeId, displayName, 0, false); } public NodeParcelable(Node node) { diff --git a/play-services-wearable/src/main/java/com/google/android/gms/wearable/internal/PutDataRequest.java b/play-services-wearable/src/main/java/com/google/android/gms/wearable/internal/PutDataRequest.java index ec9d6b2fc6..1d9234c420 100644 --- a/play-services-wearable/src/main/java/com/google/android/gms/wearable/internal/PutDataRequest.java +++ b/play-services-wearable/src/main/java/com/google/android/gms/wearable/internal/PutDataRequest.java @@ -23,11 +23,13 @@ import com.google.android.gms.wearable.Asset; import com.google.android.gms.wearable.DataItem; +import com.google.android.gms.wearable.DataItemAsset; import org.microg.gms.common.PublicApi; import org.microg.safeparcel.AutoSafeParcelable; import org.microg.safeparcel.SafeParceled; +import java.util.Collections; import java.util.HashMap; import java.util.Map; @@ -53,14 +55,35 @@ public class PutDataRequest extends AutoSafeParcelable { private PutDataRequest() { uri = null; assets = new Bundle(); + initializeAssetsClassLoader(); } private PutDataRequest(Uri uri) { this.uri = uri; assets = new Bundle(); + initializeAssetsClassLoader(); } + private PutDataRequest(Uri uri, Bundle assets, byte[] data, long syncDeadline) { + this.uri = uri; + this.assets = assets; + this.data = data; + this.syncDeadline = syncDeadline; + initializeAssetsClassLoader(); + } + + private void initializeAssetsClassLoader() { + ClassLoader classLoader = DataItemAssetParcelable.class.getClassLoader(); + if (classLoader != null) { + assets.setClassLoader(classLoader); + } + } + + public static PutDataRequest create(Uri uri) { + if (uri == null) { + throw new IllegalArgumentException("uri must not be null"); + } return new PutDataRequest(uri); } @@ -77,17 +100,45 @@ public static PutDataRequest create(String path) { } public static PutDataRequest createFromDataItem(DataItem source) { + if (source == null) { + throw new IllegalArgumentException("source must not be null"); + } PutDataRequest dataRequest = new PutDataRequest(source.getUri()); dataRequest.data = source.getData(); - // TODO: assets + + Map sourceAssets = source.getAssets(); + if (sourceAssets != null) { + for (Map.Entry entry : sourceAssets.entrySet()) { + DataItemAsset itemAsset = entry.getValue(); + if (itemAsset != null && itemAsset.getId() != null) { + Asset asset = Asset.createFromRef(itemAsset.getId()); + dataRequest.putAsset(entry.getKey(), asset); + } + } + } return dataRequest; } public static PutDataRequest createWithAutoAppendedId(String pathPrefix) { - return new PutDataRequest(null); + if (TextUtils.isEmpty(pathPrefix)) { + throw new IllegalArgumentException("An empty pathPrefix was supplied."); + } else if (!pathPrefix.startsWith("/")) { + throw new IllegalArgumentException("A pathPrefix must start with a single / ."); + } else if (pathPrefix.startsWith("//")) { + throw new IllegalArgumentException("A pathPrefix must start with a single / ."); + } + String uniqueId = Long.toHexString(System.currentTimeMillis()) + + Long.toHexString(Double.doubleToLongBits(Math.random())); + String path = pathPrefix.endsWith("/") ? pathPrefix + uniqueId : pathPrefix + "/" + uniqueId; + return create(path); } public Asset getAsset(String key) { + if (key == null) { + return null; + } + + assets.setClassLoader(DataItemAssetParcelable.class.getClassLoader()); return assets.getParcelable(key); } @@ -97,7 +148,7 @@ public Map getAssets() { for (String key : assets.keySet()) { map.put(key, (Asset) assets.getParcelable(key)); } - return map; + return Collections.unmodifiableMap(map); } public byte[] getData() { @@ -113,8 +164,15 @@ public boolean hasAsset(String key) { } public PutDataRequest putAsset(String key, Asset value) { + if (key == null) { + throw new IllegalArgumentException("key must not be null"); + } + if (value == null) { + throw new IllegalArgumentException("value must not be null"); + } assets.putParcelable(key, value); return this; + } public PutDataRequest removeAsset(String key) { @@ -127,6 +185,15 @@ public PutDataRequest setData(byte[] data) { return this; } + public PutDataRequest setUrgent(boolean urgent) { + this.syncDeadline = urgent ? 0 : DEFAULT_SYNC_DEADLINE; + return this; + } + + public boolean isUrgent() { + return syncDeadline == 0; + } + @Override public String toString() { return toString(false); @@ -134,17 +201,21 @@ public String toString() { public String toString(boolean verbose) { StringBuilder sb = new StringBuilder(); - sb.append("PutDataRequest[uri=").append(uri) - .append(", data=").append(data == null ? "null" : Base64.encodeToString(data, Base64.NO_WRAP)) - .append(", numAssets=").append(getAssets().size()); + sb.append("PutDataRequest["); + sb.append("dataSz=").append(data == null ? "null" : data.length); + sb.append(", numAssets=").append(assets.size()); + sb.append(", uri=").append(uri); + sb.append(", syncDeadline=").append(syncDeadline); + if (verbose && !getAssets().isEmpty()) { - sb.append(", assets=["); - for (String key : getAssets().keySet()) { - sb.append(key).append('=').append(getAsset(key)).append(", "); + sb.append("]\n assets: "); + for (Map.Entry entry : getAssets().entrySet()) { + sb.append("\n ").append(entry.getKey()).append(": ").append(entry.getValue()); } - sb.delete(sb.length() - 2, sb.length()).append(']'); + sb.append("\n ]"); + } else { + sb.append("]"); } - sb.append("]"); return sb.toString(); } From ab5c920077b75214002f0808dc1a3f2aab3a96a7 Mon Sep 17 00:00:00 2001 From: deadYokai Date: Fri, 9 Jan 2026 20:14:39 +0200 Subject: [PATCH 11/29] fix lint --- .../src/main/java/org/microg/gms/wearable/MessageHandler.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/play-services-wearable/core/src/main/java/org/microg/gms/wearable/MessageHandler.java b/play-services-wearable/core/src/main/java/org/microg/gms/wearable/MessageHandler.java index 8222f68b76..cf99144322 100644 --- a/play-services-wearable/core/src/main/java/org/microg/gms/wearable/MessageHandler.java +++ b/play-services-wearable/core/src/main/java/org/microg/gms/wearable/MessageHandler.java @@ -414,7 +414,7 @@ public void handleFilePiece(WearableConnection connection, String fileName, byte if (finalPieceDigest != null) { // This is a final piece. If digest matches we're so happy! try { - String digest = calculateDigest(Utils.readStreamToEnd(Files.newInputStream(file.toPath()))); + String digest = calculateDigest(Utils.readStreamToEnd(new FileInputStream(file))); if (digest.equals(finalPieceDigest)) { if (file.renameTo(wearable.createAssetFile(digest))) { wearable.getNodeDatabase().markAssetAsPresent(digest); From f9244e2f8e0c572b1f57f047133506051da2ac11 Mon Sep 17 00:00:00 2001 From: deadYokai Date: Tue, 13 Jan 2026 00:08:42 +0200 Subject: [PATCH 12/29] updated scary Bluetooth things - and some DataItem changes --- .../core/src/main/AndroidManifest.xml | 2 + .../microg/gms/wearable/DataItemRecord.java | 16 +- .../microg/gms/wearable/MessageHandler.java | 81 ++++---- .../gms/wearable/WearableConnection.java | 89 +++++--- .../org/microg/gms/wearable/WearableImpl.java | 24 ++- .../wearable/bluetooth/BluetoothClient.java | 126 +++++++++--- .../bluetooth/BluetoothConnectionThread.java | 189 +++++++++++++++-- .../BluetoothWearableConnection.java | 193 +++++++++++++++--- .../gms/wearable/channel/ChannelManager.java | 37 +++- .../core/src/main/proto/wearable.proto | 4 +- 10 files changed, 598 insertions(+), 163 deletions(-) diff --git a/play-services-wearable/core/src/main/AndroidManifest.xml b/play-services-wearable/core/src/main/AndroidManifest.xml index 9df69b73bd..befff0bbe9 100644 --- a/play-services-wearable/core/src/main/AndroidManifest.xml +++ b/play-services-wearable/core/src/main/AndroidManifest.xml @@ -6,6 +6,8 @@ + + diff --git a/play-services-wearable/core/src/main/java/org/microg/gms/wearable/DataItemRecord.java b/play-services-wearable/core/src/main/java/org/microg/gms/wearable/DataItemRecord.java index 7ff7738b56..2fec7c38cb 100644 --- a/play-services-wearable/core/src/main/java/org/microg/gms/wearable/DataItemRecord.java +++ b/play-services-wearable/core/src/main/java/org/microg/gms/wearable/DataItemRecord.java @@ -139,14 +139,18 @@ public static DataItemRecord fromCursor(Cursor cursor) { record.dataItem.data = cursor.getBlob(8); record.lastModified = cursor.getLong(9); record.assetsAreReady = cursor.getLong(10) > 0; - if (cursor.getString(11) != null) { - record.dataItem.addAsset(cursor.getString(11), Asset.createFromRef(cursor.getString(12))); - while (cursor.moveToNext()) { - if (cursor.getLong(5) == record.seqId) { - record.dataItem.addAsset(cursor.getString(11), Asset.createFromRef(cursor.getString(12))); + if (cursor.getColumnCount() >= 12) { + if (cursor.getString(11) != null) { + record.dataItem.addAsset(cursor.getString(11), Asset.createFromRef(cursor.getString(12))); + while (cursor.moveToNext()) { + if (cursor.getLong(5) == record.seqId) { + record.dataItem.addAsset(cursor.getString(11), Asset.createFromRef(cursor.getString(12))); + } } + cursor.moveToPrevious(); } - cursor.moveToPrevious(); + } else { + Log.w("DataItemRecord", "Cursor missing asset columns (11,12), skipping asset loading"); } return record; } diff --git a/play-services-wearable/core/src/main/java/org/microg/gms/wearable/MessageHandler.java b/play-services-wearable/core/src/main/java/org/microg/gms/wearable/MessageHandler.java index cf99144322..fb1d3129d0 100644 --- a/play-services-wearable/core/src/main/java/org/microg/gms/wearable/MessageHandler.java +++ b/play-services-wearable/core/src/main/java/org/microg/gms/wearable/MessageHandler.java @@ -409,54 +409,47 @@ public void handleFilePiece(WearableConnection connection, String fileName, byte fos.write(bytes); fos.close(); } catch (IOException e) { - Log.w(TAG, e); + Log.w(TAG, "Error writing file piece", e); + } + + if (finalPieceDigest == null) { + return; } - if (finalPieceDigest != null) { - // This is a final piece. If digest matches we're so happy! + + // This is a final piece. If digest matches we're so happy! + try { + String digest = calculateDigest(Utils.readStreamToEnd(new FileInputStream(file))); + + if (!digest.equals(finalPieceDigest)) { + Log.w(TAG, "Digest mismatch: expected=" + finalPieceDigest + + ", actual=" + digest + ". Deleting temp file."); + file.delete(); + return; + } + + File targetFile = wearable.createAssetFile(digest); + if (!file.renameTo(targetFile)) { + Log.w(TAG, "Failed to rename temp file to target. Deleting temp file."); + file.delete(); + return; + } + + Log.d(TAG, "Asset saved successfully: " + digest); + try { - String digest = calculateDigest(Utils.readStreamToEnd(new FileInputStream(file))); - if (digest.equals(finalPieceDigest)) { - if (file.renameTo(wearable.createAssetFile(digest))) { - wearable.getNodeDatabase().markAssetAsPresent(digest); - connection.writeMessage(new RootMessage.Builder().ackAsset(new AckAsset(digest)).build()); - Cursor cursor = wearable.getNodeDatabase().getDataItemsWaitingForAsset(digest); - if (cursor != null) { - try { - while (cursor.moveToNext()) { - DataItemRecord record = DataItemRecord.fromCursor(cursor); - boolean allPresent = true; - for (Asset asset : record.dataItem.getAssets().values()) { - if (!wearable.assetFileExists(asset.getDigest())) { - allPresent = false; - break; - } - } - - if (allPresent && !record.assetsAreReady) { - Log.d(TAG, "All assets now ready for: " + record.dataItem.uri); - record.assetsAreReady = true; - wearable.getNodeDatabase().updateAssetsReady(record.dataItem.uri.toString(), true); - - Intent intent = new Intent("com.google.android.gms.wearable.DATA_CHANGED"); - intent.setPackage(record.packageName); - intent.setData(record.dataItem.uri); - wearable.invokeListeners(intent, listener -> listener.onDataChanged(record.toEventDataHolder())); - } - } - } finally { - cursor.close(); - } - } - - } else { - Log.w(TAG, "Could not rename to target file name. delete=" + file.delete()); - } - } else { - Log.w(TAG, "Received digest does not match. delete=" + file.delete()); - } + connection.writeMessage(new RootMessage.Builder() + .ackAsset(new AckAsset(digest)) + .build()); } catch (IOException e) { - Log.w(TAG, "Failed working with temp file. delete=" + file.delete(), e); + Log.w(TAG, "Failed to send asset ACK", e); } + + Log.d(TAG, "Asset " + digest + " marked as present. " + + "Data change notifications will be handled by database layer."); + + } catch (IOException e) { + Log.w(TAG, "Error processing final file piece", e); + file.delete(); } } diff --git a/play-services-wearable/core/src/main/java/org/microg/gms/wearable/WearableConnection.java b/play-services-wearable/core/src/main/java/org/microg/gms/wearable/WearableConnection.java index 4d264deef0..e55ac4e864 100644 --- a/play-services-wearable/core/src/main/java/org/microg/gms/wearable/WearableConnection.java +++ b/play-services-wearable/core/src/main/java/org/microg/gms/wearable/WearableConnection.java @@ -5,6 +5,8 @@ package org.microg.gms.wearable; +import android.util.Log; + import org.microg.gms.wearable.proto.MessagePiece; import org.microg.gms.wearable.proto.RootMessage; @@ -20,6 +22,7 @@ public abstract class WearableConnection implements Runnable { private static String B64ALPHABET = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_"; + private final String TAG = "WearableConnection"; private HashMap> piecesQueues = new HashMap>(); private final Listener listener; @@ -66,42 +69,55 @@ protected RootMessage readMessage() throws IOException { System.out.println("Waiting for new message..."); MessagePiece piece = readMessagePiece(); if (piece.totalPieces == 1) { + byte[] payload = piece.data.toByteArray(); + String calc = calculateDigest(payload); + if (!calc.equals(piece.digest)) { + throw new IOException("Digest mismatch for single-piece message"); + } return RootMessage.ADAPTER.decode(piece.data); - } else { + } + + synchronized (piecesQueues) { + List queue = piecesQueues.get(piece.queueId); + if (piece.thisPiece == 1) { - List queue = piecesQueues.get(piece.queueId); - String oldDigest = null; if (queue != null) { - oldDigest = queue.get(0).digest; + piecesQueues.remove(piece.queueId); } - queue = new ArrayList(piece.totalPieces); + queue = new ArrayList<>(piece.totalPieces); queue.add(piece); piecesQueues.put(piece.queueId, queue); - if (oldDigest != null) { - throw new IOException("Could not finish message of digest " + oldDigest + ", queue is used for newer messagee"); - } - } else { - List queue = piecesQueues.get(piece.queueId); - if (queue == null || !queue.get(0).digest.equals(piece.digest)) { - throw new IOException("Received " + piece.thisPiece + " before first piece."); - } - if (queue.size() + 1 != piece.thisPiece) { - throw new IOException("Received " + piece.thisPiece + " but expected piece" + queue.size() + 1); + continue; + } + + if (queue == null || !queue.get(0).digest.equals(piece.digest)) { + piecesQueues.remove(piece.queueId); + throw new IOException("Received " + piece.thisPiece + " before first piece."); + } + + if (queue.size() + 1 != piece.thisPiece) { + piecesQueues.remove(piece.queueId); + throw new IOException("Received " + piece.thisPiece + " but expected piece" + queue.size() + 1); + } + + queue.add(piece); + + if (piece.thisPiece == piece.totalPieces) { + piecesQueues.remove(piece.queueId); + ByteArrayOutputStream bos = new ByteArrayOutputStream(); + + for (MessagePiece messagePiece : queue) { + messagePiece.data.write(bos); } - queue.add(piece); - if (piece.thisPiece == piece.totalPieces) { - piecesQueues.remove(piece.queueId); - ByteArrayOutputStream bos = new ByteArrayOutputStream(); - for (MessagePiece messagePiece : queue) { - messagePiece.data.write(bos); - } - byte[] bytes = bos.toByteArray(); - if (!calculateDigest(bytes).equals(piece.digest)) { - throw new IOException("Merged pieces have digest " + calculateDigest(bytes) + ", but should be " + piece.digest); - } - return RootMessage.ADAPTER.decode(bytes); + + byte[] bytes = bos.toByteArray(); + if (!calculateDigest(bytes).equals(piece.digest)) { + throw new IOException("Merged pieces have digest " + calculateDigest(bytes) + ", but should be " + piece.digest); } + + return RootMessage.ADAPTER.decode(bytes); } + } } } @@ -116,13 +132,24 @@ public void run() { listener.onConnected(this); RootMessage message; while ((message = readMessage()) != null) { - listener.onMessage(this, message); + try { + listener.onMessage(this, message); + } catch (Exception e) { + Log.e(TAG, "Error processing message", e); + } } } catch (IOException e) { - // quit + Log.e(TAG, "Connection error", e); + } catch (Exception e) { + Log.e(TAG, "Unexpected error in connection", e); + } finally { + System.out.println("WearableConnection closed"); + try { + listener.onDisconnected(); + } catch (Exception e) { + Log.e(TAG, "Error in onDisconnected callback", e); + } } - System.out.println("WearableConnection closed"); - listener.onDisconnected(); } public interface Listener { diff --git a/play-services-wearable/core/src/main/java/org/microg/gms/wearable/WearableImpl.java b/play-services-wearable/core/src/main/java/org/microg/gms/wearable/WearableImpl.java index 0597f300e1..16f1ecd2c7 100644 --- a/play-services-wearable/core/src/main/java/org/microg/gms/wearable/WearableImpl.java +++ b/play-services-wearable/core/src/main/java/org/microg/gms/wearable/WearableImpl.java @@ -480,7 +480,8 @@ public void onConnectReceived(WearableConnection connection, String nodeId, Conn config.connected = true; } } - Log.d(TAG, "Adding connection to list of open connections: " + connection + " with connect " + connect); + Log.d(TAG, "Adding connection to list of open connections: " + connection + + " with connect " + connect); activeConnections.put(connect.id, connection); onPeerConnected(new NodeParcelable(connect.id, connect.name, 0, true)); @@ -516,7 +517,8 @@ private void fetchMissingAssets(String nodeId) { while (cursor.moveToNext()) { // Check if connection is still active before each write attempt if (!activeConnections.containsKey(nodeId)) { - Log.d(TAG, "Connection closed during asset fetch, stopping (fetched " + fetchCount + " assets)"); + Log.d(TAG, "Connection closed during asset fetch, stopping (fetched " + + fetchCount + " assets)"); break; } @@ -544,16 +546,30 @@ private void fetchMissingAssets(String nodeId) { break; } } - } catch (IOException e) { - Log.w(TAG, "Error fetching asset (fetched " + fetchCount + " so far): " + e.getMessage()); + Log.w(TAG, "Error fetching asset (fetched " + fetchCount + " so far): " + + e.getMessage()); closeConnection(nodeId); break; // Stop fetching on first error } } + // More delays for heavy operations if (fetchCount > 0) { Log.d(TAG, "Fetched " + fetchCount + " missing assets from " + nodeId); + if (channelManager != null) { + long cooldownMs = 500; + if (fetchCount > 100) { + cooldownMs = 1000; + } + if (fetchCount > 200) { + cooldownMs = 1500; + } + + Log.d(TAG, "Setting " + cooldownMs + "ms cooldown after fetching " + + fetchCount + " assets"); + channelManager.setOperationCooldown(cooldownMs); + } } } finally { cursor.close(); diff --git a/play-services-wearable/core/src/main/java/org/microg/gms/wearable/bluetooth/BluetoothClient.java b/play-services-wearable/core/src/main/java/org/microg/gms/wearable/bluetooth/BluetoothClient.java index bb522f2c9e..d7423e9d0c 100644 --- a/play-services-wearable/core/src/main/java/org/microg/gms/wearable/bluetooth/BluetoothClient.java +++ b/play-services-wearable/core/src/main/java/org/microg/gms/wearable/bluetooth/BluetoothClient.java @@ -29,6 +29,7 @@ public class BluetoothClient implements Closeable { private final WearableImpl wearableImpl; + private volatile boolean isShutdown = false; public BluetoothClient(Context context, WearableImpl wearableImpl) { this.context = context; @@ -87,7 +88,7 @@ private void onAdapterStateChanged(int state) { Log.d(TAG, "Ignoring STATE_OFF - adapter still enabled (stale broadcast)"); return; } - for (BluetoothConnectionThread thread : connections.values()) { + for (BluetoothConnectionThread thread : new HashMap<>(connections).values()) { thread.close(); } } @@ -95,35 +96,56 @@ private void onAdapterStateChanged(int state) { public void addConfig(ConnectionConfiguration config) { + if (isShutdown) { + Log.w(TAG, "BluetoothClient is shutdown, ignoring addConfig"); + return; + } validateConfig(config); - if (!btAdapter.isEnabled()) { - Log.w(TAG, "Bluetooth not enabled, skipping connection"); + if (btAdapter == null || !btAdapter.isEnabled()) { + Log.w(TAG, "Bluetooth not enabled, deferring connection"); + configurations.put(config.address, config); return; } - String address = config.address; + if (configurations.containsKey(address)) { Log.d(TAG, "Configuration already exists for " + address + ", reconnecting"); + + configurations.put(address, config); + BluetoothConnectionThread thread = connections.get(address); - if (thread != null && btAdapter.isEnabled()) { - thread.retryConnection(); - } - return; - } - configurations.put(address, config); + if (thread != null) { + if (thread.isConnectionHealthy()) { + Log.d(TAG, "Connection is active for " + address + ", ignoring retry"); + return; + } + + if (isThreadActive(thread)) { + Log.d(TAG, "Thread active but connection unhealthy, triggering retry"); + thread.retryConnection(); + } else { + Log.d(TAG, "Thread not active, starting new connection"); + connections.remove(address); + startConnection(config); + } + } - if (!btAdapter.isEnabled()) { - Log.w(TAG, "Bluetooth adapter not available or disabled, deferring connection"); return; } + configurations.put(address, config); startConnection(config); + } public void removeConfig(ConnectionConfiguration config) { + if (isShutdown) { + return; + } + validateConfig(config); String address = config.address; @@ -139,6 +161,10 @@ public void removeConfig(ConnectionConfiguration config) { } private void startConnection(ConnectionConfiguration config) { + if (isShutdown) { + return; + } + String address = config.address; if (connections.containsKey(address)) { @@ -146,6 +172,11 @@ private void startConnection(ConnectionConfiguration config) { return; } + if (btAdapter == null || !btAdapter.isEnabled()) { + Log.w(TAG, "Bluetooth not available, deferring connection for " + address); + return; + } + Log.d(TAG, "Starting Bluetooth connection for " + address); BluetoothConnectionThread thread = new BluetoothConnectionThread(context, config, btAdapter, wearableImpl); connections.put(address, thread); @@ -187,6 +218,9 @@ private void onBtAdapterStateChaged(int state) { } public void retryConnection(ConnectionConfiguration config, boolean immediate) { + if (isShutdown) { + return; + } validateConfig(config); String address = config.address; @@ -195,13 +229,36 @@ public void retryConnection(ConnectionConfiguration config, boolean immediate) { return; } + if (btAdapter == null || !btAdapter.isEnabled()) { + Log.w(TAG, "Bluetooth not enabled, cannot retry connection"); + return; + } + BluetoothConnectionThread thread = connections.get(address); - if (thread != null && btAdapter != null && btAdapter.isEnabled()) { - if (immediate) - thread.retryConnection(); - else - thread.scheduleRetry(); + + if (thread == null) { + Log.d(TAG, "No connection thread exists for " + address + ", starting new one"); + startConnection(config); + return; + } + + if (!isThreadActive(thread)) { + Log.d(TAG, "Connection thread not active for " + address + ", starting new one"); + connections.remove(address); + startConnection(config); + return; + } + + if (immediate) { + thread.retryConnection(); + } else { + thread.scheduleRetry(); } + + } + + private boolean isThreadActive(BluetoothConnectionThread thread) { + return thread != null && thread.isAlive() && !thread.isInterrupted(); } private static void validateConfig(ConnectionConfiguration config){ @@ -218,23 +275,42 @@ private static void validateConfig(ConnectionConfiguration config){ @Override public void close() { + if (isShutdown) { + return; + } + + isShutdown = true; + + for (BluetoothConnectionThread thread : connections.values()) { + thread.close(); + } + + for (BluetoothConnectionThread thread : connections.values()) { + try { + thread.join(5000); + if (thread.isAlive()) { + Log.w(TAG, "Thread did not stop in time: " + thread.getName()); + } + } catch (InterruptedException e) { + Log.w(TAG, "Interrupted while waiting for thread to finish", e); + } + } + + connections.clear(); + configurations.clear(); + try { context.unregisterReceiver(btStateReceiver); } catch (Exception e) { - Log.w(TAG, "close BT: Error"); + Log.w(TAG, "Error unregistering btStateReceiver", e); } try { context.unregisterReceiver(aclConnReceiver); } catch (Exception e) { - Log.w(TAG, "close ACL: Error"); + Log.w(TAG, "Error unregistering aclConnReceiver", e); } - for (BluetoothConnectionThread thread: connections.values()) { - thread.close(); - } - - connections.clear(); - configurations.clear(); + Log.d(TAG, "BluetoothClient closed"); } } diff --git a/play-services-wearable/core/src/main/java/org/microg/gms/wearable/bluetooth/BluetoothConnectionThread.java b/play-services-wearable/core/src/main/java/org/microg/gms/wearable/bluetooth/BluetoothConnectionThread.java index 3b2c3a9a20..6407d6e333 100644 --- a/play-services-wearable/core/src/main/java/org/microg/gms/wearable/bluetooth/BluetoothConnectionThread.java +++ b/play-services-wearable/core/src/main/java/org/microg/gms/wearable/bluetooth/BluetoothConnectionThread.java @@ -5,9 +5,7 @@ import android.bluetooth.BluetoothDevice; import android.bluetooth.BluetoothSocket; import android.content.Context; -import android.os.Build; -import android.os.Handler; -import android.os.Looper; +import android.os.PowerManager; import android.util.Log; import androidx.annotation.RequiresPermission; @@ -34,13 +32,15 @@ public class BluetoothConnectionThread extends Thread implements Closeable { private static final int MAX_RETRY_DELAY_MS = 60000; private static final int MIN_RETRY_DELAY_MS = 1000; private static final int BACKOFF_MULTIPLIEER = 2; - private static final int MAX_CONSECUTIVE_FAILURES = 5; private static final long MIN_ATTEMPT_INTERVAL_MS = 3000; + private volatile boolean isConnected = false; + private volatile long lastActivityTime = 0; + private static final long ACTIVITY_TIMEOUT_MS = 5000; + private final Context context; private final ConnectionConfiguration config; private final BluetoothAdapter btAdapter; - private final Handler retryHandler; private final AtomicBoolean running = new AtomicBoolean(true); private final AtomicInteger retryCount = new AtomicInteger(0); @@ -53,13 +53,33 @@ public class BluetoothConnectionThread extends Thread implements Closeable { private final WearableImpl wearableImpl; + private final PowerManager.WakeLock wakeLock; + private static final long SOCKET_CONNECT_TIMEOUT_MS = 30000; + public BluetoothConnectionThread(Context context, ConnectionConfiguration config, BluetoothAdapter btAdapter, WearableImpl wearableImpl) { super("BtThread-" + config.address); this.context = context; this.config = config; this.btAdapter = btAdapter; this.wearableImpl = wearableImpl; - this.retryHandler = new Handler(Looper.getMainLooper()); + PowerManager pm = (PowerManager) context.getSystemService(Context.POWER_SERVICE); + wakeLock = pm.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, + "GmsWear:BtConnect:" + config.address); + wakeLock.setReferenceCounted(false); + + } + + public boolean isConnectionHealthy(){ + if (!isConnected || wearableConnection == null) { + return false; + } + + long timeSinceActivity = System.currentTimeMillis() - lastActivityTime; + return isAlive() && !isInterrupted() && timeSinceActivity < ACTIVITY_TIMEOUT_MS; + } + + private void markActivity() { + lastActivityTime = System.currentTimeMillis(); } @RequiresPermission(allOf = {Manifest.permission.BLUETOOTH_CONNECT, Manifest.permission.BLUETOOTH_SCAN, Manifest.permission.BLUETOOTH_CONNECT}) @@ -72,26 +92,64 @@ public void run(){ if (!running.get()) break; + try { + if (!wakeLock.isHeld()) { + wakeLock.acquire(5 * 60 * 1000L); + Log.d(TAG, "Wake lock acquired for connection attempt"); + } + } catch (Exception e) { + Log.w(TAG, "Failed to acquire wake lock", e); + } + try { connect(); + retryCount.incrementAndGet(); } catch (IOException e) { Log.w(TAG, "Connection failed for " + config.address + ": " + e.getMessage()); + retryCount.incrementAndGet(); + } catch (InterruptedException e) { + Log.d(TAG, "Connection thread interrupted"); + if (!running.get()) { + break; + } + } catch (Exception e) { + Log.e(TAG, "Unexpected error in connection loop", e); + retryCount.incrementAndGet(); + } finally { closeSocket(); - if (running.get()) { - try { - waitForRetry(); - } catch (InterruptedException ex) { - throw new RuntimeException(ex); + try { + if (wakeLock.isHeld()) { + wakeLock.release(); + Log.d(TAG, "Wake lock released"); + } + } catch (Exception e) { + Log.w(TAG, "Failed to release wake lock", e); + } + } + + if (running.get() && !isInterrupted()) { + try { + waitForRetry(); + } catch (InterruptedException e) { + Log.d(TAG, "Retry wait interrupted"); + if (!running.get()) { + break; } } - } catch (InterruptedException e) { - Log.d(TAG, "Connection thread interrupted"); - break; } } closeSocket(); + + try { + if (wakeLock.isHeld()) { + wakeLock.release(); + } + } catch (Exception e) { + Log.w(TAG, "Failed to release wake lock in cleanup", e); + } + Log.d(TAG, "Bluetooth connection thread stopped for " + config.address); } @@ -137,18 +195,109 @@ private void connect() throws IOException, InterruptedException { if (btAdapter.isDiscovering()) btAdapter.cancelDiscovery(); - socket.connect(); + connectSocketWithTimeout(socket); Log.d(TAG, "Socket connected to " + config.address); retryCount.set(0); + isConnected = true; + markActivity(); - wearableConnection = new BluetoothWearableConnection(socket, config.nodeId, new ConnectionListener(context, config, wearableImpl)); + wearableConnection = new BluetoothWearableConnection(socket, config.nodeId, new ConnectionListener(context, config, wearableImpl, this)); wearableConnection.run(); + } finally { + isConnected = false; WearableImpl.BluetoothConnectionLock.release(config.address, "BTLock"); } } + private void connectSocketWithTimeout(BluetoothSocket socket) throws IOException, InterruptedException { + final AtomicBoolean connected = new AtomicBoolean(false); + final AtomicBoolean timedOut = new AtomicBoolean(false); + final AtomicBoolean connectFailed = new AtomicBoolean(false); + final Object lock = new Object(); + final IOException[] exception = new IOException[1]; + + Thread connectThread = new Thread(new Runnable() { + @RequiresPermission(Manifest.permission.BLUETOOTH_CONNECT) + @Override + public void run() { + try { + synchronized (lock) { + if (timedOut.get()) { + Log.w(TAG, "Connect aborted - already timed out"); + return; + } + } + + socket.connect(); + + synchronized (lock) { + if (!timedOut.get()) { + connected.set(true); + Log.d(TAG, "Socket connect succeeded"); + } else { + Log.w(TAG, "Socket connect succeeded but timeout already occurred"); + try { + socket.close(); + } catch (IOException e) { + Log.w(TAG, "Failed to close socket after timeout", e); + } + } + } + } catch (IOException e) { + synchronized (lock) { + if (!timedOut.get()) { + exception[0] = e; + connectFailed.set(true); + } + } + } + } + }, "BtSocketConnect-" + config.address); + + connectThread.start(); + + long startTime = System.currentTimeMillis(); + long endTime = startTime + SOCKET_CONNECT_TIMEOUT_MS; + + while (System.currentTimeMillis() < endTime && running.get()) { + synchronized (lock) { + if (connected.get()) { + return; + } + + if (connectFailed.get()) { + throw exception[0]; + } + } + + try { + Thread.sleep(100); + } catch (InterruptedException e) { + connectThread.interrupt(); + throw e; + } + } + + synchronized (lock) { + if (!connected.get()) { + timedOut.set(true); + Log.e(TAG, "Socket connect timed out after " + SOCKET_CONNECT_TIMEOUT_MS + "ms"); + + try { + socket.close(); + } catch (IOException e) { + Log.w(TAG, "Failed to close socket after timeout", e); + } + + connectThread.interrupt(); + throw new IOException("Socket connect timed out after " + SOCKET_CONNECT_TIMEOUT_MS + "ms"); + } + } + } + + private void waitForRetry() throws InterruptedException { if (!running.get()) return; @@ -218,6 +367,7 @@ public void scheduleRetry() { } private void closeSocket() { + isConnected = false; if (socket != null) { try { socket.close(); @@ -252,12 +402,15 @@ private static class ConnectionListener implements WearableConnection.Listener { private Connect peerConnect; private WearableConnection connection; + private final BluetoothConnectionThread thread; + private MessageHandler messageHandler; - public ConnectionListener(Context context, ConnectionConfiguration config, WearableImpl wearableImpl) { + public ConnectionListener(Context context, ConnectionConfiguration config, WearableImpl wearableImpl, BluetoothConnectionThread thread) { this.context = context; this.config = config; this.wearableImpl = wearableImpl; + this.thread = thread; } @Override @@ -271,12 +424,14 @@ public void onConnected(WearableConnection connection) { this.messageHandler = new MessageHandler(context, wearableImpl, config); + thread.markActivity(); wearableImpl.onConnectReceived(connection, config.nodeId, peerConnect); } @Override public void onMessage(WearableConnection connection, RootMessage message) { Log.d(TAG, "Message received from " + config.address + ": " + message.toString()); + thread.markActivity(); if (peerConnect != null && messageHandler != null) messageHandler.handleMessage(connection, peerConnect.id, message); } diff --git a/play-services-wearable/core/src/main/java/org/microg/gms/wearable/bluetooth/BluetoothWearableConnection.java b/play-services-wearable/core/src/main/java/org/microg/gms/wearable/bluetooth/BluetoothWearableConnection.java index 44bdd7a303..449b90a24f 100644 --- a/play-services-wearable/core/src/main/java/org/microg/gms/wearable/bluetooth/BluetoothWearableConnection.java +++ b/play-services-wearable/core/src/main/java/org/microg/gms/wearable/bluetooth/BluetoothWearableConnection.java @@ -6,6 +6,8 @@ package org.microg.gms.wearable.bluetooth; import android.bluetooth.BluetoothSocket; +import android.os.Handler; +import android.os.Looper; import android.util.Log; import org.microg.gms.profile.Build; @@ -16,11 +18,14 @@ import java.io.DataInputStream; import java.io.DataOutputStream; +import java.io.EOFException; import java.io.IOException; +import java.nio.ByteBuffer; +import java.util.concurrent.atomic.AtomicBoolean; public class BluetoothWearableConnection extends WearableConnection { private static final String TAG = "BtWearableConnection"; - private final int MAX_PIECE_SIZE = 20 * 1024 * 1024; + private final int MAX_PIECE_SIZE = 64 * 1024 * 1024; private final BluetoothSocket socket; private final DataInputStream is; private final DataOutputStream os; @@ -31,6 +36,13 @@ public class BluetoothWearableConnection extends WearableConnection { private boolean handshakeComplete = false; private Connect peerConnect; + private final AtomicBoolean isClosed = new AtomicBoolean(false); + private volatile Thread readerThread; + + private final Handler watchdogHandler; + private static final long READ_TIMEOUT_MS = 60000; + private static final long HANDSHAKE_TIMEOUT_MS = 30000; + public BluetoothWearableConnection(BluetoothSocket socket, String localNodeId, Listener listener) throws IOException { super(listener); this.socket = socket; @@ -38,6 +50,7 @@ public BluetoothWearableConnection(BluetoothSocket socket, String localNodeId, L this.os = new DataOutputStream(socket.getOutputStream()); this.localNodeId = localNodeId; this.listener = listener; + this.watchdogHandler = new Handler(Looper.getMainLooper()); if (localNodeId == null) { throw new IllegalArgumentException("localNodeId cannot be null"); @@ -45,9 +58,25 @@ public BluetoothWearableConnection(BluetoothSocket socket, String localNodeId, L } private boolean handshake() { - try { - Log.d(TAG, "Starting handshake, local node ID: " + localNodeId); + Log.d(TAG, "Starting handshake, local node ID: " + localNodeId); + + final AtomicBoolean timedOut = new AtomicBoolean(false); + + Runnable timeoutWatchdog = () -> { + if (!handshakeComplete) { + Log.e(TAG, "Handshake timeout after " + HANDSHAKE_TIMEOUT_MS + "ms - forcing close"); + timedOut.set(true); + try { + socket.close(); + } catch (IOException e) { + Log.w(TAG, "Error closing socket on timeout", e); + } + } + }; + + watchdogHandler.postDelayed(timeoutWatchdog, HANDSHAKE_TIMEOUT_MS); + try { Connect connectMessage = new Connect.Builder() .id(localNodeId) .name(Build.MODEL) @@ -62,8 +91,22 @@ private boolean handshake() { writeMessage(outgoingMessage); Log.d(TAG, "Sent Connect message with node ID: " + localNodeId); + if (isClosed.get() || timedOut.get()) { + Log.w(TAG, "Connection closed before receiving handshake response"); + return false; + } + RootMessage incomingMessage = readMessage(); - Log.d(TAG, "Received message type: " + incomingMessage); + + if (incomingMessage == null) { + Log.e(TAG, "Received null message during handshake"); + return false; + } + + if (timedOut.get()) { + Log.e(TAG, "Handshake completed but timeout already triggered"); + return false; + } if (incomingMessage.connect == null) { Log.e(TAG, "Expected Connect message but received: " + incomingMessage); @@ -79,13 +122,18 @@ private boolean handshake() { } Log.d(TAG, "Handshake successful! Peer node ID: " + peerNodeId); - Log.d(TAG, "Connect message details: " + incomingMessage.connect); - handshakeComplete = true; return true; + } catch (IOException e) { - Log.e(TAG, "Handshake failed", e); + if (timedOut.get()) { + Log.e(TAG, "Handshake failed due to timeout", e); + } else { + Log.e(TAG, "Handshake failed", e); + } return false; + } finally { + watchdogHandler.removeCallbacks(timeoutWatchdog); } } @@ -102,27 +150,28 @@ public boolean isHandshakeComplete() { } protected void writeMessagePiece(MessagePiece piece) throws IOException { - if (socket == null) { - throw new IOException("Socket is null"); + if (isClosed.get()) { + throw new IOException("Socket not connected"); } - if (!socket.isConnected()) { - throw new IOException("Socket is not connected"); - } + byte[] bytes = MessagePiece.ADAPTER.encode(piece); - try { - byte[] bytes = MessagePiece.ADAPTER.encode(piece); - os.writeInt(bytes.length); - os.write(bytes); - os.flush(); - } catch (IOException e) { - throw new IOException("Failed to write message piece (size: " + - (piece.data != null ? piece.data.size() : 0) + " bytes): " + e.getMessage(), e); + synchronized (os) { + try { + os.writeInt(bytes.length); + os.write(bytes); + os.flush(); + } catch (IOException e) { + Log.e(TAG, "Write failed, socket may be closed", e); + throw e; + } } } @Override public void run() { + readerThread = Thread.currentThread(); + try { // Perform handshake first if (!handshake()) { @@ -139,37 +188,115 @@ public void run() { } catch (Exception e) { Log.e(TAG, "Error in connection run loop", e); + } finally { + readerThread = null; } } protected MessagePiece readMessagePiece() throws IOException { - int len = is.readInt(); - if (len > MAX_PIECE_SIZE) { - throw new IOException("Piece size " + len + " exceeded limit of " + MAX_PIECE_SIZE + " bytes."); + if (isClosed.get()) { + throw new IOException("Socket not connected"); + } + + final AtomicBoolean timedOut = new AtomicBoolean(false); + final Thread currentThread = Thread.currentThread(); + + Runnable readWatchdog = () -> { + Log.e(TAG, "Read operation timed out after " + READ_TIMEOUT_MS + "ms"); + timedOut.set(true); + try { + socket.close(); + } catch (IOException e) { + Log.w(TAG, "Error closing socket on read timeout", e); + } + currentThread.interrupt(); + }; + + watchdogHandler.postDelayed(readWatchdog, READ_TIMEOUT_MS); + + try { + int len = is.readInt(); + + if (len <= 0 || len > MAX_PIECE_SIZE) { + throw new IOException("Invalid piece length: " + len); + } + + byte[] bytes = new byte[len]; + + try { + is.readFully(bytes); + } catch (EOFException e) { + throw new IOException("Socket closed by peer while reading data", e); + } + + return MessagePiece.ADAPTER.decode(bytes); + } catch (IOException e) { + if (isClosed.get()) { + throw new IOException("Connection closed during read", e); + } + + String msg = e.getMessage(); + if (msg != null && msg.contains("bt socket closed")) { + Log.d(TAG, "Bluetooth socket closed during read"); + isClosed.set(true); + } + + throw new IOException("Connection closed by peer", e); } - System.out.println("Reading piece of length " + len); - byte[] bytes = new byte[len]; - is.readFully(bytes); -// return wire.parseFrom(bytes, MessagePiece.class); - return MessagePiece.ADAPTER.decode(bytes); } @Override public void close() throws IOException { + if (isClosed.getAndSet(true)) { + Log.d(TAG, "Connection already closed"); + return; + } + + Log.d(TAG, "Closing Bluetooth wearable connection"); + + Thread reader = readerThread; + if (reader != null && reader != Thread.currentThread()) { + reader.interrupt(); + } + + IOException exception = null; + + try { + if (is != null) { + is.close(); + } + } catch (IOException e) { + Log.w(TAG, "Error closing input stream", e); + exception = e; + } + try { - if (is != null) is.close(); + if (os != null) { + os.close(); + } } catch (IOException e) { - // Ignore + Log.w(TAG, "Error closing output stream", e); + if (exception == null) exception = e; } + try { - if (os != null) os.close(); + socket.close(); } catch (IOException e) { - // Ignore + Log.w(TAG, "Error closing socket", e); + if (exception == null) exception = e; + } + + if (exception != null) { + throw exception; } - socket.close(); } public Connect getPeerConnect() { return peerConnect; } + + public boolean isClosed() { + return isClosed.get(); + } + } \ No newline at end of file diff --git a/play-services-wearable/core/src/main/java/org/microg/gms/wearable/channel/ChannelManager.java b/play-services-wearable/core/src/main/java/org/microg/gms/wearable/channel/ChannelManager.java index 376ae4ee20..34bf01ce6c 100644 --- a/play-services-wearable/core/src/main/java/org/microg/gms/wearable/channel/ChannelManager.java +++ b/play-services-wearable/core/src/main/java/org/microg/gms/wearable/channel/ChannelManager.java @@ -53,6 +53,8 @@ public class ChannelManager { private ChannelCallbacks channelCallbacks; + private volatile long cooldownUntil = 0; + public ChannelManager(Handler handler, WearableImpl wearable, String localNodeId) { this.handler = handler; this.wearable = wearable; @@ -60,6 +62,21 @@ public ChannelManager(Handler handler, WearableImpl wearable, String localNodeId this.random = new Random(); } + public void setOperationCooldown(long durationMs) { + cooldownUntil = System.currentTimeMillis() + durationMs; + Log.d(TAG, "Operation cooldown set for " + durationMs + "ms"); + } + + private boolean isInCooldown() { + long now = System.currentTimeMillis(); + if (now < cooldownUntil) { + long remaining = cooldownUntil - now; + Log.d(TAG, "In cooldown period, " + remaining + "ms remaining"); + return true; + } + return false; + } + public void start() { isRunning.set(true); Log.d(TAG, "ChannelManager started, localNodeId=" + localNodeId); @@ -95,10 +112,25 @@ public void openChannel(AppKey appKey, String nodeId, String path, OpenChannelCa return; } + if (isInCooldown()) { + long delay = cooldownUntil - System.currentTimeMillis() + 100; + Log.d(TAG, "Deferring channel open by " + delay + "ms due to cooldown"); + + handler.postDelayed(() -> doOpenChannel(appKey, nodeId, path, callback), delay); + return; + } + handler.post(() -> doOpenChannel(appKey, nodeId, path, callback)); } private void doOpenChannel(AppKey appKey, String nodeId, String path, OpenChannelCallback callback) { + if (isInCooldown()) { + long delay = cooldownUntil - System.currentTimeMillis() + 100; + Log.d(TAG, "Cooldown detected in doOpenChannel, deferring by " + delay + "ms"); + handler.postDelayed(() -> doOpenChannel(appKey, nodeId, path, callback), delay); + return; + } + try { WearableConnection connection = wearable.getActiveConnections().get(nodeId); if (connection == null) { @@ -133,7 +165,7 @@ private void doOpenChannel(AppKey appKey, String nodeId, String path, OpenChanne .fromChannelOperator(true) .packageName(appKey.packageName) .signatureDigest(appKey.signatureDigest) - .path(path) +// .path(path) .build(); ChannelRequest channelRequest = new ChannelRequest.Builder() @@ -141,7 +173,7 @@ private void doOpenChannel(AppKey appKey, String nodeId, String path, OpenChanne .version(1) .origin(0) .build(); - + Log.d(TAG, "ChannelRequest: " + channelRequest); int requestId = requestIdCounter.getAndIncrement(); int generation = generationCounter.get(); @@ -159,6 +191,7 @@ private void doOpenChannel(AppKey appKey, String nodeId, String path, OpenChanne RootMessage message = new RootMessage.Builder() .channelRequest(request) .build(); + Log.d(TAG, "RootMessage: " + message); try { connection.writeMessage(message); diff --git a/play-services-wearable/core/src/main/proto/wearable.proto b/play-services-wearable/core/src/main/proto/wearable.proto index ff04c8def2..af7f9f6f5f 100644 --- a/play-services-wearable/core/src/main/proto/wearable.proto +++ b/play-services-wearable/core/src/main/proto/wearable.proto @@ -20,7 +20,9 @@ message AppKeys { } message Asset { - // TODO + // cannot find what other fields is + // maybe deprecated and + // not used anymore in new google gms optional string digest = 4; } From a261776f598f95fd5db7bb44f76d7e82210f196f Mon Sep 17 00:00:00 2001 From: deadYokai Date: Tue, 13 Jan 2026 13:47:13 +0200 Subject: [PATCH 13/29] Node & Assets db update --- .../microg/gms/wearable/MessageHandler.java | 67 +++- .../gms/wearable/NodeDatabaseHelper.java | 361 +++++++++++++----- .../bluetooth/BluetoothConnectionThread.java | 6 +- .../BluetoothWearableConnection.java | 3 + .../gms/wearable/channel/ChannelManager.java | 2 +- 5 files changed, 335 insertions(+), 104 deletions(-) diff --git a/play-services-wearable/core/src/main/java/org/microg/gms/wearable/MessageHandler.java b/play-services-wearable/core/src/main/java/org/microg/gms/wearable/MessageHandler.java index fb1d3129d0..9c77baf6a1 100644 --- a/play-services-wearable/core/src/main/java/org/microg/gms/wearable/MessageHandler.java +++ b/play-services-wearable/core/src/main/java/org/microg/gms/wearable/MessageHandler.java @@ -236,16 +236,32 @@ private void handleSetAsset(WearableConnection connection, String sourceNodeId, SetAsset setAsset, Boolean hasAsset) { Log.d(TAG, "handleSetAsset: digest=" + setAsset.digest + ", hasAsset=" + hasAsset); - if (setAsset.appkeys != null) { + if (setAsset.appkeys != null && setAsset.appkeys.appKeys != null && + !setAsset.appkeys.appKeys.isEmpty()) { for (AppKey appKey : setAsset.appkeys.appKeys) { - wearable.getNodeDatabase().allowAssetAccess(setAsset.digest, appKey.packageName, appKey.signatureDigest); + wearable.getNodeDatabase().allowAssetAccess( + setAsset.digest, + appKey.packageName, + appKey.signatureDigest + ); } } - if (hasAsset != null && !hasAsset) { - handleFetchAsset(connection, sourceNodeId, new FetchAsset.Builder() - .assetName(setAsset.digest) - .build()); + boolean assetExistsLocally = wearable.assetFileExists(setAsset.digest); + + if (assetExistsLocally) { + wearable.getNodeDatabase().markAssetAsPresent(setAsset.digest); + Log.d(TAG, "Asset already present locally: " + setAsset.digest); + } else { + if (setAsset.appkeys != null && setAsset.appkeys.appKeys != null && + !setAsset.appkeys.appKeys.isEmpty()) { + AppKey firstKey = setAsset.appkeys.appKeys.get(0); + wearable.getNodeDatabase().markAssetAsMissing( + setAsset.digest, + firstKey.packageName, + firstKey.signatureDigest + ); + } } } @@ -444,9 +460,42 @@ public void handleFilePiece(WearableConnection connection, String fileName, byte Log.w(TAG, "Failed to send asset ACK", e); } - Log.d(TAG, "Asset " + digest + " marked as present. " + - "Data change notifications will be handled by database layer."); - + synchronized (wearable.getNodeDatabase()) { + wearable.getNodeDatabase().markAssetAsPresent(digest); + + Cursor cursor = wearable.getNodeDatabase().getDataItemsWaitingForAsset(digest); + if (cursor != null) { + try { + while (cursor.moveToNext()) { + DataItemRecord record = DataItemRecord.fromCursor(cursor); + + boolean allPresent = true; + for (Asset asset : record.dataItem.getAssets().values()) { + if (!wearable.assetFileExists(asset.getDigest())) { + allPresent = false; + break; + } + } + + if (allPresent && !record.assetsAreReady) { + Log.d(TAG, "All assets now ready for: " + record.dataItem.uri); + + record.assetsAreReady = true; + wearable.getNodeDatabase().updateAssetsReady( + record.dataItem.uri.toString(), true); + + Intent intent = new Intent("com.google.android.gms.wearable.DATA_CHANGED"); + intent.setPackage(record.packageName); + intent.setData(record.dataItem.uri); + wearable.invokeListeners(intent, + listener -> listener.onDataChanged(record.toEventDataHolder())); + } + } + } finally { + cursor.close(); + } + } + } } catch (IOException e) { Log.w(TAG, "Error processing final file piece", e); file.delete(); diff --git a/play-services-wearable/core/src/main/java/org/microg/gms/wearable/NodeDatabaseHelper.java b/play-services-wearable/core/src/main/java/org/microg/gms/wearable/NodeDatabaseHelper.java index cee6b0f136..d831bc71a8 100644 --- a/play-services-wearable/core/src/main/java/org/microg/gms/wearable/NodeDatabaseHelper.java +++ b/play-services-wearable/core/src/main/java/org/microg/gms/wearable/NodeDatabaseHelper.java @@ -36,7 +36,7 @@ public class NodeDatabaseHelper extends SQLiteOpenHelper { private static final String DB_NAME = "node.db"; private static final String[] GDIBHAP_FIELDS = new String[]{"dataitems_id", "packageName", "signatureDigest", "host", "path", "seqId", "deleted", "sourceNode", "data", "timestampMs", "assetsPresent", "assetname", "assets_digest", "v1SourceNode", "v1SeqId"}; - private static final int VERSION = 9; + private static final int VERSION = 14; private ClockworkNodePreferences clockworkNodePreferences; @@ -47,23 +47,179 @@ public NodeDatabaseHelper(Context context) { @Override public void onCreate(SQLiteDatabase db) { - db.execSQL("CREATE TABLE appkeys(_id INTEGER PRIMARY KEY AUTOINCREMENT,packageName TEXT NOT NULL,signatureDigest TEXT NOT NULL);"); - db.execSQL("CREATE TABLE dataitems(_id INTEGER PRIMARY KEY AUTOINCREMENT, appkeys_id INTEGER NOT NULL REFERENCES appkeys(_id), host TEXT NOT NULL, path TEXT NOT NULL, seqId INTEGER NOT NULL, deleted INTEGER NOT NULL, sourceNode TEXT NOT NULL, data BLOB, timestampMs INTEGER NOT NULL, assetsPresent INTEGER NOT NULL, v1SourceNode TEXT NOT NULL, v1SeqId INTEGER NOT NULL);"); - db.execSQL("CREATE TABLE assets(digest TEXT PRIMARY KEY, dataPresent INTEGER NOT NULL DEFAULT 0, timestampMs INTEGER NOT NULL);"); - db.execSQL("CREATE TABLE assetrefs(assetname TEXT NOT NULL, dataitems_id INTEGER NOT NULL REFERENCES dataitems(_id), assets_digest TEXT NOT NULL REFERENCES assets(digest));"); - db.execSQL("CREATE TABLE assetsacls(appkeys_id INTEGER NOT NULL REFERENCES appkeys(_id), assets_digest TEXT NOT NULL);"); - db.execSQL("CREATE TABLE nodeinfo(node TEXT NOT NULL PRIMARY KEY, seqId INTEGER, lastActivityMs INTEGER);"); - db.execSQL("CREATE VIEW appKeyDataItems AS SELECT appkeys._id AS appkeys_id, appkeys.packageName AS packageName, appkeys.signatureDigest AS signatureDigest, dataitems._id AS dataitems_id, dataitems.host AS host, dataitems.path AS path, dataitems.seqId AS seqId, dataitems.deleted AS deleted, dataitems.sourceNode AS sourceNode, dataitems.data AS data, dataitems.timestampMs AS timestampMs, dataitems.assetsPresent AS assetsPresent, dataitems.v1SourceNode AS v1SourceNode, dataitems.v1SeqId AS v1SeqId FROM appkeys, dataitems WHERE appkeys._id=dataitems.appkeys_id"); - db.execSQL("CREATE VIEW appKeyAcls AS SELECT appkeys._id AS appkeys_id, appkeys.packageName AS packageName, appkeys.signatureDigest AS signatureDigest, assetsacls.assets_digest AS assets_digest FROM appkeys, assetsacls WHERE _id=appkeys_id"); - db.execSQL("CREATE VIEW dataItemsAndAssets AS SELECT appKeyDataItems.packageName AS packageName, appKeyDataItems.signatureDigest AS signatureDigest, appKeyDataItems.dataitems_id AS dataitems_id, appKeyDataItems.host AS host, appKeyDataItems.path AS path, appKeyDataItems.seqId AS seqId, appKeyDataItems.deleted AS deleted, appKeyDataItems.sourceNode AS sourceNode, appKeyDataItems.data AS data, appKeyDataItems.timestampMs AS timestampMs, appKeyDataItems.assetsPresent AS assetsPresent, assetrefs.assetname AS assetname, assetrefs.assets_digest AS assets_digest, appKeyDataItems.v1SourceNode AS v1SourceNode, appKeyDataItems.v1SeqId AS v1SeqId FROM appKeyDataItems LEFT OUTER JOIN assetrefs ON appKeyDataItems.dataitems_id=assetrefs.dataitems_id"); - db.execSQL("CREATE VIEW assetsReadyStatus AS SELECT dataitems_id AS dataitems_id, COUNT(*) = SUM(dataPresent) AS nowReady, assetsPresent AS markedReady FROM assetrefs, dataitems LEFT OUTER JOIN assets ON assetrefs.assets_digest = assets.digest WHERE assetrefs.dataitems_id=dataitems._id GROUP BY dataitems_id;"); - db.execSQL("CREATE UNIQUE INDEX appkeys_NAME_AND_SIG ON appkeys(packageName,signatureDigest);"); - db.execSQL("CREATE UNIQUE INDEX assetrefs_ASSET_REFS ON assetrefs(assets_digest,dataitems_id,assetname);"); + db.execSQL("CREATE TABLE appkeys(" + + "_id INTEGER PRIMARY KEY AUTOINCREMENT, " + + "packageName TEXT NOT NULL, " + + "signatureDigest TEXT NOT NULL);"); + + db.execSQL("CREATE TABLE dataitems(" + + "_id INTEGER PRIMARY KEY AUTOINCREMENT," + + "appkeys_id INTEGER NOT NULL REFERENCES appkeys(_id)," + + "host TEXT NOT NULL, " + + "path TEXT NOT NULL, " + + "seqId INTEGER NOT NULL, " + + "deleted INTEGER NOT NULL, " + + "sourceNode TEXT NOT NULL, " + + "data BLOB," + + "timestampMs INTEGER NOT NULL, " + + "assetsPresent INTEGER NOT NULL, " + + "v1SourceNode TEXT NOT NULL, " + + "v1SeqId INTEGER NOT NULL);"); + + db.execSQL("CREATE TABLE archiveDataItems(" + + "_id INTEGER PRIMARY KEY AUTOINCREMENT, " + + "migratingNode TEXT NOT NULL, " + + "appkeys_id INTEGER NOT NULL REFERENCES appkeys(_id), " + + "path TEXT NOT NULL, " + + "data BLOB, " + + "timestampMs INTEGER NOT NULL, " + + "assetsPresent INTEGER NOT NULL);"); + + db.execSQL("CREATE TABLE assets(" + + "digest TEXT PRIMARY KEY, " + + "dataPresent INTEGER NOT NULL DEFAULT 0, " + + "timestampMs INTEGER NOT NULL);"); + + db.execSQL("CREATE TABLE assetrefs(" + + "assetname TEXT NOT NULL, " + + "dataitems_id INTEGER NOT NULL REFERENCES dataitems(_id), " + + "assets_digest TEXT NOT NULL REFERENCES assets(digest));"); + + db.execSQL("CREATE TABLE archiveAssetRefs(" + + "assetname TEXT NOT NULL, " + + "archiveDataItems_id INTEGER NOT NULL REFERENCES dataitems(_id), " + + "assets_digest TEXT NOT NULL REFERENCES assets(digest));"); + + db.execSQL("CREATE TABLE assetsacls(" + + "appkeys_id INTEGER NOT NULL REFERENCES appkeys(_id), " + + "assets_digest TEXT NOT NULL);"); + + db.execSQL("CREATE TABLE nodeinfo(" + + "node TEXT NOT NULL PRIMARY KEY, " + + "seqId INTEGER, " + + "lastActivityMs INTEGER, " + + "migratingFrom TEXT DEFAULT NULL, " + + "enrollmentId TEXT DEFAULT NULL);"); + + db.execSQL("CREATE VIEW appKeyDataItems AS SELECT " + + "appkeys._id AS appkeys_id, " + + "appkeys.packageName AS packageName, " + + "appkeys.signatureDigest AS signatureDigest, " + + "dataitems._id AS dataitems_id, " + + "dataitems.host AS host, " + + "dataitems.path AS path, " + + "dataitems.seqId AS seqId, " + + "dataitems.deleted AS deleted, " + + "dataitems.sourceNode AS sourceNode, " + + "dataitems.data AS data, " + + "dataitems.timestampMs AS timestampMs, " + + "dataitems.assetsPresent AS assetsPresent, " + + "dataitems.v1SourceNode AS v1SourceNode, " + + "dataitems.v1SeqId AS v1SeqId " + + "FROM appkeys, dataitems " + + "WHERE appkeys._id=dataitems.appkeys_id"); + + db.execSQL("CREATE VIEW appKeyAcls AS SELECT " + + "appkeys._id AS appkeys_id, " + + "appkeys.packageName AS packageName, " + + "appkeys.signatureDigest AS signatureDigest, " + + "assetsacls.assets_digest AS assets_digest " + + "FROM appkeys, assetsacls " + + "WHERE _id=appkeys_id"); + + db.execSQL("CREATE VIEW dataItemsAndAssets AS SELECT " + + "appKeyDataItems.packageName AS packageName, " + + "appKeyDataItems.signatureDigest AS signatureDigest, " + + "appKeyDataItems.dataitems_id AS dataitems_id, " + + "appKeyDataItems.host AS host, " + + "appKeyDataItems.path AS path, " + + "appKeyDataItems.seqId AS seqId, " + + "appKeyDataItems.deleted AS deleted, " + + "appKeyDataItems.sourceNode AS sourceNode, " + + "appKeyDataItems.data AS data, " + + "appKeyDataItems.timestampMs AS timestampMs, " + + "appKeyDataItems.assetsPresent AS assetsPresent, " + + "assetrefs.assetname AS assetname, " + + "assetrefs.assets_digest AS assets_digest, " + + "appKeyDataItems.v1SourceNode AS v1SourceNode, " + + "appKeyDataItems.v1SeqId AS v1SeqId " + + "FROM appKeyDataItems " + + "LEFT OUTER JOIN assetrefs ON appKeyDataItems.dataitems_id=assetrefs.dataitems_id"); + + db.execSQL("CREATE VIEW assetsReadyStatus AS SELECT " + + "dataitems_id AS dataitems_id, " + + "COUNT(*) = SUM(dataPresent) AS nowReady, " + + "assetsPresent AS markedReady " + + "FROM assetrefs, dataitems " + + "LEFT OUTER JOIN assets ON assetrefs.assets_digest = assets.digest " + + "WHERE assetrefs.dataitems_id=dataitems._id " + + "GROUP BY dataitems_id;"); + + db.execSQL("CREATE VIEW appKeyArchiveDataItems AS SELECT " + + "appkeys._id AS appkeys_id, " + + "appkeys.packageName AS packageName, " + + "appkeys.signatureDigest AS signatureDigest, " + + "archiveDataItems._id AS archiveDataItems_id, " + + "archiveDataItems.migratingNode AS migratingNode, " + + "archiveDataItems.path AS path, " + + "archiveDataItems.data AS data, " + + "archiveDataItems.timestampMs AS timestampMs, " + + "archiveDataItems.assetsPresent AS assetsPresent " + + "FROM appkeys, archiveDataItems " + + "WHERE appkeys._id = archiveDataItems.appkeys_id"); + + db.execSQL("CREATE VIEW archiveDataItemsAndAssets AS SELECT " + + "appKeyArchiveDataItems.appkeys_id AS appkeys_id, " + + "appKeyArchiveDataItems.packageName AS packageName, " + + "appKeyArchiveDataItems.signatureDigest AS signatureDigest, " + + "appKeyArchiveDataItems.archiveDataItems_id AS archiveDataItems_id, " + + "appKeyArchiveDataItems.migratingNode AS migratingNode, " + + "appKeyArchiveDataItems.path AS path, " + + "appKeyArchiveDataItems.data AS data, " + + "appKeyArchiveDataItems.timestampMs AS timestampMs, " + + "appKeyArchiveDataItems.assetsPresent AS assetsPresent, " + + "archiveAssetRefs.assetname AS assetname, " + + "archiveAssetRefs.assets_digest AS assets_digest " + + "FROM appKeyArchiveDataItems " + + "LEFT OUTER JOIN archiveAssetRefs ON appKeyArchiveDataItems.archiveDataItems_id = archiveAssetRefs.archiveDataItems_id"); + + db.execSQL("CREATE VIEW archiveAssetsReadyStatus AS SELECT " + + "archiveDataItems_id AS archiveDataItems_id, " + + "COUNT(*) = SUM(dataPresent) AS nowReady, " + + "assetsPresent AS markedReady " + + "FROM archiveAssetRefs, archiveDataItems " + + "LEFT OUTER JOIN assets ON archiveAssetRefs.assets_digest = assets.digest " + + "WHERE archiveAssetRefs.archiveDataItems_id = archiveDataItems._id " + + "GROUP BY archiveDataItems_id;"); + + db.execSQL("CREATE UNIQUE INDEX appkeys_NAME_AND_SIG ON appkeys(" + + "packageName, signatureDigest);"); + + db.execSQL("CREATE UNIQUE INDEX assetrefs_ASSET_REFS ON assetrefs(" + + "assets_digest, dataitems_id, assetname);"); + + db.execSQL("CREATE INDEX assetrefs_DATAITEM_ID ON assetrefs(dataitems_id);"); + + db.execSQL("CREATE UNIQUE INDEX archiveAssetRefs_ASSET_REFS ON archiveAssetRefs(" + + "assets_digest, archiveDataItems_id, assetname);"); + + db.execSQL("CREATE INDEX archiveAssetRefs_DATAITEM_ID ON archiveAssetRefs(archiveDataItems_id);"); + db.execSQL("CREATE UNIQUE INDEX assets_DIGEST ON assets(digest);"); - db.execSQL("CREATE UNIQUE INDEX assetsacls_APPKEY_AND_DIGEST ON assetsacls(appkeys_id,assets_digest);"); - db.execSQL("CREATE UNIQUE INDEX dataitems_APPKEY_HOST_AND_PATH ON dataitems(appkeys_id,host,path);"); - db.execSQL("CREATE UNIQUE INDEX dataitems_SOURCENODE_AND_SEQID ON dataitems(sourceNode,seqId);"); - db.execSQL("CREATE UNIQUE INDEX dataitems_SOURCENODE_DELETED_AND_SEQID ON dataitems(sourceNode,deleted,seqId);"); + + db.execSQL("CREATE UNIQUE INDEX assetsacls_APPKEY_AND_DIGEST ON assetsacls(" + + "appkeys_id, assets_digest);"); + + db.execSQL("CREATE UNIQUE INDEX dataitems_APPPKEY_PATH_AND_HOST ON dataitems(" + + "appkeys_id, path, host);"); + + db.execSQL("CREATE UNIQUE INDEX dataitems_SOURCENODE_AND_SEQID ON dataitems(" + + "sourceNode, seqId);"); + db.execSQL("CREATE UNIQUE INDEX dataitems_SOURCENODE_DELETED_AND_SEQID ON dataitems(" + + "sourceNode, deleted, seqId);"); + + db.execSQL("CREATE UNIQUE INDEX archiveDataItems_NODE_APPPKEY_PATH ON archiveDataItems(" + + "migratingNode, appkeys_id, path);"); } public synchronized Cursor getDataItemsForDataHolder(String packageName, String signatureDigest) { @@ -125,7 +281,7 @@ public synchronized Cursor getDataItemsByHostAndPath(String packageName, String @Override public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) { if (oldVersion != VERSION) { - // TODO: Upgrade not supported, cleaning up + // just recreate everything db.execSQL("DROP TABLE IF EXISTS appkeys;"); db.execSQL("DROP TABLE IF EXISTS dataitems;"); db.execSQL("DROP TABLE IF EXISTS assets;"); @@ -160,29 +316,46 @@ private static synchronized long getAppKey(SQLiteDatabase db, String packageName public synchronized void putRecord(DataItemRecord record) { SQLiteDatabase db = getWritableDatabase(); db.beginTransaction(); - Cursor cursor = getDataItemsByHostAndPath(db, record.packageName, record.signatureDigest, record.dataItem.host, record.dataItem.path); try { - String key; - if (cursor.moveToNext()) { - // update - key = cursor.getString(0); - updateRecord(db, key, record); - } else { - // insert - key = insertRecord(db, record); - } + long appKeyId = getAppKey(db, record.packageName, record.signatureDigest); + + ContentValues cv = new ContentValues(); + cv.put("appkeys_id", appKeyId); + cv.put("host", record.dataItem.host); + cv.put("path", record.dataItem.path); + cv.put("seqId", record.seqId); + cv.put("deleted", record.deleted ? 1 : 0); + cv.put("sourceNode", record.source); + cv.put("data", record.dataItem.data); + cv.put("timestampMs", System.currentTimeMillis()); + cv.put("assetsPresent", record.assetsAreReady ? 1 : 0); + cv.put("v1SourceNode", record.source); + cv.put("v1SeqId", record.v1SeqId != 0 ? record.v1SeqId : record.seqId); + + long dataItemId = db.insertWithOnConflict("dataitems", null, cv, + SQLiteDatabase.CONFLICT_REPLACE); + + db.delete("assetrefs", "dataitems_id=?", + new String[]{String.valueOf(dataItemId)}); + + if (record.dataItem.getAssets() != null && !record.dataItem.getAssets().isEmpty()) { + for (Map.Entry entry : record.dataItem.getAssets().entrySet()) { + ContentValues assetRef = new ContentValues(); + assetRef.put("dataitems_id", dataItemId); + assetRef.put("assetname", entry.getKey()); + assetRef.put("assets_digest", entry.getValue().getDigest()); - if (record.assetsAreReady) { - ContentValues update = new ContentValues(); - update.put("assetsPresent", 1); - db.update("dataitems", update, "_id=?", new String[]{key}); + db.insertWithOnConflict("assetrefs", null, assetRef, + SQLiteDatabase.CONFLICT_IGNORE); + } } db.setTransactionSuccessful(); + } catch (Exception e) { + Log.e(TAG, "Error in putRecord", e); } finally { - cursor.close(); + db.endTransaction(); } - db.endTransaction(); } private static void updateRecord(SQLiteDatabase db, String dataItemId, DataItemRecord record) { @@ -205,7 +378,8 @@ private static void updateRecord(SQLiteDatabase db, String dataItemId, DataItemR assetRef.put("assetname", entry.getKey()); assetRef.put("assets_digest", entry.getValue().getDigest()); - db.insert("assetrefs", null, assetRef); + db.insertWithOnConflict("assetrefs", null, assetRef, + SQLiteDatabase.CONFLICT_IGNORE); } } @@ -227,23 +401,24 @@ private static String insertRecord(SQLiteDatabase db, DataItemRecord record) { cv.put("v1SourceNode", record.source); cv.put("v1SeqId", record.v1SeqId != 0 ? record.v1SeqId : record.seqId); - long dataItemId = db.insert("dataitems", null, cv); + long dataItemId = db.insertWithOnConflict("dataitems", null, cv, + SQLiteDatabase.CONFLICT_REPLACE); + + db.delete("assetrefs", "dataitems_id=?", + new String[]{String.valueOf(dataItemId)}); if (record.dataItem.getAssets() != null && !record.dataItem.getAssets().isEmpty()) { for (Map.Entry entry : record.dataItem.getAssets().entrySet()) { - String assetKey = entry.getKey(); - Asset asset = entry.getValue(); - ContentValues assetRef = new ContentValues(); assetRef.put("dataitems_id", dataItemId); - assetRef.put("assetname", assetKey); - assetRef.put("assets_digest", asset.getDigest()); + assetRef.put("assetname", entry.getKey()); + assetRef.put("assets_digest", entry.getValue().getDigest()); - db.insert("assetrefs", null, assetRef); + db.insertWithOnConflict("assetrefs", null, assetRef, + SQLiteDatabase.CONFLICT_IGNORE); } } - return String.valueOf(dataItemId); } @@ -256,7 +431,10 @@ private static String finishRecord(SQLiteDatabase db, String key, DataItemRecord assetValues.put("assetname", asset.getKey()); db.insertWithOnConflict("assetrefs", "assetname", assetValues, SQLiteDatabase.CONFLICT_IGNORE); } - Cursor status = db.query("assetsReadyStatus", new String[]{"nowReady"}, "dataitems_id=?", new String[]{key}, null, null, null); + Cursor status = db.query("assetsReadyStatus", + new String[]{"nowReady"}, "dataitems_id=?", + new String[]{key}, null, null, null); + if (status.moveToNext()) { record.assetsAreReady = status.getLong(0) != 0; } @@ -267,21 +445,38 @@ private static String finishRecord(SQLiteDatabase db, String key, DataItemRecord return key; } - private static Cursor getDataItemsByHostAndPath(SQLiteDatabase db, String packageName, String signatureDigest, String host, String path) { + private static Cursor getDataItemsByHostAndPath(SQLiteDatabase db, String packageName, + String signatureDigest, String host, String path) { String[] params; String selection; + if (path == null) { params = new String[]{packageName, signatureDigest}; - selection = "packageName =? AND signatureDigest =?"; + selection = "packageName = ? AND signatureDigest = ? AND deleted = 0"; } else if (host == null) { - params = new String[]{packageName, signatureDigest, path}; - selection = "packageName =? AND signatureDigest =? AND path =?"; + String pathPattern = path; + if (path.endsWith("/")) { + pathPattern = path + "*"; + } + pathPattern = pathPattern.replace("*", "%"); + + params = new String[]{packageName, signatureDigest, pathPattern}; + selection = "packageName = ? AND signatureDigest = ? AND path LIKE ? AND deleted = 0"; } else { - params = new String[]{packageName, signatureDigest, host, path}; - selection = "packageName =? AND signatureDigest =? AND host =? AND path =?"; + String pathPattern = path; + if (path.endsWith("/")) { + pathPattern = path + "*"; + } + pathPattern = pathPattern.replace("*", "%"); + + String hostPattern = host.replace("*", "%"); + + params = new String[]{packageName, signatureDigest, hostPattern, pathPattern}; + selection = "packageName = ? AND signatureDigest = ? AND host LIKE ? AND path LIKE ? AND deleted = 0"; } - selection += " AND deleted=0"; - return db.query("dataItemsAndAssets", GDIBHAP_FIELDS, selection, params, null, null, "packageName, signatureDigest, host, path"); + + return db.query("dataItemsAndAssets", GDIBHAP_FIELDS, selection, params, + null, null, "packageName, signatureDigest, host, path"); } public Cursor getModifiedDataItems(final String nodeId, final long seqId, final boolean excludeDeleted) { @@ -294,7 +489,7 @@ public Cursor getModifiedDataItems(final String nodeId, final long seqId, final String query = "SELECT " + - "d._id AS _id, " + + "d._id AS dataitems_id, " + "a.packageName AS packageName, " + "a.signatureDigest AS signatureDigest, " + "d.host AS host, " + @@ -305,6 +500,8 @@ public Cursor getModifiedDataItems(final String nodeId, final long seqId, final "d.data AS data, " + "d.timestampMs AS timestampMs, " + "d.assetsPresent AS assetsPresent, " + + "'' AS assetname, " + + "'' AS assets_digest, " + "d.v1SourceNode AS v1SourceNode, " + "d.v1SeqId AS v1SeqId " + "FROM dataitems d " + @@ -372,7 +569,7 @@ public Cursor listMissingAssets() { SQLiteDatabase db = getReadableDatabase(); String query = - "SELECT " + + "SELECT DISTINCT " + "a.packageName AS packageName, " + "a.signatureDigest AS signatureDigest, " + "d.host AS host, " + @@ -390,10 +587,12 @@ public Cursor listMissingAssets() { "FROM dataitems d " + "JOIN appkeys a ON d.appkeys_id = a._id " + "JOIN assetrefs ar ON d._id = ar.dataitems_id " + - "WHERE d.assetsPresent = 0 " + - "AND ar.assets_digest IS NOT NULL " + + "LEFT JOIN assets ast ON ar.assets_digest = ast.digest " + + "WHERE d.deleted = 0 " + + "AND (ast.dataPresent = 0 OR ast.dataPresent IS NULL) " + "ORDER BY a.packageName, a.signatureDigest, d.host, d.path"; + return db.rawQuery(query, null); } @@ -431,7 +630,7 @@ public Cursor getDataItemsWaitingForAsset(String digest) { SQLiteDatabase db = getReadableDatabase(); String query = "SELECT " + - "d._id AS _id, " + + "d._id AS dataitems_id, " + "a.packageName AS packageName, " + "a.signatureDigest AS signatureDigest, " + "d.host AS host, " + @@ -441,52 +640,36 @@ public Cursor getDataItemsWaitingForAsset(String digest) { "d.sourceNode AS sourceNode, " + "d.data AS data, " + "d.timestampMs AS timestampMs, " + - "d.assetsPresent AS assetsPresent " + + "d.assetsPresent AS assetsPresent, " + + "ar.assetname AS assetname, " + + "ar.assets_digest AS assets_digest, " + + "d.v1SourceNode AS v1SourceNode, " + + "d.v1SeqId AS v1SeqId " + "FROM dataitems d " + "JOIN appkeys a ON d.appkeys_id = a._id " + + "JOIN assetrefs ar ON d._id = ar.dataitems_id " + "WHERE d.assetsPresent = 0 " + - "AND d._id IN (" + - " SELECT dataitems_id FROM assetrefs WHERE assets_digest = ?" + - ")"; + "AND ar.assets_digest = ?"; return db.rawQuery(query, new String[]{digest}); } - public void updateAssetsReady(String uri, boolean ready) { - SQLiteDatabase db = getWritableDatabase(); - ContentValues values = new ContentValues(); - values.put("assetsPresent", ready ? 1 : 0); - - Uri parsedUri = Uri.parse(uri); - String host = parsedUri.getAuthority(); - String path = parsedUri.getPath(); + public synchronized void updateAssetsReady(String uri, boolean ready) { + ContentValues cv = new ContentValues(); + cv.put("assetsPresent", ready ? 1 : 0); - db.update("dataitems", values, - "host = ? AND path = ?", new String[]{host, path}); + getWritableDatabase().update("dataitems", cv, + "host || path = ?", new String[]{uri}); } - public synchronized void markAssetAsPresent(String digest) { ContentValues cv = new ContentValues(); + cv.put("digest", digest); cv.put("dataPresent", 1); cv.put("timestampMs", System.currentTimeMillis()); - SQLiteDatabase db = getWritableDatabase(); - int rows = db.update("assets", cv, "digest=?", new String[]{digest}); - if (rows == 0) { - cv.put("digest", digest); - db.insert("assets", null, cv); - } - Cursor status = db.query("assetsReadyStatus", null, "nowReady != markedReady", null, null, null, null); - while (status.moveToNext()) { - int nowReady = status.getInt(status.getColumnIndexOrThrow("nowReady")); - int dataItemId = status.getInt(status.getColumnIndexOrThrow("dataitems_id")); - - ContentValues update = new ContentValues(); - update.put("assetsPresent", nowReady); - db.update("dataitems", update, "_id=?", - new String[]{String.valueOf(dataItemId)}); - } - status.close(); + getWritableDatabase().insertWithOnConflict("assets", null, cv, + SQLiteDatabase.CONFLICT_REPLACE); } + } diff --git a/play-services-wearable/core/src/main/java/org/microg/gms/wearable/bluetooth/BluetoothConnectionThread.java b/play-services-wearable/core/src/main/java/org/microg/gms/wearable/bluetooth/BluetoothConnectionThread.java index 6407d6e333..54fea06bef 100644 --- a/play-services-wearable/core/src/main/java/org/microg/gms/wearable/bluetooth/BluetoothConnectionThread.java +++ b/play-services-wearable/core/src/main/java/org/microg/gms/wearable/bluetooth/BluetoothConnectionThread.java @@ -357,13 +357,9 @@ public void resetBackoff() { } public void scheduleRetry() { + // TODO: find better approach retryCount.set(0); - immediateRetry.set(true); interrupt(); -// retryHandler.post(() -> { -// Log.d(TAG, "Scheduled retry triggered for " + config.address); -// interrupt(); -// }); } private void closeSocket() { diff --git a/play-services-wearable/core/src/main/java/org/microg/gms/wearable/bluetooth/BluetoothWearableConnection.java b/play-services-wearable/core/src/main/java/org/microg/gms/wearable/bluetooth/BluetoothWearableConnection.java index 449b90a24f..6997c3ec4b 100644 --- a/play-services-wearable/core/src/main/java/org/microg/gms/wearable/bluetooth/BluetoothWearableConnection.java +++ b/play-services-wearable/core/src/main/java/org/microg/gms/wearable/bluetooth/BluetoothWearableConnection.java @@ -229,8 +229,11 @@ protected MessagePiece readMessagePiece() throws IOException { throw new IOException("Socket closed by peer while reading data", e); } + watchdogHandler.removeCallbacks(readWatchdog); return MessagePiece.ADAPTER.decode(bytes); } catch (IOException e) { + watchdogHandler.removeCallbacks(readWatchdog); + if (isClosed.get()) { throw new IOException("Connection closed during read", e); } diff --git a/play-services-wearable/core/src/main/java/org/microg/gms/wearable/channel/ChannelManager.java b/play-services-wearable/core/src/main/java/org/microg/gms/wearable/channel/ChannelManager.java index 34bf01ce6c..10ba47a6ff 100644 --- a/play-services-wearable/core/src/main/java/org/microg/gms/wearable/channel/ChannelManager.java +++ b/play-services-wearable/core/src/main/java/org/microg/gms/wearable/channel/ChannelManager.java @@ -165,7 +165,7 @@ private void doOpenChannel(AppKey appKey, String nodeId, String path, OpenChanne .fromChannelOperator(true) .packageName(appKey.packageName) .signatureDigest(appKey.signatureDigest) -// .path(path) + .path(path) .build(); ChannelRequest channelRequest = new ChannelRequest.Builder() From 8544d124eb4d939b9622c5ee29a6ec12ebbdc60a Mon Sep 17 00:00:00 2001 From: deadYokai Date: Wed, 14 Jan 2026 22:50:27 +0200 Subject: [PATCH 14/29] reworked Bluetooth --- .../core/src/main/AndroidManifest.xml | 6 + .../microg/gms/wearable/MessageHandler.java | 12 +- .../org/microg/gms/wearable/WearableImpl.java | 116 +---- .../bluetooth/AlarmManagerHelper.java | 105 +++++ .../bluetooth/BleClientConnection.java | 168 ------- .../wearable/bluetooth/BleClientManager.java | 153 ------- .../bluetooth/BleDeviceDiscoverer.java | 263 +++++++++++ .../wearable/bluetooth/BluetoothClient.java | 293 ++++++------ .../bluetooth/BluetoothConnectionThread.java | 426 +++++++++++------- .../BluetoothWearableConnection.java | 57 +++ .../gms/wearable/bluetooth/RetryStrategy.java | 156 +++++++ .../wearable/bluetooth/WakeLockManager.java | 214 +++++++++ 12 files changed, 1194 insertions(+), 775 deletions(-) create mode 100644 play-services-wearable/core/src/main/java/org/microg/gms/wearable/bluetooth/AlarmManagerHelper.java delete mode 100644 play-services-wearable/core/src/main/java/org/microg/gms/wearable/bluetooth/BleClientConnection.java delete mode 100644 play-services-wearable/core/src/main/java/org/microg/gms/wearable/bluetooth/BleClientManager.java create mode 100644 play-services-wearable/core/src/main/java/org/microg/gms/wearable/bluetooth/BleDeviceDiscoverer.java create mode 100644 play-services-wearable/core/src/main/java/org/microg/gms/wearable/bluetooth/RetryStrategy.java create mode 100644 play-services-wearable/core/src/main/java/org/microg/gms/wearable/bluetooth/WakeLockManager.java diff --git a/play-services-wearable/core/src/main/AndroidManifest.xml b/play-services-wearable/core/src/main/AndroidManifest.xml index befff0bbe9..2b1e416d85 100644 --- a/play-services-wearable/core/src/main/AndroidManifest.xml +++ b/play-services-wearable/core/src/main/AndroidManifest.xml @@ -6,7 +6,13 @@ + + + + + + diff --git a/play-services-wearable/core/src/main/java/org/microg/gms/wearable/MessageHandler.java b/play-services-wearable/core/src/main/java/org/microg/gms/wearable/MessageHandler.java index 9c77baf6a1..a31207574b 100644 --- a/play-services-wearable/core/src/main/java/org/microg/gms/wearable/MessageHandler.java +++ b/play-services-wearable/core/src/main/java/org/microg/gms/wearable/MessageHandler.java @@ -58,7 +58,7 @@ import okio.ByteString; public class MessageHandler extends ServerMessageListener { - private static final String TAG = "GmsWearMsgHandler"; + private static final String TAG = "WearMessageHandler"; private final WearableImpl wearable; private final String oldConfigNodeId; private String peerNodeId; @@ -171,11 +171,6 @@ public void onRpcRequest(Request rpcRequest) { } else { // TODO: find next hop } - try { - getConnection().writeMessage(new RootMessage.Builder().heartbeat(new Heartbeat()).build()); - } catch (IOException e) { - onDisconnected(); - } } @Override @@ -197,6 +192,11 @@ public void onChannelRequest(Request channelRequest) { public void handleMessage(WearableConnection connection, String sourceNodeId, RootMessage message) { Log.d(TAG, "handleMessage from " + sourceNodeId); + if (message.heartbeat != null) { + Log.d(TAG, "Received heartbeat from " + sourceNodeId); + return; + } + if (message.syncStart != null) { handleSyncStart(connection, sourceNodeId, message.syncStart); } diff --git a/play-services-wearable/core/src/main/java/org/microg/gms/wearable/WearableImpl.java b/play-services-wearable/core/src/main/java/org/microg/gms/wearable/WearableImpl.java index 16f1ecd2c7..73408dd7a9 100644 --- a/play-services-wearable/core/src/main/java/org/microg/gms/wearable/WearableImpl.java +++ b/play-services-wearable/core/src/main/java/org/microg/gms/wearable/WearableImpl.java @@ -17,14 +17,11 @@ package org.microg.gms.wearable; import android.Manifest; -import android.bluetooth.BluetoothAdapter; -import android.bluetooth.BluetoothDevice; import android.content.Context; import android.content.Intent; import android.content.IntentFilter; import android.database.Cursor; import android.net.Uri; -import android.os.Build; import android.os.Handler; import android.os.Looper; import android.os.RemoteException; @@ -41,23 +38,19 @@ import com.google.android.gms.wearable.MessageOptions; import com.google.android.gms.wearable.Node; import com.google.android.gms.wearable.internal.IWearableListener; -import com.google.android.gms.wearable.internal.MessageEventParcelable; import com.google.android.gms.wearable.internal.NodeParcelable; import com.google.android.gms.wearable.internal.PutDataRequest; import org.microg.gms.common.PackageUtils; import org.microg.gms.common.RemoteListenerProxy; import org.microg.gms.common.Utils; -import org.microg.gms.wearable.bluetooth.BleClientManager; import org.microg.gms.wearable.bluetooth.BluetoothClient; import org.microg.gms.wearable.bluetooth.BluetoothServer; import org.microg.gms.wearable.channel.ChannelCallbacks; import org.microg.gms.wearable.channel.ChannelManager; import org.microg.gms.wearable.channel.ChannelToken; -import org.microg.gms.wearable.proto.AckAsset; import org.microg.gms.wearable.proto.AppKey; import org.microg.gms.wearable.proto.AppKeys; -import org.microg.gms.wearable.proto.AssetEntry; import org.microg.gms.wearable.proto.Connect; import org.microg.gms.wearable.proto.FetchAsset; import org.microg.gms.wearable.proto.FilePiece; @@ -80,10 +73,7 @@ import java.util.List; import java.util.Map; import java.util.Set; -import java.util.concurrent.ConcurrentHashMap; -import java.util.concurrent.ConcurrentMap; import java.util.concurrent.CountDownLatch; -import java.util.concurrent.atomic.AtomicBoolean; import okio.ByteString; @@ -108,8 +98,6 @@ public class WearableImpl { public Handler networkHandler; private BluetoothClient bluetoothClient; - private BluetoothServer bluetoothServer; - private BleClientManager bleClientManager; public static final int TYPE_BLUETOOTH_RFCOMM = 1; public static final int TYPE_NETWORK = 2; @@ -472,10 +460,6 @@ public File createAssetReceiveTempFile(String name) { public void onConnectReceived(WearableConnection connection, String nodeId, Connect connect) { for (ConnectionConfiguration config : getConfigurations()) { if (config.nodeId.equals(nodeId)) { - if (config.nodeId != nodeId) { - config.nodeId = connect.id; - configDatabase.putConfiguration(config, nodeId); - } config.peerNodeId = connect.id; config.connected = true; } @@ -486,7 +470,6 @@ public void onConnectReceived(WearableConnection connection, String nodeId, Conn onPeerConnected(new NodeParcelable(connect.id, connect.name, 0, true)); - // Fetch missing assets syncToPeer(connect.id, nodeId, getCurrentSeqId(nodeId)); try { @@ -497,7 +480,7 @@ public void onConnectReceived(WearableConnection connection, String nodeId, Conn } else { Log.d(TAG, "Connection closed before asset fetch could start"); } - }, 1000); + }, 5000); } catch (InterruptedException e) { Log.w(TAG, "Interrupted while scheduling asset fetch", e); } @@ -805,83 +788,13 @@ public void createConnection(ConnectionConfiguration config) { } } - public static class BluetoothConnectionLock { - private static final String TAG = "BtConnLock"; - private static final Map locks = new ConcurrentHashMap<>(); - - public static synchronized boolean tryAcquire(String address, String owner) { - AtomicBoolean lock = locks.get(address); - if (lock == null) { - lock = new AtomicBoolean(false); - locks.put(address, lock); - } - - boolean acquired = lock.compareAndSet(false, true); - if (acquired) { - Log.d(TAG, owner + " acquired lock for " + address); - } else { - Log.d(TAG, owner + " failed to acquire lock for " + address + " (already held)"); - } - return acquired; - } - - public static synchronized void release(String address, String owner) { - AtomicBoolean lock = locks.get(address); - if (lock != null) { - lock.set(false); - Log.d(TAG, owner + " released lock for " + address); - } - } - - public static boolean isHeld(String address) { - AtomicBoolean lock = locks.get(address); - return lock != null && lock.get(); - } - - } - - @RequiresPermission(Manifest.permission.BLUETOOTH_CONNECT) private void handleBle(ConnectionConfiguration config, boolean enabled) { - Log.d(TAG, "BLE not implemented"); - if (config.role == ROLE_CLIENT) { - if (enabled) { - try { - networkHandlerLock.await(); - networkHandler.post(() -> { - if (bleClientManager == null) { - Log.d(TAG, "No BleClientManager found. Initializing a new one."); - bleClientManager = new BleClientManager(context); - } - bleClientManager.addConfiguration(config); - }); - } catch (InterruptedException e) { - Log.w(TAG, "Interrupted while starting BLE client", e); - } - } else { - try { - networkHandlerLock.await(); - networkHandler.post(() -> { - if (bleClientManager != null) { - bleClientManager.removeConfiguration(config); - } - }); - } catch (InterruptedException e) { - Log.w(TAG, "Interrupted while stopping BLE client", e); - } - } - } else if (config.role == ROLE_SERVER) { - // update ble server config - } + Log.w(TAG, "BLE not implemented"); } private void handleNetwork(ConnectionConfiguration config, boolean enabled) { - Log.d(TAG, "Network not implemented"); - if (enabled) { - // initialize new network service - } else { - // close network service - } + Log.w(TAG, "Network not implemented"); } private void handleLegacy(ConnectionConfiguration config, boolean enabled) { @@ -891,7 +804,7 @@ private void handleLegacy(ConnectionConfiguration config, boolean enabled) { networkHandlerLock.await(); networkHandler.post(() -> { if (bluetoothClient == null) { - Log.d(TAG, "No BluetoothClient found. Initializing a new one."); + Log.d(TAG, "Initializing BluetoothClient"); bluetoothClient = new BluetoothClient(context, this); } bluetoothClient.addConfig(config); @@ -905,27 +818,10 @@ private void handleLegacy(ConnectionConfiguration config, boolean enabled) { }); } } else if (config.role == ROLE_SERVER) { - Log.d(TAG, "Bluetooth server not implemented"); - if (enabled) { -// networkHandlerLock.await(); -// networkHandler.post(() -> { -// if (bluetoothServer == null) { -// Log.d(TAG, "No BluetoothClient found. Initializing a new one."); -// bluetoothServer = new BluetoothServer(context); -// } -// bluetoothServer.addConfiguration(config); -// }); - } else { -// networkHandlerLock.await(); -// networkHandler.post(() -> { -// if (bluetoothServer != null) { -// bluetoothServer.removeConfiguration(config); -// } -// }); - } + Log.w(TAG, "Bluetooth role Server not implemented"); } } catch (InterruptedException e) { - Log.w(TAG, "Interrupted while duing stuff with bluetooth", e); + Log.w(TAG, "Interrupted while handling Bluetooth", e); } } diff --git a/play-services-wearable/core/src/main/java/org/microg/gms/wearable/bluetooth/AlarmManagerHelper.java b/play-services-wearable/core/src/main/java/org/microg/gms/wearable/bluetooth/AlarmManagerHelper.java new file mode 100644 index 0000000000..f4b82b6e65 --- /dev/null +++ b/play-services-wearable/core/src/main/java/org/microg/gms/wearable/bluetooth/AlarmManagerHelper.java @@ -0,0 +1,105 @@ +package org.microg.gms.wearable.bluetooth; + +import android.app.AlarmManager; +import android.app.PendingIntent; +import android.content.Context; +import android.os.Build; +import android.os.SystemClock; +import android.util.Log; + +public class AlarmManagerHelper { + private static final String TAG = "AlarmManagerHelper"; + + private final Context context; + private final AlarmManager alarmManager; + + public AlarmManagerHelper(Context context) { + this.context = context; + this.alarmManager = (AlarmManager) context.getSystemService(Context.ALARM_SERVICE); + + if (alarmManager == null) { + throw new IllegalStateException("AlarmManager not available"); + } + } + + public void setExactAndAllowWhileIdle(String tag, int type, long triggerAtMillis, + PendingIntent operation) { + if (triggerAtMillis <= 0) { + Log.w(TAG, String.format("Invalid trigger time: %d", triggerAtMillis)); + return; + } + + long delayMs = triggerAtMillis - SystemClock.elapsedRealtime(); + Log.d(TAG, String.format("setExactAndAllowWhileIdle: tag=%s, type=%d, delay=%dms", + tag, type, delayMs)); + + try { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { + alarmManager.setExactAndAllowWhileIdle(type, triggerAtMillis, operation); + } else { + alarmManager.setExact(type, triggerAtMillis, operation); + } + } catch (SecurityException e) { + Log.e(TAG, "SecurityException setting alarm", e); + throw e; + } catch (Exception e) { + Log.e(TAG, "Error setting alarm", e); + throw new RuntimeException("Failed to set alarm", e); + } + } + + public void setWindow(String tag, int type, long triggerAtMillis, long windowMs, + PendingIntent operation) { + if (triggerAtMillis <= 0) { + Log.w(TAG, String.format("Invalid trigger time: %d", triggerAtMillis)); + return; + } + + long delayMs = triggerAtMillis - SystemClock.elapsedRealtime(); + Log.d(TAG, String.format("setWindow: tag=%s, type=%d, delay=%dms, window=%dms", + tag, type, delayMs, windowMs)); + + try { + alarmManager.setWindow(type, triggerAtMillis, windowMs, operation); + } catch (Exception e) { + Log.e(TAG, "Error setting windowed alarm", e); + throw new RuntimeException("Failed to set windowed alarm", e); + } + } + + public void cancel(PendingIntent operation) { + try { + alarmManager.cancel(operation); + Log.d(TAG, "Cancelled alarm"); + } catch (Exception e) { + Log.w(TAG, "Error cancelling alarm", e); + } + } + + public static PendingIntent createPendingIntent(Context context, int requestCode, + android.content.Intent intent) { + int flags = PendingIntent.FLAG_UPDATE_CURRENT; + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { + flags |= PendingIntent.FLAG_IMMUTABLE; + } + + return PendingIntent.getBroadcast(context, requestCode, intent, flags); + } + + public static long elapsedRealtimeFromNow(long delayMs) { + return SystemClock.elapsedRealtime() + delayMs; + } + + public boolean canScheduleExactAlarms() { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { + try { + return alarmManager.canScheduleExactAlarms(); + } catch (Exception e) { + Log.w(TAG, "Error checking exact alarm permission", e); + return false; + } + } + return true; + } +} \ No newline at end of file diff --git a/play-services-wearable/core/src/main/java/org/microg/gms/wearable/bluetooth/BleClientConnection.java b/play-services-wearable/core/src/main/java/org/microg/gms/wearable/bluetooth/BleClientConnection.java deleted file mode 100644 index 4e0f712f92..0000000000 --- a/play-services-wearable/core/src/main/java/org/microg/gms/wearable/bluetooth/BleClientConnection.java +++ /dev/null @@ -1,168 +0,0 @@ -package org.microg.gms.wearable.bluetooth; - -import android.Manifest; -import android.bluetooth.BluetoothAdapter; -import android.bluetooth.BluetoothDevice; -import android.bluetooth.BluetoothGatt; -import android.bluetooth.BluetoothGattCallback; -import android.bluetooth.BluetoothGattCharacteristic; -import android.bluetooth.BluetoothProfile; -import android.content.Context; -import android.os.Build; -import android.os.Handler; -import android.os.Looper; -import android.util.Log; - -import androidx.annotation.RequiresPermission; - -import com.google.android.gms.wearable.ConnectionConfiguration; - -import java.io.Closeable; -import java.util.UUID; - -public class BleClientConnection extends Thread implements Closeable { - private static final String TAG = "GmsWearBleConn"; - - private static final UUID WEAR_SERVICE_UUID = UUID.fromString("0000fef7-0000-1000-8000-00805f9b34fb"); - - private final Context context; - private final ConnectionConfiguration config; - private final BluetoothAdapter bluetoothAdapter; - private final Handler handler; - private volatile boolean running = true; - private BluetoothGatt bluetoothGatt; - - public BleClientConnection(Context context, ConnectionConfiguration config, BluetoothAdapter adapter) { - super("BleClientConn-" + config.address); - this.context = context; - this.config = config; - this.bluetoothAdapter = adapter; - this.handler = new Handler(Looper.getMainLooper()); - } - - @RequiresPermission(Manifest.permission.BLUETOOTH_CONNECT) - @Override - public void run() { - Log.d(TAG, "BLE connection thread started for " + config.address); - - while (running && !isInterrupted()) { - try { - connect(); - synchronized (this) { - wait(); - } - } catch (InterruptedException e) { - Log.d(TAG, "BLE connection interrupted"); - break; - } catch (Exception e) { - Log.w(TAG, "BLE connection error: " + e.getMessage(), e); - disconnect(); - - if (running) { - try { - Thread.sleep(5000); // Wait before retry - } catch (InterruptedException ie) { - break; - } - } - } - } - - disconnect(); - } - - @RequiresPermission(Manifest.permission.BLUETOOTH_CONNECT) - private void connect() { - if (!running || bluetoothAdapter == null || !bluetoothAdapter.isEnabled()) { - throw new IllegalStateException("Bluetooth not available"); - } - - BluetoothDevice device = bluetoothAdapter.getRemoteDevice(config.address); - if (device == null) { - throw new IllegalStateException("Could not get remote device"); - } - - Log.d(TAG, "Connecting to BLE device " + config.address); - - handler.post(() -> { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { - bluetoothGatt = device.connectGatt(context, false, gattCallback, - BluetoothDevice.TRANSPORT_LE); - } else { - bluetoothGatt = device.connectGatt(context, false, gattCallback); - } - }); - } - - @RequiresPermission(Manifest.permission.BLUETOOTH_CONNECT) - private void disconnect() { - if (bluetoothGatt != null) { - handler.post(() -> { - try { - bluetoothGatt.disconnect(); - bluetoothGatt.close(); - } catch (Exception e) { - Log.w(TAG, "Error disconnecting GATT", e); - } - bluetoothGatt = null; - }); - } - } - - private final BluetoothGattCallback gattCallback = new BluetoothGattCallback() { - @RequiresPermission(Manifest.permission.BLUETOOTH_CONNECT) - @Override - public void onConnectionStateChange(BluetoothGatt gatt, int status, int newState) { - if (newState == BluetoothProfile.STATE_CONNECTED) { - Log.d(TAG, "BLE connected to " + config.address); - gatt.discoverServices(); - } else if (newState == BluetoothProfile.STATE_DISCONNECTED) { - Log.d(TAG, "BLE disconnected from " + config.address); - synchronized (BleClientConnection.this) { - BleClientConnection.this.notifyAll(); - } - } - } - - @Override - public void onServicesDiscovered(BluetoothGatt gatt, int status) { - if (status == BluetoothGatt.GATT_SUCCESS) { - Log.d(TAG, "BLE services discovered for " + config.address); - // Handle service discovery and setup characteristics - } else { - Log.w(TAG, "BLE service discovery failed: " + status); - } - } - - @Override - public void onCharacteristicRead(BluetoothGatt gatt, BluetoothGattCharacteristic characteristic, int status) { - if (status == BluetoothGatt.GATT_SUCCESS) { - Log.d(TAG, "BLE characteristic read"); - // Handle characteristic read - } - } - - @Override - public void onCharacteristicWrite(BluetoothGatt gatt, BluetoothGattCharacteristic characteristic, int status) { - if (status == BluetoothGatt.GATT_SUCCESS) { - Log.d(TAG, "BLE characteristic written"); - } - } - - @Override - public void onCharacteristicChanged(BluetoothGatt gatt, BluetoothGattCharacteristic characteristic) { - Log.d(TAG, "BLE characteristic changed"); - // Handle notifications - } - }; - - @RequiresPermission(Manifest.permission.BLUETOOTH_CONNECT) - @Override - public void close() { - Log.d(TAG, "Closing BLE connection for " + config.address); - running = false; - interrupt(); - disconnect(); - } - -} diff --git a/play-services-wearable/core/src/main/java/org/microg/gms/wearable/bluetooth/BleClientManager.java b/play-services-wearable/core/src/main/java/org/microg/gms/wearable/bluetooth/BleClientManager.java deleted file mode 100644 index 6babf6eaae..0000000000 --- a/play-services-wearable/core/src/main/java/org/microg/gms/wearable/bluetooth/BleClientManager.java +++ /dev/null @@ -1,153 +0,0 @@ -package org.microg.gms.wearable.bluetooth; - -import android.Manifest; -import android.bluetooth.BluetoothAdapter; -import android.content.BroadcastReceiver; -import android.content.Context; -import android.content.Intent; -import android.content.IntentFilter; -import android.util.Log; - -import androidx.annotation.RequiresPermission; - -import com.google.android.gms.wearable.ConnectionConfiguration; - -import org.microg.gms.wearable.WearableImpl; - -import java.io.Closeable; -import java.util.HashMap; -import java.util.Map; - -public class BleClientManager implements Closeable { - private static final String TAG = "GmsWearBleClient"; - - private final Context context; - private final BluetoothAdapter bluetoothAdapter; - private final Map configurations = new HashMap<>(); - private final Map connections = new HashMap<>(); - private final BroadcastReceiver bluetoothStateReceiver; - - public BleClientManager(Context context) { - this.context = context; - this.bluetoothAdapter = BluetoothAdapter.getDefaultAdapter(); - - this.bluetoothStateReceiver = new BroadcastReceiver() { - @RequiresPermission(Manifest.permission.BLUETOOTH_CONNECT) - @Override - public void onReceive(Context context, Intent intent) { - if (BluetoothAdapter.ACTION_STATE_CHANGED.equals(intent.getAction())) { - int state = intent.getIntExtra(BluetoothAdapter.EXTRA_STATE, BluetoothAdapter.STATE_OFF); - onBluetoothAdapterStateChanged(state); - } - } - }; - - context.registerReceiver(bluetoothStateReceiver, new IntentFilter(BluetoothAdapter.ACTION_STATE_CHANGED)); - } - - public void addConfiguration(ConnectionConfiguration config) { - validateConfiguration(config); - - String address = config.address; - Log.d(TAG, "Adding BLE client configuration for " + address); - - if (configurations.containsKey(address)) { - Log.d(TAG, "Configuration already exists for " + address); - return; - } - - configurations.put(address, config); - - if (bluetoothAdapter == null || !bluetoothAdapter.isEnabled()) { - Log.w(TAG, "Bluetooth not available, deferring BLE connection"); - return; - } - - startConnection(config); - } - - @RequiresPermission(Manifest.permission.BLUETOOTH_CONNECT) - public void removeConfiguration(ConnectionConfiguration config) { - validateConfiguration(config); - - String address = config.address; - Log.d(TAG, "Removing BLE client configuration for " + address); - - BleClientConnection connection = connections.get(address); - if (connection != null) { - connection.close(); - connections.remove(address); - } - - configurations.remove(address); - } - - private void startConnection(ConnectionConfiguration config) { - String address = config.address; - if (connections.containsKey(address)) { - Log.d(TAG, "BLE connection already active for " + address); - return; - } - - Log.d(TAG, "Starting BLE connection for " + address); - BleClientConnection connection = new BleClientConnection(context, config, bluetoothAdapter); - connections.put(address, connection); - connection.start(); - } - - - @RequiresPermission(Manifest.permission.BLUETOOTH_CONNECT) - private void onBluetoothAdapterStateChanged(int state) { - Log.d(TAG, "Bluetooth adapter state changed to " + state); - - if (state == BluetoothAdapter.STATE_ON) { - // Start all configured connections - for (ConnectionConfiguration config : configurations.values()) { - String address = config.address; - if (!connections.containsKey(address)) { - startConnection(config); - } - } - } else if (state == BluetoothAdapter.STATE_OFF) { - // Close all connections - Log.d(TAG, "Closing all BLE connections due to adapter off"); - for (BleClientConnection connection : connections.values()) { - connection.close(); - } - connections.clear(); - } - } - - private static void validateConfiguration(ConnectionConfiguration config) { - if (config == null || config.address == null) { - throw new IllegalArgumentException("Invalid configuration"); - } - - if (config.type != WearableImpl.TYPE_BLE) { - throw new IllegalArgumentException("Invalid connection type for BLE: " + config.type); - } - - if (config.role != WearableImpl.ROLE_CLIENT) { - throw new IllegalArgumentException("Invalid role for BLE client: " + config.role); - } - } - - @RequiresPermission(Manifest.permission.BLUETOOTH_CONNECT) - @Override - public void close() { - Log.d(TAG, "Closing BleClientManager"); - - try { - context.unregisterReceiver(bluetoothStateReceiver); - } catch (Exception e) { - Log.w(TAG, "Error unregistering receiver", e); - } - - for (BleClientConnection connection : connections.values()) { - connection.close(); - } - connections.clear(); - configurations.clear(); - } - -} diff --git a/play-services-wearable/core/src/main/java/org/microg/gms/wearable/bluetooth/BleDeviceDiscoverer.java b/play-services-wearable/core/src/main/java/org/microg/gms/wearable/bluetooth/BleDeviceDiscoverer.java new file mode 100644 index 0000000000..4ea030684e --- /dev/null +++ b/play-services-wearable/core/src/main/java/org/microg/gms/wearable/bluetooth/BleDeviceDiscoverer.java @@ -0,0 +1,263 @@ +package org.microg.gms.wearable.bluetooth; + +import android.Manifest; +import android.bluetooth.BluetoothAdapter; +import android.bluetooth.BluetoothDevice; +import android.bluetooth.le.BluetoothLeScanner; +import android.bluetooth.le.ScanCallback; +import android.bluetooth.le.ScanFilter; +import android.bluetooth.le.ScanResult; +import android.bluetooth.le.ScanSettings; +import android.content.Context; +import android.os.Build; +import android.util.Log; + +import androidx.annotation.RequiresPermission; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +public class BleDeviceDiscoverer { + private static final String TAG = "BleDeviceDiscoverer"; + + private final Context context; + private final BluetoothAdapter bluetoothAdapter; + private final Map deviceFilters = new HashMap<>(); + private final Map deviceCallbacks = new HashMap<>(); + private final Object lock = new Object(); + + private BluetoothLeScanner scanner; + private boolean isScanning = false; + private ScanCallback scanCallback; + + public interface DeviceDiscoveryCallback { + void onDeviceDiscovered(BluetoothDevice device); + } + + public BleDeviceDiscoverer(Context context, BluetoothAdapter bluetoothAdapter) { + this.context = context; + this.bluetoothAdapter = bluetoothAdapter; + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { + if (bluetoothAdapter != null) { + this.scanner = bluetoothAdapter.getBluetoothLeScanner(); + } + } + } + + @RequiresPermission(Manifest.permission.BLUETOOTH_SCAN) + public void addDevice(BluetoothDevice device, DeviceDiscoveryCallback callback) { + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) { + Log.w(TAG, "BLE scanning not supported on Android < 5.0"); + return; + } + + synchronized (lock) { + if (deviceFilters.containsKey(device)) { + Log.d(TAG, "Device already being watched: " + device.getAddress()); + return; + } + + Log.d(TAG, "Adding device to watch: " + device.getAddress()); + + ScanFilter filter = new ScanFilter.Builder() + .setDeviceAddress(device.getAddress()) + .build(); + + deviceFilters.put(device, filter); + deviceCallbacks.put(device, callback); + + updateScanning(); + } + } + + @RequiresPermission(Manifest.permission.BLUETOOTH_SCAN) + public void removeDevice(BluetoothDevice device) { + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) { + return; + } + + synchronized (lock) { + if (!deviceFilters.containsKey(device)) { + Log.d(TAG, "Device not being watched: " + device.getAddress()); + return; + } + + Log.d(TAG, "Removing device from watch: " + device.getAddress()); + + deviceFilters.remove(device); + deviceCallbacks.remove(device); + + updateScanning(); + } + } + + @RequiresPermission(Manifest.permission.BLUETOOTH_SCAN) + public void clear() { + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) { + return; + } + + synchronized (lock) { + Log.d(TAG, "Clearing all devices"); + deviceFilters.clear(); + deviceCallbacks.clear(); + stopScanning(); + } + } + + @RequiresPermission(Manifest.permission.BLUETOOTH_SCAN) + private void updateScanning() { + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) { + return; + } + + if (deviceFilters.isEmpty()) { + stopScanning(); + return; + } + + if (bluetoothAdapter == null || !bluetoothAdapter.isEnabled()) { + Log.w(TAG, "Bluetooth not available, cannot start scanning"); + return; + } + + if (scanner == null) { + scanner = bluetoothAdapter.getBluetoothLeScanner(); + if (scanner == null) { + Log.w(TAG, "BluetoothLeScanner not available"); + return; + } + } + + if (isScanning) { + stopScanning(); + } + + startScanning(); + } + + @RequiresPermission(Manifest.permission.BLUETOOTH_SCAN) + private void startScanning() { + + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) { + return; + } + + try { + scanCallback = new ScanCallback() { + @Override + public void onScanResult(int callbackType, ScanResult result) { + handleScanResult(result); + } + + @Override + public void onBatchScanResults(List results) { + for (ScanResult result : results) { + handleScanResult(result); + } + } + + @Override + public void onScanFailed(int errorCode) { + Log.e(TAG, "BLE scan failed with error: " + errorCode); + synchronized (lock) { + isScanning = false; + } + } + }; + + List filters = new ArrayList<>(deviceFilters.values()); + + ScanSettings settings = new ScanSettings.Builder() + .setScanMode(ScanSettings.SCAN_MODE_LOW_POWER) + .build(); + + scanner.startScan(filters, settings, scanCallback); + isScanning = true; + + Log.d(TAG, String.format("Started BLE scanning for %d devices", filters.size())); + + } catch (SecurityException e) { + Log.e(TAG, "Permission denied for BLE scanning", e); + } catch (Exception e) { + Log.e(TAG, "Error starting BLE scan", e); + } + } + + @RequiresPermission(Manifest.permission.BLUETOOTH_SCAN) + private void stopScanning() { + if (!isScanning) { + return; + } + + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) { + return; + } + + try { + if (scanner != null && scanCallback != null) { + scanner.stopScan(scanCallback); + Log.d(TAG, "Stopped BLE scanning"); + } + } catch (Exception e) { + Log.w(TAG, "Error stopping BLE scan", e); + } finally { + isScanning = false; + scanCallback = null; + } + } + + @RequiresPermission(Manifest.permission.BLUETOOTH_SCAN) + private void handleScanResult(ScanResult result) { + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) { + return; + } + + if (result == null || result.getDevice() == null) { + return; + } + + BluetoothDevice device = result.getDevice(); + + synchronized (lock) { + DeviceDiscoveryCallback callback = deviceCallbacks.get(device); + if (callback != null) { + Log.d(TAG, String.format("Device discovered: %s (RSSI: %d)", + device.getAddress(), result.getRssi())); + + try { + callback.onDeviceDiscovered(device); + } catch (Exception e) { + Log.e(TAG, "Error in discovery callback", e); + } + + deviceFilters.remove(device); + deviceCallbacks.remove(device); + + if (deviceFilters.isEmpty()) { + stopScanning(); + } + } + } + } + + public boolean isScanning() { + synchronized (lock) { + return isScanning; + } + } + + public int getWatchedDeviceCount() { + synchronized (lock) { + return deviceFilters.size(); + } + } + + @RequiresPermission(Manifest.permission.BLUETOOTH_SCAN) + public void shutdown() { + clear(); + } +} \ No newline at end of file diff --git a/play-services-wearable/core/src/main/java/org/microg/gms/wearable/bluetooth/BluetoothClient.java b/play-services-wearable/core/src/main/java/org/microg/gms/wearable/bluetooth/BluetoothClient.java index d7423e9d0c..19aaa45c2f 100644 --- a/play-services-wearable/core/src/main/java/org/microg/gms/wearable/bluetooth/BluetoothClient.java +++ b/play-services-wearable/core/src/main/java/org/microg/gms/wearable/bluetooth/BluetoothClient.java @@ -1,5 +1,6 @@ package org.microg.gms.wearable.bluetooth; +import android.Manifest; import android.bluetooth.BluetoothAdapter; import android.bluetooth.BluetoothDevice; import android.content.BroadcastReceiver; @@ -8,6 +9,8 @@ import android.content.IntentFilter; import android.util.Log; +import androidx.annotation.RequiresPermission; + import com.google.android.gms.wearable.ConnectionConfiguration; import org.microg.gms.wearable.WearableImpl; @@ -15,6 +18,8 @@ import java.io.Closeable; import java.util.HashMap; import java.util.Map; +import java.util.concurrent.Executors; +import java.util.concurrent.ScheduledExecutorService; public class BluetoothClient implements Closeable { private static final String TAG = "GmsWearBtClient"; @@ -29,6 +34,9 @@ public class BluetoothClient implements Closeable { private final WearableImpl wearableImpl; + private final ScheduledExecutorService executor; + private final BleDeviceDiscoverer bleDiscoverer; + private volatile boolean isShutdown = false; public BluetoothClient(Context context, WearableImpl wearableImpl) { @@ -40,23 +48,10 @@ public BluetoothClient(Context context, WearableImpl wearableImpl) { this.btStateReceiver = new BroadcastReceiver() { @Override public void onReceive(Context context, Intent intent) { - String action = intent.getAction(); - - if (BluetoothAdapter.ACTION_STATE_CHANGED.equals(action)) { - int state = intent.getIntExtra(BluetoothAdapter.EXTRA_STATE, BluetoothAdapter.ERROR); - onAdapterStateChanged(state); - - } else if (BluetoothDevice.ACTION_ACL_CONNECTED.equals(action)) { - BluetoothDevice device = intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE); - if (device != null) { - onAclConnected(device); - } - - } else if (BluetoothDevice.ACTION_ACL_DISCONNECTED.equals(action)) { - BluetoothDevice device = intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE); - if (device != null) { - onAclDisconnected(device.getAddress()); - } + if (BluetoothAdapter.ACTION_STATE_CHANGED.equals(intent.getAction())) { + int state = intent.getIntExtra(BluetoothAdapter.EXTRA_STATE, + BluetoothAdapter.ERROR); + onBluetoothStateChanged(state); } } }; @@ -65,80 +60,63 @@ public void onReceive(Context context, Intent intent) { @Override public void onReceive(Context context, Intent intent) { if (BluetoothDevice.ACTION_ACL_CONNECTED.equals(intent.getAction())) { - BluetoothDevice device = intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE); - if (device != null) onAclConnected(device); + BluetoothDevice device = intent.getParcelableExtra( + BluetoothDevice.EXTRA_DEVICE); + if (device != null) { + onAclConnected(device); + } } } }; - context.registerReceiver(btStateReceiver, new IntentFilter(BluetoothAdapter.ACTION_STATE_CHANGED)); - context.registerReceiver(aclConnReceiver, new IntentFilter(BluetoothDevice.ACTION_ACL_CONNECTED)); - } - - private void onAdapterStateChanged(int state) { - Log.d(TAG, "Bluetooth adapter state changed to " + state); + this.bleDiscoverer = new BleDeviceDiscoverer(context, btAdapter); + this.executor = Executors.newScheduledThreadPool(2); - if (state == BluetoothAdapter.STATE_ON) { - for (BluetoothConnectionThread thread : connections.values()) { - thread.resetBackoff(); - thread.scheduleRetry(); - } - } else if (state == BluetoothAdapter.STATE_OFF) { - if (btAdapter != null && btAdapter.isEnabled()) { - Log.d(TAG, "Ignoring STATE_OFF - adapter still enabled (stale broadcast)"); - return; - } - for (BluetoothConnectionThread thread : new HashMap<>(connections).values()) { - thread.close(); - } - } + context.registerReceiver(btStateReceiver, + new IntentFilter(BluetoothAdapter.ACTION_STATE_CHANGED)); + context.registerReceiver(aclConnReceiver, + new IntentFilter(BluetoothDevice.ACTION_ACL_CONNECTED)); } - public void addConfig(ConnectionConfiguration config) { if (isShutdown) { - Log.w(TAG, "BluetoothClient is shutdown, ignoring addConfig"); + Log.w(TAG, "Client is shutdown, ignoring addConfig"); return; } - validateConfig(config); - if (btAdapter == null || !btAdapter.isEnabled()) { - Log.w(TAG, "Bluetooth not enabled, deferring connection"); - configurations.put(config.address, config); - return; - } + validateConfig(config); String address = config.address; - if (configurations.containsKey(address)) { - Log.d(TAG, "Configuration already exists for " + address + ", reconnecting"); + synchronized (this) { + if (configurations.containsKey(address)) { + Log.d(TAG, "Configuration already exists for " + address + ", updating"); - configurations.put(address, config); + configurations.put(address, config); - BluetoothConnectionThread thread = connections.get(address); - - if (thread != null) { - if (thread.isConnectionHealthy()) { - Log.d(TAG, "Connection is active for " + address + ", ignoring retry"); - return; - } - - if (isThreadActive(thread)) { - Log.d(TAG, "Thread active but connection unhealthy, triggering retry"); - thread.retryConnection(); + BluetoothConnectionThread thread = connections.get(address); + if (thread != null) { + if (thread.isConnectionHealthy()) { + Log.d(TAG, "Connection is healthy, ignoring retry"); + } else { + Log.d(TAG, "Connection unhealthy, triggering retry"); + thread.resetBackoffAndRetryConnection(); + } } else { - Log.d(TAG, "Thread not active, starting new connection"); - connections.remove(address); startConnection(config); } - } - return; - } + return; + } - configurations.put(address, config); - startConnection(config); + configurations.put(address, config); + if (btAdapter != null && btAdapter.isEnabled()) { + startConnection(config); + } else { + Log.w(TAG, "Bluetooth disabled, deferring connection"); + } + } } public void removeConfig(ConnectionConfiguration config) { @@ -151,13 +129,15 @@ public void removeConfig(ConnectionConfiguration config) { String address = config.address; Log.d(TAG, "Removing configuration for " + address); - BluetoothConnectionThread thread = connections.get(address); - if (thread != null) { - thread.close(); - connections.remove(address); - } + synchronized (this) { + BluetoothConnectionThread thread = connections.get(address); + if (thread != null) { + thread.close(); + connections.remove(address); + } - configurations.remove(address); + configurations.remove(address); + } } private void startConnection(ConnectionConfiguration config) { @@ -167,98 +147,75 @@ private void startConnection(ConnectionConfiguration config) { String address = config.address; - if (connections.containsKey(address)) { - Log.d(TAG, "Connection already active for " + address); - return; - } + synchronized (this) { + if (connections.containsKey(address)) { + Log.d(TAG, "Connection already active for " + address); + return; + } - if (btAdapter == null || !btAdapter.isEnabled()) { - Log.w(TAG, "Bluetooth not available, deferring connection for " + address); - return; - } + if (btAdapter == null || !btAdapter.isEnabled()) { + Log.w(TAG, "Bluetooth not available, deferring connection"); + return; + } - Log.d(TAG, "Starting Bluetooth connection for " + address); - BluetoothConnectionThread thread = new BluetoothConnectionThread(context, config, btAdapter, wearableImpl); - connections.put(address, thread); - thread.start(); - } + Log.d(TAG, "Starting connection for " + address); - private void onAclConnected(BluetoothDevice device) { - String address = device.getAddress(); - ConnectionConfiguration config = configurations.get(address); - if (config != null) { - Log.d(TAG, "ACL_CONNECTED for configured device " + address + ", attempting reconnection"); - retryConnection(config, false); + BluetoothConnectionThread thread = new BluetoothConnectionThread( + context, config, btAdapter, wearableImpl, executor, bleDiscoverer + ); + + connections.put(address, thread); + thread.start(); } } - private void onAclDisconnected(String address) { - Log.d(TAG, "ACL_DISCONNECTED for " + address); - } + private void onAclConnected(BluetoothDevice device) { + String address = device.getAddress(); - private void onBtAdapterStateChaged(int state) { - Log.d(TAG, "Bluetooth adapter state changed to " + state); + synchronized (this) { + ConnectionConfiguration config = configurations.get(address); + if (config != null) { + Log.d(TAG, "ACL_CONNECTED for configured device " + address + + ", attempting reconnection"); - if (state == BluetoothAdapter.STATE_ON) { - for (ConnectionConfiguration config: configurations.values()) { - String address = config.address; - if (!connections.containsKey(address)) + BluetoothConnectionThread thread = connections.get(address); + if (thread != null) { + thread.retryConnection(); + } else { startConnection(config); + } } - } else if (state == BluetoothAdapter.STATE_OFF) { - if (btAdapter != null && btAdapter.isEnabled()) { - Log.d(TAG, "Ignoring STATE_OFF broadcast - adapter is still enabled"); - return; - } - for (BluetoothConnectionThread thread : connections.values()) { - thread.close(); - } - connections.clear(); } } - public void retryConnection(ConnectionConfiguration config, boolean immediate) { - if (isShutdown) { - return; - } - validateConfig(config); - - String address = config.address; - if (!configurations.containsKey(address)) { - Log.w(TAG, "Configuration not found for " + address); - return; - } - - if (btAdapter == null || !btAdapter.isEnabled()) { - Log.w(TAG, "Bluetooth not enabled, cannot retry connection"); - return; - } - - BluetoothConnectionThread thread = connections.get(address); - - if (thread == null) { - Log.d(TAG, "No connection thread exists for " + address + ", starting new one"); - startConnection(config); - return; - } - - if (!isThreadActive(thread)) { - Log.d(TAG, "Connection thread not active for " + address + ", starting new one"); - connections.remove(address); - startConnection(config); - return; - } + private void onBluetoothStateChanged(int state) { + Log.d(TAG, "Bluetooth state changed to " + state); + + synchronized (this) { + if (state == BluetoothAdapter.STATE_ON) { + for (ConnectionConfiguration config : configurations.values()) { + String address = config.address; + if (!connections.containsKey(address)) { + startConnection(config); + } else { + BluetoothConnectionThread thread = connections.get(address); + if (thread != null) { + thread.resetBackoffAndRetryConnection(); + } + } + } + } else if (state == BluetoothAdapter.STATE_OFF) { + if (btAdapter != null && btAdapter.isEnabled()) { + Log.d(TAG, "Ignoring STATE_OFF - adapter still enabled"); + return; + } - if (immediate) { - thread.retryConnection(); - } else { - thread.scheduleRetry(); + for (BluetoothConnectionThread thread : connections.values()) { + thread.close(); + } + connections.clear(); + } } - - } - - private boolean isThreadActive(BluetoothConnectionThread thread) { - return thread != null && thread.isAlive() && !thread.isInterrupted(); } private static void validateConfig(ConnectionConfiguration config){ @@ -273,31 +230,39 @@ private static void validateConfig(ConnectionConfiguration config){ throw new IllegalArgumentException("Role is not client: " + config.role); } + @RequiresPermission(Manifest.permission.BLUETOOTH_SCAN) @Override public void close() { if (isShutdown) { return; } + Log.d(TAG, "Shutting down BluetoothClient"); isShutdown = true; - for (BluetoothConnectionThread thread : connections.values()) { - thread.close(); - } + synchronized (this) { + for (BluetoothConnectionThread thread : connections.values()) { + thread.close(); + } - for (BluetoothConnectionThread thread : connections.values()) { - try { - thread.join(5000); - if (thread.isAlive()) { - Log.w(TAG, "Thread did not stop in time: " + thread.getName()); + for (BluetoothConnectionThread thread : connections.values()) { + try { + thread.join(5000); + if (thread.isAlive()) { + Log.w(TAG, "Thread did not stop in time: " + thread.getName()); + } + } catch (InterruptedException e) { + Log.w(TAG, "Interrupted while waiting for thread", e); } - } catch (InterruptedException e) { - Log.w(TAG, "Interrupted while waiting for thread to finish", e); } + + connections.clear(); + configurations.clear(); } - connections.clear(); - configurations.clear(); + bleDiscoverer.shutdown(); + + executor.shutdownNow(); try { context.unregisterReceiver(btStateReceiver); @@ -308,7 +273,7 @@ public void close() { try { context.unregisterReceiver(aclConnReceiver); } catch (Exception e) { - Log.w(TAG, "Error unregistering aclConnReceiver", e); + Log.w(TAG, "Error unregistering aclConnectedReceiver", e); } Log.d(TAG, "BluetoothClient closed"); diff --git a/play-services-wearable/core/src/main/java/org/microg/gms/wearable/bluetooth/BluetoothConnectionThread.java b/play-services-wearable/core/src/main/java/org/microg/gms/wearable/bluetooth/BluetoothConnectionThread.java index 54fea06bef..a74fabf03b 100644 --- a/play-services-wearable/core/src/main/java/org/microg/gms/wearable/bluetooth/BluetoothConnectionThread.java +++ b/play-services-wearable/core/src/main/java/org/microg/gms/wearable/bluetooth/BluetoothConnectionThread.java @@ -1,11 +1,17 @@ package org.microg.gms.wearable.bluetooth; import android.Manifest; +import android.app.PendingIntent; import android.bluetooth.BluetoothAdapter; import android.bluetooth.BluetoothDevice; import android.bluetooth.BluetoothSocket; +import android.content.BroadcastReceiver; import android.content.Context; +import android.content.Intent; +import android.content.IntentFilter; +import android.net.Uri; import android.os.PowerManager; +import android.os.SystemClock; import android.util.Log; import androidx.annotation.RequiresPermission; @@ -21,54 +27,92 @@ import java.io.Closeable; import java.io.IOException; import java.util.UUID; +import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.atomic.AtomicBoolean; -import java.util.concurrent.atomic.AtomicInteger; +import java.util.concurrent.locks.Condition; +import java.util.concurrent.locks.Lock; +import java.util.concurrent.locks.ReentrantLock; public class BluetoothConnectionThread extends Thread implements Closeable { private static final String TAG = "GmsWearBtConnThread"; private static final UUID WEAR_BT_UUID = UUID.fromString("5e8945b0-9525-11e3-a5e2-0800200c9a66"); - private static final int MAX_RETRY_DELAY_MS = 60000; - private static final int MIN_RETRY_DELAY_MS = 1000; - private static final int BACKOFF_MULTIPLIEER = 2; private static final long MIN_ATTEMPT_INTERVAL_MS = 3000; - - private volatile boolean isConnected = false; - private volatile long lastActivityTime = 0; + private static final long SOCKET_CONNECT_TIMEOUT_MS = 30000; private static final long ACTIVITY_TIMEOUT_MS = 5000; private final Context context; private final ConnectionConfiguration config; private final BluetoothAdapter btAdapter; + private final BluetoothDevice btDevice; + private final WearableImpl wearableImpl; + private final ScheduledExecutorService executor; + + private final WakeLockManager wakeLockManager; + private final RetryStrategy retryStrategy; + private final AlarmManagerHelper alarmHelper; + private final BleDeviceDiscoverer bleDiscoverer; // Nullable + private final Lock lock = new ReentrantLock(); + private final Condition retryCondition = lock.newCondition(); private final AtomicBoolean running = new AtomicBoolean(true); - private final AtomicInteger retryCount = new AtomicInteger(0); private final AtomicBoolean immediateRetry = new AtomicBoolean(false); + private volatile boolean isConnected = false; + private volatile long lastActivityTime = 0; private long lastAttemptTime = 0; private BluetoothSocket socket; private WearableConnection wearableConnection; - private final WearableImpl wearableImpl; - - private final PowerManager.WakeLock wakeLock; - private static final long SOCKET_CONNECT_TIMEOUT_MS = 30000; + private BroadcastReceiver retryReceiver; + private boolean receiverRegistered = false; - public BluetoothConnectionThread(Context context, ConnectionConfiguration config, BluetoothAdapter btAdapter, WearableImpl wearableImpl) { + public BluetoothConnectionThread(Context context, ConnectionConfiguration config, + BluetoothAdapter btAdapter, WearableImpl wearableImpl, + ScheduledExecutorService executor, BleDeviceDiscoverer bleDiscoverer) { super("BtThread-" + config.address); this.context = context; this.config = config; this.btAdapter = btAdapter; this.wearableImpl = wearableImpl; - PowerManager pm = (PowerManager) context.getSystemService(Context.POWER_SERVICE); - wakeLock = pm.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, - "GmsWear:BtConnect:" + config.address); - wakeLock.setReferenceCounted(false); + + this.executor = executor; + this.bleDiscoverer = bleDiscoverer; + + this.btDevice = btAdapter.getRemoteDevice(config.address); + + this.wakeLockManager = new WakeLockManager(context, + "BtConnect:" + config.address, executor); + this.retryStrategy = RetryStrategy.fromPolicy(config.connectionRetryStrategy); + this.alarmHelper = new AlarmManagerHelper(context); + + registerRetryReceiver(); } + private void registerRetryReceiver() { + retryReceiver = new BroadcastReceiver() { + @Override + public void onReceive(Context context, Intent intent) { + if (intent != null && "com.google.android.gms.wearable.RETRY_CONNECTION".equals(intent.getAction())) { + String address = intent.getData() != null ? intent.getData().getAuthority() : null; + if (config.address.equals(address)) { + Log.d(TAG, "Alarm triggered retry for " + config.address); + signalRetry(); + } + } + } + }; + + IntentFilter filter = new IntentFilter("com.google.android.gms.wearable.RETRY_CONNECTION"); + filter.addDataScheme("wearable"); + + context.registerReceiver(retryReceiver, filter); + receiverRegistered = true; + } + public boolean isConnectionHealthy(){ if (!isConnected || wearableConnection == null) { return false; @@ -88,69 +132,43 @@ public void run(){ Log.d(TAG, "Bluetooth connection thread started for " + config.address); while (running.get() && !isInterrupted()) { - enforceMinInterval(); - - if (!running.get()) break; - try { - if (!wakeLock.isHeld()) { - wakeLock.acquire(5 * 60 * 1000L); - Log.d(TAG, "Wake lock acquired for connection attempt"); - } - } catch (Exception e) { - Log.w(TAG, "Failed to acquire wake lock", e); - } + enforceMinInterval(); - try { - connect(); - retryCount.incrementAndGet(); - } catch (IOException e) { - Log.w(TAG, "Connection failed for " + config.address + ": " + e.getMessage()); - retryCount.incrementAndGet(); - } catch (InterruptedException e) { - Log.d(TAG, "Connection thread interrupted"); - if (!running.get()) { - break; - } - } catch (Exception e) { - Log.e(TAG, "Unexpected error in connection loop", e); - retryCount.incrementAndGet(); - } finally { - closeSocket(); + if (!running.get()) break; + + wakeLockManager.acquire("connect", SOCKET_CONNECT_TIMEOUT_MS + 5000); try { - if (wakeLock.isHeld()) { - wakeLock.release(); - Log.d(TAG, "Wake lock released"); - } + connect(); + retryStrategy.reset(); + + } catch (IOException e) { + Log.w(TAG, "Connection failed: " + e.getMessage()); + } catch (InterruptedException e) { + Log.d(TAG, "Connection interrupted"); + if (!running.get()) break; } catch (Exception e) { - Log.w(TAG, "Failed to release wake lock", e); + Log.e(TAG, "Unexpected error", e); + } finally { + closeSocket(); + wakeLockManager.release("connect"); } - } - if (running.get() && !isInterrupted()) { - try { + if (running.get() && !isInterrupted()) { waitForRetry(); - } catch (InterruptedException e) { - Log.d(TAG, "Retry wait interrupted"); - if (!running.get()) { - break; - } } - } - } - - closeSocket(); - try { - if (wakeLock.isHeld()) { - wakeLock.release(); + } catch (InterruptedException e) { + Log.d(TAG, "Thread interrupted"); + if (!running.get()) break; + } catch (Exception e) { + Log.e(TAG, "Unexpected error in main loop", e); } - } catch (Exception e) { - Log.w(TAG, "Failed to release wake lock in cleanup", e); } Log.d(TAG, "Bluetooth connection thread stopped for " + config.address); + cleanup(); } private void enforceMinInterval() { @@ -172,85 +190,60 @@ private void enforceMinInterval() { @RequiresPermission(allOf = {Manifest.permission.BLUETOOTH_CONNECT, Manifest.permission.BLUETOOTH_SCAN}) private void connect() throws IOException, InterruptedException { - if (!WearableImpl.BluetoothConnectionLock.tryAcquire(config.address, "BTLock")) { - throw new IOException("Connection lock held by another app"); + if (!running.get() || btAdapter == null || !btAdapter.isEnabled()) { + throw new IOException("Bluetooth not available"); } - try { - - if (!running.get() || btAdapter == null || !btAdapter.isEnabled()) { - throw new IOException("Bluetooth not available"); - } - - BluetoothDevice device = btAdapter.getRemoteDevice(config.address); - if (device == null) throw new IOException("Could not get remote device"); + Log.d(TAG, "Connecting to " + config.address); - Log.d(TAG, "Connecting to " + config.address + " via " + getConnectionTypeName()); - - if (config.type != WearableImpl.TYPE_BLUETOOTH_RFCOMM && config.type != 5) { - return; - } + socket = btDevice.createRfcommSocketToServiceRecord(WEAR_BT_UUID); - socket = device.createRfcommSocketToServiceRecord(WEAR_BT_UUID); - - if (btAdapter.isDiscovering()) btAdapter.cancelDiscovery(); + if (btAdapter.isDiscovering()) { + btAdapter.cancelDiscovery(); + } - connectSocketWithTimeout(socket); - Log.d(TAG, "Socket connected to " + config.address); + connectSocketWithTimeout(socket); - retryCount.set(0); - isConnected = true; - markActivity(); + Log.d(TAG, "Socket connected to " + config.address); - wearableConnection = new BluetoothWearableConnection(socket, config.nodeId, new ConnectionListener(context, config, wearableImpl, this)); - wearableConnection.run(); + isConnected = true; + markActivity(); - } finally { - isConnected = false; - WearableImpl.BluetoothConnectionLock.release(config.address, "BTLock"); - } + wearableConnection = new BluetoothWearableConnection( + socket, config.nodeId, + new ConnectionListener(context, config, wearableImpl, this) + ); + wearableConnection.run(); } + @RequiresPermission(Manifest.permission.BLUETOOTH_CONNECT) private void connectSocketWithTimeout(BluetoothSocket socket) throws IOException, InterruptedException { final AtomicBoolean connected = new AtomicBoolean(false); final AtomicBoolean timedOut = new AtomicBoolean(false); - final AtomicBoolean connectFailed = new AtomicBoolean(false); - final Object lock = new Object(); + final Object connectLock = new Object(); final IOException[] exception = new IOException[1]; - Thread connectThread = new Thread(new Runnable() { - @RequiresPermission(Manifest.permission.BLUETOOTH_CONNECT) - @Override - public void run() { - try { - synchronized (lock) { - if (timedOut.get()) { - Log.w(TAG, "Connect aborted - already timed out"); - return; - } - } + Thread connectThread = new Thread(() -> { + try { + synchronized (connectLock) { + if (timedOut.get()) return; + } - socket.connect(); - - synchronized (lock) { - if (!timedOut.get()) { - connected.set(true); - Log.d(TAG, "Socket connect succeeded"); - } else { - Log.w(TAG, "Socket connect succeeded but timeout already occurred"); - try { - socket.close(); - } catch (IOException e) { - Log.w(TAG, "Failed to close socket after timeout", e); - } - } + socket.connect(); + + synchronized (connectLock) { + if (!timedOut.get()) { + connected.set(true); + } else { + try { + socket.close(); + } catch (IOException ignored) {} } - } catch (IOException e) { - synchronized (lock) { - if (!timedOut.get()) { - exception[0] = e; - connectFailed.set(true); - } + } + } catch (IOException e) { + synchronized (connectLock) { + if (!timedOut.get()) { + exception[0] = e; } } } @@ -262,12 +255,12 @@ public void run() { long endTime = startTime + SOCKET_CONNECT_TIMEOUT_MS; while (System.currentTimeMillis() < endTime && running.get()) { - synchronized (lock) { + synchronized (connectLock) { if (connected.get()) { return; } - if (connectFailed.get()) { + if (exception[0] != null) { throw exception[0]; } } @@ -280,90 +273,162 @@ public void run() { } } - synchronized (lock) { + synchronized (connectLock) { if (!connected.get()) { timedOut.set(true); Log.e(TAG, "Socket connect timed out after " + SOCKET_CONNECT_TIMEOUT_MS + "ms"); try { socket.close(); - } catch (IOException e) { - Log.w(TAG, "Failed to close socket after timeout", e); - } + } catch (IOException ignored) {} connectThread.interrupt(); - throw new IOException("Socket connect timed out after " + SOCKET_CONNECT_TIMEOUT_MS + "ms"); + throw new IOException("Socket connect timed out"); } } } + @RequiresPermission(Manifest.permission.BLUETOOTH_SCAN) private void waitForRetry() throws InterruptedException { if (!running.get()) return; + long delayMs = retryStrategy.nextDelayMs(); + + if (delayMs < 0) { + Log.d(TAG, "Retry strategy OFF, waiting for external trigger"); + waitForExternalRetry(); + return; + } + if (immediateRetry.getAndSet(false)) { - Log.d(TAG, "Immediate retry flag set, skipping delay"); + Log.d(TAG, "Immediate retry requested"); + wakeLockManager.acquire("retry", delayMs + 5000); return; } - int count = retryCount.get(); - int delay = calcRetryDelay(count); - Log.d(TAG, "Waiting " + delay + "ms before retry #" + count + " for " + config.address); + Log.d(TAG, String.format("Waiting %dms before retry", delayMs)); + + if (delayMs > 60_000) { + useAlarmManagerForRetry(delayMs); + } else { + useThreadSleepForRetry(delayMs); + } + } + + @RequiresPermission(Manifest.permission.BLUETOOTH_SCAN) + private void useAlarmManagerForRetry(long delayMs) throws InterruptedException { + Log.d(TAG, "Using AlarmManager for retry delay"); + if (bleDiscoverer != null && config.type == 5) { + bleDiscoverer.addDevice(btDevice, device -> { + Log.d(TAG, "BLE discovered device, triggering retry"); + signalRetry(); + }); + } + + long triggerTime = SystemClock.elapsedRealtime() + delayMs; + PendingIntent pendingIntent = createRetryPendingIntent(); + + alarmHelper.setExactAndAllowWhileIdle( + "WearRetry", + android.app.AlarmManager.ELAPSED_REALTIME_WAKEUP, + triggerTime, + pendingIntent + ); + + wakeLockManager.release("retry-wait"); + + lock.lock(); try { - Thread.sleep(delay); - } catch (InterruptedException e) { - if (!running.get()) { - Log.d(TAG, "Sleep interrupted for close"); - } else { - Log.d(TAG, "Sleep interrupted for retry"); + while (running.get() && !immediateRetry.get()) { + retryCondition.await(); + } + immediateRetry.set(false); + } finally { + lock.unlock(); + } + + alarmHelper.cancel(pendingIntent); + + wakeLockManager.acquire("retry", 60_000); + } + + private void useThreadSleepForRetry(long delayMs) throws InterruptedException { + lock.lock(); + try { + long endTime = System.currentTimeMillis() + delayMs; + + while (running.get() && !immediateRetry.get()) { + long remaining = endTime - System.currentTimeMillis(); + if (remaining <= 0) break; + + retryCondition.await(remaining, java.util.concurrent.TimeUnit.MILLISECONDS); } + + immediateRetry.set(false); + } finally { + lock.unlock(); } + + wakeLockManager.acquire("retry", 60_000); } private void waitForExternalRetry() { Log.d(TAG, "Waiting for external retry trigger for " + config.address); + wakeLockManager.release("wait-external"); + + lock.lock(); try { while (running.get() && !immediateRetry.get()) { - Thread.sleep(5000); + retryCondition.await(); } immediateRetry.set(false); - retryCount.set(0); } catch (InterruptedException e) { - if (running.get()) { - Log.d(TAG, "External retry triggered for " + config.address); - retryCount.set(0); - } + throw new RuntimeException(e); + } finally { + lock.unlock(); } + + wakeLockManager.acquire("external-retry", 60_000); } - private int calcRetryDelay(int retryCount) { - int delay = MIN_RETRY_DELAY_MS * (int)Math.pow(BACKOFF_MULTIPLIEER, Math.min(retryCount - 1, 6)); - return Math.min(delay, MAX_RETRY_DELAY_MS); + private void signalRetry() { + lock.lock(); + try { + immediateRetry.set(true); + retryCondition.signal(); + } finally { + lock.unlock(); + } } - public void retryConnection(){ - Log.d(TAG, "Immediate retry requested for " + config.address); - retryCount.set(0); - immediateRetry.set(true); - interrupt(); + public void resetBackoffAndRetryConnection() { + Log.d(TAG, "Reset backoff and retry requested"); + retryStrategy.reset(); + signalRetry(); } - public void resetBackoff() { - Log.d(TAG, "Resetting backoff for " + config.address); - retryCount.set(0); - lastAttemptTime = 0; + public void retryConnection(){ + Log.d(TAG, "Retry requested"); + signalRetry(); } - public void scheduleRetry() { - // TODO: find better approach - retryCount.set(0); - interrupt(); + private PendingIntent createRetryPendingIntent() { + Intent intent = new Intent("com.google.android.gms.wearable.RETRY_CONNECTION"); + intent.setData(new Uri.Builder() + .scheme("wearable") + .authority(config.address) + .build()); + intent.setPackage(context.getPackageName()); + + return AlarmManagerHelper.createPendingIntent(context, 1, intent); } private void closeSocket() { isConnected = false; + if (socket != null) { try { socket.close(); @@ -372,22 +437,35 @@ private void closeSocket() { } socket = null; } + wearableConnection = null; } - private String getConnectionTypeName() { - switch (config.type) { - case 5: return "RFCOMM (type 5)"; - case WearableImpl.TYPE_BLUETOOTH_RFCOMM: return "RFCOMM"; - default: return "Unknown"; + @RequiresPermission(Manifest.permission.BLUETOOTH_SCAN) + private void cleanup() { + closeSocket(); + + if (bleDiscoverer != null) { + bleDiscoverer.removeDevice(btDevice); } + + if (receiverRegistered) { + try { + context.unregisterReceiver(retryReceiver); + } catch (Exception e) { + Log.w(TAG, "Error unregistering receiver", e); + } + receiverRegistered = false; + } + + wakeLockManager.shutdown(); } @Override public void close(){ - Log.d(TAG, "Closing Bluetooth connection for " + config.address); + Log.d(TAG, "Closing connection thread for " + config.address); running.set(false); - closeSocket(); + signalRetry(); interrupt(); } diff --git a/play-services-wearable/core/src/main/java/org/microg/gms/wearable/bluetooth/BluetoothWearableConnection.java b/play-services-wearable/core/src/main/java/org/microg/gms/wearable/bluetooth/BluetoothWearableConnection.java index 6997c3ec4b..4321f33099 100644 --- a/play-services-wearable/core/src/main/java/org/microg/gms/wearable/bluetooth/BluetoothWearableConnection.java +++ b/play-services-wearable/core/src/main/java/org/microg/gms/wearable/bluetooth/BluetoothWearableConnection.java @@ -13,6 +13,7 @@ import org.microg.gms.profile.Build; import org.microg.gms.wearable.WearableConnection; import org.microg.gms.wearable.proto.Connect; +import org.microg.gms.wearable.proto.Heartbeat; import org.microg.gms.wearable.proto.MessagePiece; import org.microg.gms.wearable.proto.RootMessage; @@ -43,6 +44,10 @@ public class BluetoothWearableConnection extends WearableConnection { private static final long READ_TIMEOUT_MS = 60000; private static final long HANDSHAKE_TIMEOUT_MS = 30000; + private static final long HEARTBEAT_INTERVAL_MS = 20000; + private volatile boolean heartbeatEnabled = false; + private Thread heartbeatThread; + public BluetoothWearableConnection(BluetoothSocket socket, String localNodeId, Listener listener) throws IOException { super(listener); this.socket = socket; @@ -172,6 +177,8 @@ protected void writeMessagePiece(MessagePiece piece) throws IOException { public void run() { readerThread = Thread.currentThread(); + startHeartbeat(); + try { // Perform handshake first if (!handshake()) { @@ -189,10 +196,58 @@ public void run() { } catch (Exception e) { Log.e(TAG, "Error in connection run loop", e); } finally { + stopHeartbeat(); readerThread = null; } } + private void startHeartbeat() { + heartbeatEnabled = true; + heartbeatThread = new Thread(() -> { + Log.d(TAG, "Heartbeat thread started for peer: " + peerNodeId); + while (heartbeatEnabled && !isClosed()) { + try { + Thread.sleep(HEARTBEAT_INTERVAL_MS); + + if (heartbeatEnabled && !isClosed()) { + Log.d(TAG, "Sending heartbeat to " + peerNodeId); + writeMessage(new RootMessage.Builder() + .heartbeat(new Heartbeat()) + .build()); + } + } catch (InterruptedException e) { + Log.d(TAG, "Heartbeat thread interrupted"); + break; + } catch (IOException e) { + Log.w(TAG, "Failed to send heartbeat, closing connection", e); + try { + close(); + } catch (IOException ex) { + Log.w(TAG, "Error closing connection", ex); + } + break; + } + } + Log.d(TAG, "Heartbeat thread stopped for peer: " + peerNodeId); + }, "BtHeartbeat-" + peerNodeId); + + heartbeatThread.setDaemon(true); + heartbeatThread.start(); + } + + private void stopHeartbeat() { + heartbeatEnabled = false; + if (heartbeatThread != null) { + heartbeatThread.interrupt(); + try { + heartbeatThread.join(1000); + } catch (InterruptedException e) { + // Ignore + } + heartbeatThread = null; + } + } + protected MessagePiece readMessagePiece() throws IOException { if (isClosed.get()) { throw new IOException("Socket not connected"); @@ -257,6 +312,8 @@ public void close() throws IOException { Log.d(TAG, "Closing Bluetooth wearable connection"); + stopHeartbeat(); + Thread reader = readerThread; if (reader != null && reader != Thread.currentThread()) { reader.interrupt(); diff --git a/play-services-wearable/core/src/main/java/org/microg/gms/wearable/bluetooth/RetryStrategy.java b/play-services-wearable/core/src/main/java/org/microg/gms/wearable/bluetooth/RetryStrategy.java new file mode 100644 index 0000000000..fb1b0eb416 --- /dev/null +++ b/play-services-wearable/core/src/main/java/org/microg/gms/wearable/bluetooth/RetryStrategy.java @@ -0,0 +1,156 @@ +package org.microg.gms.wearable.bluetooth; + +import android.util.Log; + +public class RetryStrategy { + private static final String TAG = "RetryStrategy"; + + public static final int POLICY_DEFAULT = 0; + public static final int POLICY_AGGRESSIVE = 1; + public static final int POLICY_LOW_POWER = 2; + public static final int POLICY_OFF = 3; + + private static final RetryParams DEFAULT_PARAMS = new RetryParams( + 6,30000L, 320000L); + + private static final RetryParams AGGRESSIVE_PARAMS = new RetryParams( + 6,30000L, 320000L); + + private static final RetryParams LOW_POWER_PARAMS = new RetryParams( + 10,600000L, 1024000L); + + private static final RetryParams OFF_PARAMS = new RetryParams( + 0,0L, -1L); + + private final int maxRetryStep; + private final long totalRetryTimeLimitMs; + private final long retryDelayAtLimitMs; + + private long currentRetryCount = 0; + private long cumulativeDelayMs = 0; + private long lastResetTime = System.currentTimeMillis(); + + public static RetryStrategy fromPolicy(int policy) { + RetryParams params; + switch (policy) { + case POLICY_AGGRESSIVE: + params = AGGRESSIVE_PARAMS; + break; + case POLICY_LOW_POWER: + params = LOW_POWER_PARAMS; + break; + case POLICY_OFF: + params = OFF_PARAMS; + break; + case POLICY_DEFAULT: + default: + params = DEFAULT_PARAMS; + break; + } + + Log.d(TAG, String.format("Created retry strategy: policy=%s, params=%s", + policyToString(policy), params)); + + return new RetryStrategy(params.maxRetryStep, params.totalRetryTimeLimit, + params.retryDelayAtLimit); + } + + public RetryStrategy(int maxRetryStep, long totalRetryTimeLimitMs, long retryDelayAtLimitMs) { + this.maxRetryStep = maxRetryStep; + this.totalRetryTimeLimitMs = totalRetryTimeLimitMs; + this.retryDelayAtLimitMs = retryDelayAtLimitMs; + } + + public long nextDelayMs() { + if (retryDelayAtLimitMs < 0) { + Log.d(TAG, "Retry strategy is OFF, returning -1"); + return -1; + } + + long retryCount = Math.min(maxRetryStep, currentRetryCount + 1); + currentRetryCount = retryCount; + + // exponential increase + long delay = (1L << (int)(retryCount - 1)) * 1000L; + + long newCumulativeMs = cumulativeDelayMs + delay; + cumulativeDelayMs = newCumulativeMs; + + Log.d(TAG, String.format("nextDelay: retryCount=%d, delay=%dms, cumulative=%dms/%dms", + retryCount, delay, newCumulativeMs, totalRetryTimeLimitMs)); + + if (totalRetryTimeLimitMs >= 0 && newCumulativeMs >= totalRetryTimeLimitMs) { + Log.w(TAG, String.format( + "Cumulative retry time limit exceeded (%dms >= %dms), returning fallback delay: %dms", + newCumulativeMs, totalRetryTimeLimitMs, retryDelayAtLimitMs)); + + return retryDelayAtLimitMs; + } + + return delay; + } + + public void disableRetries() { + Log.d(TAG, "Disabling retries"); + currentRetryCount = maxRetryStep; + cumulativeDelayMs = Math.max(cumulativeDelayMs, totalRetryTimeLimitMs); + } + + public void reset() { + Log.d(TAG, String.format("Resetting retry state (was: retryCount=%d, cumulative=%dms)", + currentRetryCount, cumulativeDelayMs)); + + currentRetryCount = 0; + cumulativeDelayMs = 0; + lastResetTime = System.currentTimeMillis(); + } + + public boolean isEnabled() { + return retryDelayAtLimitMs >= 0; + } + + public boolean hasExceededLimit() { + return totalRetryTimeLimitMs >= 0 && cumulativeDelayMs >= totalRetryTimeLimitMs; + } + + public long getRetryCount() { + return currentRetryCount; + } + + public long getCumulativeDelayMs() { + return cumulativeDelayMs; + } + + public long getTimeSinceResetMs() { + return System.currentTimeMillis() - lastResetTime; + } + + private static String policyToString(int policy) { + switch (policy) { + case POLICY_DEFAULT: return "DEFAULT"; + case POLICY_AGGRESSIVE: return "AGGRESSIVE"; + case POLICY_LOW_POWER: return "LOW_POWER"; + case POLICY_OFF: return "OFF"; + default: return "UNKNOWN(" + policy + ")"; + } + } + + private static class RetryParams { + final int maxRetryStep; + final long totalRetryTimeLimit; + final long retryDelayAtLimit; + + RetryParams(int maxRetryStep, long totalRetryTimeLimit, long retryDelayAtLimit) { + this.maxRetryStep = maxRetryStep; + this.totalRetryTimeLimit = totalRetryTimeLimit; + this.retryDelayAtLimit = retryDelayAtLimit; + } + + @Override + public String toString() { + return "RetryParams{maxStep=" + maxRetryStep + + ", timeLimit=" + totalRetryTimeLimit + + ", fallback=" + retryDelayAtLimit + "}"; + } + } +} \ No newline at end of file diff --git a/play-services-wearable/core/src/main/java/org/microg/gms/wearable/bluetooth/WakeLockManager.java b/play-services-wearable/core/src/main/java/org/microg/gms/wearable/bluetooth/WakeLockManager.java new file mode 100644 index 0000000000..1543e7604f --- /dev/null +++ b/play-services-wearable/core/src/main/java/org/microg/gms/wearable/bluetooth/WakeLockManager.java @@ -0,0 +1,214 @@ +package org.microg.gms.wearable.bluetooth; + +import android.content.Context; +import android.os.PowerManager; +import android.util.Log; + +import java.util.HashMap; +import java.util.Map; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.ScheduledFuture; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicInteger; + +public class WakeLockManager { + private static final String TAG = "WakeLockManager"; + private static final long DEFAULT_TIMEOUT_MS = 5 * 60 * 1000L; + private static final long MAX_TIMEOUT_MS = 10 * 60 * 1000L; + + private final Context context; + private final PowerManager.WakeLock wakeLock; + private final Object lock = new Object(); + private final ScheduledExecutorService executor; + + private final AtomicInteger refCount = new AtomicInteger(0); + private final Map tagCounts = new HashMap<>(); + + private ScheduledFuture timeoutFuture; + private long acquireTimeMs = 0; + private long maxTimeoutMs = MAX_TIMEOUT_MS; + + private int totalAcquires = 0; + private int totalReleases = 0; + private int forceReleaseCount = 0; + private boolean isForceReleased = false; + + private volatile boolean isEnabled = true; + + public WakeLockManager(Context context, String tag, ScheduledExecutorService executor) { + this.context = context; + this.executor = executor; + + PowerManager pm = (PowerManager) context.getSystemService(Context.POWER_SERVICE); + if (pm == null) { + throw new IllegalStateException("PowerManager not available"); + } + + this.wakeLock = pm.newWakeLock( + PowerManager.PARTIAL_WAKE_LOCK, + "GmsWear:" + tag + ); + this.wakeLock.setReferenceCounted(false); + } + + public void acquire() { + acquire(null, DEFAULT_TIMEOUT_MS); + } + + public void acquire(long timeoutMs) { + acquire(null, timeoutMs); + } + + public void acquire(String tag, long timeoutMs) { + synchronized (lock) { + if (!isEnabled) { + Log.w(TAG, "Wake lock disabled, ignoring acquire request"); + return; + } + + int count = refCount.incrementAndGet(); + totalAcquires++; + + if (tag != null) { + Integer tagCount = tagCounts.get(tag); + tagCounts.put(tag, tagCount == null ? 1 : tagCount + 1); + } + + Log.d(TAG, String.format("acquire(tag=%s, timeout=%dms) refCount=%d", + tag, timeoutMs, count)); + + if (count == 1) { + try { + wakeLock.acquire(DEFAULT_TIMEOUT_MS); + acquireTimeMs = System.currentTimeMillis(); + isForceReleased = false; + + Log.d(TAG, "Wake lock acquired (first reference)"); + } catch (Exception e) { + Log.e(TAG, "Failed to acquire wake lock", e); + refCount.decrementAndGet(); + throw e; + } + } + + if (timeoutMs > 0) { + long effectiveTimeout = Math.min(timeoutMs, MAX_TIMEOUT_MS); + + if (effectiveTimeout > maxTimeoutMs) { + maxTimeoutMs = effectiveTimeout; + scheduleTimeout(effectiveTimeout); + } + } + } + } + + public void release() { + release(null); + } + + public void release(String tag) { + synchronized (lock) { + int count = refCount.get(); + + if (count <= 0) { + Log.w(TAG, String.format("release(tag=%s) called but refCount=%d (already released)", + tag, count)); + return; + } + + count = refCount.decrementAndGet(); + totalReleases++; + + if (tag != null) { + Integer tagCount = tagCounts.get(tag); + if (tagCount != null) { + if (tagCount == 1) { + tagCounts.remove(tag); + } else { + tagCounts.put(tag, tagCount - 1); + } + } + } + + Log.d(TAG, String.format("release(tag=%s) refCount=%d", tag, count)); + + if (count == 0) { + doRelease(); + } + } + } + + public void forceRelease() { + synchronized (lock) { + int count = refCount.get(); + if (count > 0) { + Log.w(TAG, String.format("Force releasing wake lock (refCount=%d)", count)); + refCount.set(0); + tagCounts.clear(); + isForceReleased = true; + forceReleaseCount++; + doRelease(); + } + } + } + + public void setEnabled(boolean enabled) { + synchronized (lock) { + this.isEnabled = enabled; + if (!enabled) { + forceRelease(); + } + } + } + + public boolean isHeld() { + synchronized (lock) { + return refCount.get() > 0; + } + } + + public int getRefCount() { + return refCount.get(); + } + + private void doRelease() { + try { + if (timeoutFuture != null) { + timeoutFuture.cancel(false); + timeoutFuture = null; + } + + if (wakeLock.isHeld()) { + wakeLock.release(); + long heldDurationMs = System.currentTimeMillis() - acquireTimeMs; + Log.d(TAG, String.format("Wake lock released (held for %dms)", heldDurationMs)); + } + + maxTimeoutMs = MAX_TIMEOUT_MS; + acquireTimeMs = 0; + + } catch (Exception e) { + Log.e(TAG, "Error releasing wake lock", e); + } + } + + private void scheduleTimeout(long timeoutMs) { + if (timeoutFuture != null) { + timeoutFuture.cancel(false); + } + + timeoutFuture = executor.schedule(() -> { + Log.w(TAG, "Wake lock timeout - force releasing"); + forceRelease(); + }, timeoutMs, TimeUnit.MILLISECONDS); + + Log.d(TAG, String.format("Scheduled timeout in %dms", timeoutMs)); + } + + public void shutdown() { + synchronized (lock) { + isEnabled = false; + forceRelease(); + } + } +} \ No newline at end of file From 1eacce13e8b4749706ec9f8acc5da48bb1264db4 Mon Sep 17 00:00:00 2001 From: deadYokai Date: Fri, 6 Feb 2026 03:57:01 +0200 Subject: [PATCH 15/29] update code --- .../wearable/ClockworkNodePreferences.java | 33 +- .../gms/wearable/NodeDatabaseHelper.java | 30 +- .../gms/wearable/NodeMigrationController.java | 65 ++ .../org/microg/gms/wearable/WearableImpl.java | 195 ++++-- .../gms/wearable/WearableServiceImpl.java | 5 +- .../bluetooth/BleDeviceDiscoverer.java | 87 ++- .../bluetooth/BluetoothConnectionThread.java | 5 +- .../wearable/channel/ChannelException.java | 24 + .../gms/wearable/channel/ChannelManager.java | 633 ++++++++--------- .../wearable/channel/ChannelStateMachine.java | 641 ++++++++++++++---- .../wearable/channel/ChannelStatusCodes.java | 6 + .../gms/wearable/channel/ChannelTable.java | 62 ++ .../gms/wearable/channel/ChannelTask.java | 70 ++ .../gms/wearable/channel/ChannelToken.java | 18 +- .../wearable/channel/ChannelTransport.java | 92 +++ .../channel/OnChannelControlTask.java | 333 +++++++++ .../channel/OnChannelDataAckTask.java | 50 ++ .../wearable/channel/OnChannelDataTask.java | 51 ++ .../wearable/channel/PendingOperation.java | 47 ++ .../core/src/main/proto/wearable.proto | 1 + .../gms/wearable/WearableListenerService.java | 91 +-- .../internal/ChannelEventParcelable.java | 35 +- .../wearable/internal/ChannelParcelable.java | 24 +- 23 files changed, 1916 insertions(+), 682 deletions(-) create mode 100644 play-services-wearable/core/src/main/java/org/microg/gms/wearable/NodeMigrationController.java create mode 100644 play-services-wearable/core/src/main/java/org/microg/gms/wearable/channel/ChannelException.java create mode 100644 play-services-wearable/core/src/main/java/org/microg/gms/wearable/channel/ChannelTable.java create mode 100644 play-services-wearable/core/src/main/java/org/microg/gms/wearable/channel/ChannelTask.java create mode 100644 play-services-wearable/core/src/main/java/org/microg/gms/wearable/channel/ChannelTransport.java create mode 100644 play-services-wearable/core/src/main/java/org/microg/gms/wearable/channel/OnChannelControlTask.java create mode 100644 play-services-wearable/core/src/main/java/org/microg/gms/wearable/channel/OnChannelDataAckTask.java create mode 100644 play-services-wearable/core/src/main/java/org/microg/gms/wearable/channel/OnChannelDataTask.java create mode 100644 play-services-wearable/core/src/main/java/org/microg/gms/wearable/channel/PendingOperation.java diff --git a/play-services-wearable/core/src/main/java/org/microg/gms/wearable/ClockworkNodePreferences.java b/play-services-wearable/core/src/main/java/org/microg/gms/wearable/ClockworkNodePreferences.java index d6fc016af0..73dc389e08 100644 --- a/play-services-wearable/core/src/main/java/org/microg/gms/wearable/ClockworkNodePreferences.java +++ b/play-services-wearable/core/src/main/java/org/microg/gms/wearable/ClockworkNodePreferences.java @@ -19,7 +19,8 @@ import android.content.Context; import android.content.SharedPreferences; -import java.util.UUID; +import java.security.SecureRandom; +import java.util.Random; public class ClockworkNodePreferences { @@ -31,17 +32,20 @@ public class ClockworkNodePreferences { private static long seqIdBlock; private static long seqIdInBlock = -1; - private Context context; + private final Context context; + private final Random random; public ClockworkNodePreferences(Context context) { this.context = context; + this.random = new SecureRandom(); } public String getLocalNodeId() { - SharedPreferences preferences = context.getSharedPreferences(CLOCKWORK_NODE_PREFERENCES, Context.MODE_PRIVATE); + SharedPreferences preferences = context.getSharedPreferences( + CLOCKWORK_NODE_PREFERENCES, Context.MODE_PRIVATE); String nodeId = preferences.getString(CLOCKWORK_NODE_PREFERENCE_NODE_ID, null); if (nodeId == null) { - nodeId = UUID.randomUUID().toString(); + nodeId = Integer.toHexString(random.nextInt()); preferences.edit().putString(CLOCKWORK_NODE_PREFERENCE_NODE_ID, nodeId).apply(); } return nodeId; @@ -49,13 +53,26 @@ public String getLocalNodeId() { public long getNextSeqId() { synchronized (lock) { - SharedPreferences preferences = context.getSharedPreferences(CLOCKWORK_NODE_PREFERENCES, Context.MODE_PRIVATE); - if (seqIdInBlock < 0) seqIdInBlock = 1000; + SharedPreferences preferences = context.getSharedPreferences( + CLOCKWORK_NODE_PREFERENCES, Context.MODE_PRIVATE); + if (seqIdInBlock < 0) { + seqIdBlock = preferences.getLong( + CLOCKWORK_NODE_PREFERENCE_NEXT_SEQ_ID_BLOCK, 100); + preferences.edit() + .putLong(CLOCKWORK_NODE_PREFERENCE_NEXT_SEQ_ID_BLOCK, seqIdBlock + 1000) + .commit(); + seqIdBlock = 0; + } + if (seqIdInBlock >= 1000) { - seqIdBlock = preferences.getLong(CLOCKWORK_NODE_PREFERENCE_NEXT_SEQ_ID_BLOCK, 100); - preferences.edit().putLong(CLOCKWORK_NODE_PREFERENCE_NEXT_SEQ_ID_BLOCK, seqIdBlock + seqIdInBlock).apply(); + seqIdBlock = preferences.getLong( + CLOCKWORK_NODE_PREFERENCE_NEXT_SEQ_ID_BLOCK, seqIdBlock + 1000); + preferences.edit() + .putLong(CLOCKWORK_NODE_PREFERENCE_NEXT_SEQ_ID_BLOCK,seqIdBlock + 1000) + .commit(); seqIdInBlock = 0; } + return seqIdBlock + seqIdInBlock++; } } diff --git a/play-services-wearable/core/src/main/java/org/microg/gms/wearable/NodeDatabaseHelper.java b/play-services-wearable/core/src/main/java/org/microg/gms/wearable/NodeDatabaseHelper.java index d831bc71a8..3c732b6efa 100644 --- a/play-services-wearable/core/src/main/java/org/microg/gms/wearable/NodeDatabaseHelper.java +++ b/play-services-wearable/core/src/main/java/org/microg/gms/wearable/NodeDatabaseHelper.java @@ -21,7 +21,6 @@ import android.database.Cursor; import android.database.sqlite.SQLiteDatabase; import android.database.sqlite.SQLiteOpenHelper; -import android.net.Uri; import android.text.TextUtils; import android.util.Log; @@ -38,7 +37,7 @@ public class NodeDatabaseHelper extends SQLiteOpenHelper { private static final String[] GDIBHAP_FIELDS = new String[]{"dataitems_id", "packageName", "signatureDigest", "host", "path", "seqId", "deleted", "sourceNode", "data", "timestampMs", "assetsPresent", "assetname", "assets_digest", "v1SourceNode", "v1SeqId"}; private static final int VERSION = 14; - private ClockworkNodePreferences clockworkNodePreferences; + private final ClockworkNodePreferences clockworkNodePreferences; public NodeDatabaseHelper(Context context) { super(context, DB_NAME, null, VERSION); @@ -298,15 +297,15 @@ public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) { private static synchronized long getAppKey(SQLiteDatabase db, String packageName, String signatureDigest) { Cursor cursor = db.rawQuery("SELECT _id FROM appkeys WHERE packageName=? AND signatureDigest=?", new String[]{packageName, signatureDigest}); - if (cursor != null) { - try { - if (cursor.moveToNext()) { - return cursor.getLong(0); - } - } finally { - cursor.close(); + + try { + if (cursor.moveToNext()) { + return cursor.getLong(0); } + } finally { + cursor.close(); } + ContentValues appKey = new ContentValues(); appKey.put("packageName", packageName); appKey.put("signatureDigest", signatureDigest); @@ -540,12 +539,10 @@ public long getCurrentSeqId(String sourceNode) { private long getCurrentSeqId(SQLiteDatabase db, String sourceNode) { Cursor cursor = db.query("dataItemsAndAssets", new String[]{"seqId"}, "sourceNode=?", new String[]{sourceNode}, null, null, "seqId DESC", "1"); long res = 1; - if (cursor != null) { - if (cursor.moveToFirst()) { - res = cursor.getLong(0); - } - cursor.close(); + if (cursor.moveToFirst()) { + res = cursor.getLong(0); } + cursor.close(); return res; } @@ -597,8 +594,9 @@ public Cursor listMissingAssets() { } public boolean hasAsset(Asset asset) { - Cursor cursor = getReadableDatabase().query("assets", new String[]{"dataPresent"}, "digest=?", new String[]{asset.getDigest()}, null, null, null); - if (cursor == null) return false; + Cursor cursor = getReadableDatabase().query("assets",new String[]{"dataPresent"}, + "digest=?", new String[]{asset.getDigest()}, + null, null, null); try { return (cursor.moveToNext() && cursor.getInt(0) == 1); } finally { diff --git a/play-services-wearable/core/src/main/java/org/microg/gms/wearable/NodeMigrationController.java b/play-services-wearable/core/src/main/java/org/microg/gms/wearable/NodeMigrationController.java new file mode 100644 index 0000000000..42ca9a6f3b --- /dev/null +++ b/play-services-wearable/core/src/main/java/org/microg/gms/wearable/NodeMigrationController.java @@ -0,0 +1,65 @@ +package org.microg.gms.wearable; + +import android.os.Build; + +import java.util.Map; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.locks.ReentrantReadWriteLock; + +public class NodeMigrationController { + + public final ReentrantReadWriteLock migrationLock = new ReentrantReadWriteLock(); + public final ReentrantReadWriteLock archiveLock = new ReentrantReadWriteLock(); + + public final ConcurrentHashMap> nodeToCompletedAppsMap = new ConcurrentHashMap<>(); + public final Object denylistLock = new Object(); + public final ConcurrentHashMap> nodeToDenylistMap = new ConcurrentHashMap<>(); + public final Map archiveNodeHighwaterMap = new ConcurrentHashMap<>(); + public final Set completedNodes; + + public NodeMigrationController() { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { + this.completedNodes = ConcurrentHashMap.newKeySet(); + } else { + this.completedNodes = null; + } + } + + public void markNodeMigrationCompleted(String nodeId) { + migrationLock.writeLock().lock(); + try { + Set completedApps = nodeToCompletedAppsMap.remove(nodeId); + if (completedApps != null) { + android.util.Log.d("NodeMigration", "Marking " + nodeId + " as completed with apps: " + completedApps); + } + } finally { + migrationLock.writeLock().unlock(); + } + + synchronized (denylistLock) { + nodeToDenylistMap.remove(nodeId); + } + } + + public void addCompletedNode(String nodeId) { + completedNodes.add(nodeId); + } + + public boolean shouldDeliverEvents(String packageName, String sourceNodeId) { + synchronized (denylistLock) { + Set denylist = nodeToDenylistMap.get(sourceNodeId); + if (denylist != null) { + return !denylist.contains(packageName); + } + } + + migrationLock.readLock().lock(); + try { + Set completedApps = nodeToCompletedAppsMap.get(sourceNodeId); + return completedApps == null || completedApps.contains(packageName); + } finally { + migrationLock.readLock().unlock(); + } + } +} \ No newline at end of file diff --git a/play-services-wearable/core/src/main/java/org/microg/gms/wearable/WearableImpl.java b/play-services-wearable/core/src/main/java/org/microg/gms/wearable/WearableImpl.java index 73408dd7a9..9eb7b10299 100644 --- a/play-services-wearable/core/src/main/java/org/microg/gms/wearable/WearableImpl.java +++ b/play-services-wearable/core/src/main/java/org/microg/gms/wearable/WearableImpl.java @@ -37,6 +37,8 @@ import com.google.android.gms.wearable.ConnectionConfiguration; import com.google.android.gms.wearable.MessageOptions; import com.google.android.gms.wearable.Node; +import com.google.android.gms.wearable.internal.ChannelEventParcelable; +import com.google.android.gms.wearable.internal.ChannelParcelable; import com.google.android.gms.wearable.internal.IWearableListener; import com.google.android.gms.wearable.internal.NodeParcelable; import com.google.android.gms.wearable.internal.PutDataRequest; @@ -45,7 +47,6 @@ import org.microg.gms.common.RemoteListenerProxy; import org.microg.gms.common.Utils; import org.microg.gms.wearable.bluetooth.BluetoothClient; -import org.microg.gms.wearable.bluetooth.BluetoothServer; import org.microg.gms.wearable.channel.ChannelCallbacks; import org.microg.gms.wearable.channel.ChannelManager; import org.microg.gms.wearable.channel.ChannelToken; @@ -68,11 +69,10 @@ import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; -import java.util.HashMap; -import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.CountDownLatch; import okio.ByteString; @@ -86,9 +86,9 @@ public class WearableImpl { private final Context context; private final NodeDatabaseHelper nodeDatabase; private final ConfigurationDatabaseHelper configDatabase; - private final Map> listeners = new HashMap>(); - private final Set connectedNodes = new HashSet(); - private final Map activeConnections = new HashMap(); + private final Map> listeners = new ConcurrentHashMap<>(); + private final Set connectedNodes = Collections.newSetFromMap(new ConcurrentHashMap<>()); + private final Map activeConnections = new ConcurrentHashMap<>(); private RpcHelper rpcHelper; private SocketConnectionThread sct; private ConnectionConfiguration[] configurations; @@ -108,6 +108,11 @@ public class WearableImpl { public static final int ROLE_SERVER = 2; private ChannelManager channelManager; + private NodeMigrationController migrationController; + + private static final long ASSET_FETCH_COOLDOWN_MS = 500; + private static final int ASSET_BATCH_SIZE = 10; + private volatile long lastAssetFetchTime = 0; public WearableImpl(Context context, NodeDatabaseHelper nodeDatabase, ConfigurationDatabaseHelper configDatabase) { this.context = context; @@ -132,6 +137,9 @@ public WearableImpl(Context context, NodeDatabaseHelper nodeDatabase, Configurat Log.w(TAG, "Failed to initialize ChannelManager", e); } }).start(); + + this.migrationController = new NodeMigrationController(); + } public ChannelManager getChannelManager() { @@ -147,33 +155,86 @@ private class WearableChannelCallbacks implements ChannelCallbacks { @Override public void onChannelOpened(ChannelToken token, String path) { Log.d(TAG, "onChannelOpened: " + token + ", path=" + path); - invokeListeners(null, listener -> { - // todo + ChannelParcelable channel = token.toParcelable(path); + + Intent intent = new Intent("com.google.android.gms.wearable.CHANNEL_EVENT"); + intent.setPackage(token.appKey.packageName); + intent.setData(Uri.parse("wear://" + token.nodeId + "/" + path)); + + invokeListeners(intent, listener -> { + try { + ChannelEventParcelable event = new ChannelEventParcelable( + channel, ChannelEventParcelable.EVENT_TYPE_CHANNEL_OPENED, + 0, 0); + listener.onChannelEvent(event); + } catch (Exception e) { + Log.w(TAG, "Error notifying listener of channel opened", e); + } }); } @Override public void onChannelClosed(ChannelToken token, String path, int closeReason, int errorCode) { Log.d(TAG, "onChannelClosed: " + token + ", reason=" + closeReason); - invokeListeners(null, listener -> { - // todo + ChannelParcelable channel = token.toParcelable(path); + + Intent intent = new Intent("com.google.android.gms.wearable.CHANNEL_EVENT"); + intent.setPackage(token.appKey.packageName); + intent.setData(Uri.parse("wear://" + token.nodeId + "/" + path)); + + invokeListeners(intent, listener -> { + try { + ChannelEventParcelable event = new ChannelEventParcelable( + channel, ChannelEventParcelable.EVENT_TYPE_CHANNEL_CLOSED, + closeReason, errorCode); + listener.onChannelEvent(event); + } catch (Exception e) { + Log.w(TAG, "Error notifying listener of channel closed", e); + } }); } @Override public void onChannelInputClosed(ChannelToken token, String path, int closeReason, int errorCode) { Log.d(TAG, "onChannelInputClosed: " + token); - invokeListeners(null, listener -> { - // todo + ChannelParcelable channel = token.toParcelable(path); + + Intent intent = new Intent("com.google.android.gms.wearable.CHANNEL_EVENT"); + intent.setPackage(token.appKey.packageName); + intent.setData(Uri.parse("wear://" + token.nodeId + "/" + path)); + + invokeListeners(intent, listener -> { + try { + ChannelEventParcelable event = new ChannelEventParcelable( + channel, ChannelEventParcelable.EVENT_TYPE_INPUT_CLOSED, + closeReason, errorCode); + listener.onChannelEvent(event); + } catch (Exception e) { + Log.w(TAG, "Error notifying listener of input closed", e); + } }); + } @Override public void onChannelOutputClosed(ChannelToken token, String path, int closeReason, int errorCode) { Log.d(TAG, "onChannelOutputClosed: " + token); - invokeListeners(null, listener -> { - // todo + ChannelParcelable channel = token.toParcelable(path); + + Intent intent = new Intent("com.google.android.gms.wearable.CHANNEL_EVENT"); + intent.setPackage(token.appKey.packageName); + intent.setData(Uri.parse("wear://" + token.nodeId + "/" + path)); + + invokeListeners(intent, listener -> { + try { + ChannelEventParcelable event = new ChannelEventParcelable( + channel, ChannelEventParcelable.EVENT_TYPE_OUTPUT_CLOSED, closeReason, errorCode); + listener.onChannelEvent(event); + } catch (Exception e) { + Log.w(TAG, "Error notifying listener of output closed", e); + } }); + } } @@ -190,7 +251,7 @@ public DataItemRecord putDataItem(String packageName, String signatureDigest, St record.source = source; record.dataItem = dataItem; record.v1SeqId = clockworkNodePreferences.getNextSeqId(); - if (record.source.equals(getLocalNodeId())) record.seqId = record.v1SeqId; + record.seqId = record.v1SeqId; nodeDatabase.putRecord(record); return record; } @@ -325,9 +386,10 @@ public synchronized ConnectionConfiguration[] getConfigurations() { } // companion app crash if name is null - // name can be null in failed pair (maybe), + // name can be null due failed pair (maybe), // or maybe i something broke, // or not setting name properly somewhere + // so we just set address as name for (int i = 0; i < configurations.length; i++) { ConnectionConfiguration c = configurations[i]; if (c.name == null || c.name.isEmpty() || "null".equals(c.name)) { @@ -392,9 +454,15 @@ public Map getActiveConnections() { } private boolean syncRecordToPeer(String nodeId, DataItemRecord record) { + WearableConnection connection = activeConnections.get(nodeId); + if (connection == null) { + Log.w(TAG, "Cannot sync to " + nodeId + " - connection not found"); + return false; + } + for (Asset asset : record.dataItem.getAssets().values()) { try { - syncAssetToPeer(nodeId, record, asset); + syncAssetToPeer(connection, record, asset); } catch (Exception e) { Log.w(TAG, "Could not sync asset " + asset + " for " + nodeId + " and " + record, e); closeConnection(nodeId); @@ -413,25 +481,44 @@ private boolean syncRecordToPeer(String nodeId, DataItemRecord record) { return true; } - private void syncAssetToPeer(String nodeId, DataItemRecord record, Asset asset) throws IOException { + private void syncAssetToPeer(WearableConnection connection, DataItemRecord record, Asset asset) throws IOException { RootMessage announceMessage = new RootMessage.Builder().setAsset(new SetAsset.Builder() .digest(asset.getDigest()) - .appkeys(new AppKeys(Collections.singletonList(new AppKey(record.packageName, record.signatureDigest)))) - .build()).hasAsset(true).build(); - activeConnections.get(nodeId).writeMessage(announceMessage); + .appkeys( + new AppKeys( + Collections.singletonList( + new AppKey(record.packageName, record.signatureDigest) + ) + ) + ).build()).hasAsset(true).build(); + + connection.writeMessage(announceMessage); + File assetFile = createAssetFile(asset.getDigest()); - String fileName = calculateDigest(announceMessage.encode()); - FileInputStream fis = new FileInputStream(assetFile); - byte[] arr = new byte[12215]; - ByteString lastPiece = null; - int c = 0; - while ((c = fis.read(arr)) > 0) { - if (lastPiece != null) { - activeConnections.get(nodeId).writeMessage(new RootMessage.Builder().filePiece(new FilePiece(fileName, false, lastPiece, null)).build()); + String filename = calculateDigest(announceMessage.encode()); + + try (FileInputStream fis = new FileInputStream(assetFile)){ + byte[] arr = new byte[12215]; + ByteString lastPiece = null; + int c; + while ((c = fis.read(arr)) > 0) { + if (lastPiece != null) { + connection.writeMessage( + new RootMessage.Builder() + .filePiece( + new FilePiece(filename, false, lastPiece, null) + ).build() + ); + } + lastPiece = ByteString.of(arr, 0, c); } - lastPiece = ByteString.of(arr, 0, c); + connection.writeMessage( + new RootMessage.Builder() + .filePiece( + new FilePiece(filename, true, lastPiece, asset.getDigest()) + ).build() + ); } - activeConnections.get(nodeId).writeMessage(new RootMessage.Builder().filePiece(new FilePiece(fileName, true, lastPiece, asset.getDigest())).build()); } public void addAssetToDatabase(Asset asset, List appKeys) { @@ -487,6 +574,19 @@ public void onConnectReceived(WearableConnection connection, String nodeId, Conn } private void fetchMissingAssets(String nodeId) { + long now = System.currentTimeMillis(); + long timeSinceLastFetch = now - lastAssetFetchTime; + if (timeSinceLastFetch < ASSET_FETCH_COOLDOWN_MS) { + long delay = ASSET_FETCH_COOLDOWN_MS - timeSinceLastFetch; + networkHandler.postDelayed(() -> doFetchMissingAssets(nodeId), delay); + return; + } + + doFetchMissingAssets(nodeId); + lastAssetFetchTime = now; + } + + private void doFetchMissingAssets(String nodeId) { WearableConnection connection = activeConnections.get(nodeId); if (connection == null) { Log.d(TAG, "Connection no longer active for node: " + nodeId); @@ -505,23 +605,23 @@ private void fetchMissingAssets(String nodeId) { break; } - try { - String assetName = cursor.getString(12); - String packageName = cursor.getString(1); - String signatureDigest = cursor.getString(2); + String assetName = cursor.getString(12); + String packageName = cursor.getString(1); + String signatureDigest = cursor.getString(2); + try { connection.writeMessage(new RootMessage.Builder() .fetchAsset(new FetchAsset.Builder() .assetName(assetName) .packageName(packageName) .signatureDigest(signatureDigest) - .permission(false) - .build()).build()); + .build()) + .build()); fetchCount++; // Add small delay between requests to avoid overwhelming the connection - if (fetchCount % 10 == 0) { + if (fetchCount % ASSET_BATCH_SIZE == 0) { try { Thread.sleep(100); } catch (InterruptedException e) { @@ -537,21 +637,9 @@ private void fetchMissingAssets(String nodeId) { } } - // More delays for heavy operations - if (fetchCount > 0) { - Log.d(TAG, "Fetched " + fetchCount + " missing assets from " + nodeId); + if (fetchCount > 100) { if (channelManager != null) { - long cooldownMs = 500; - if (fetchCount > 100) { - cooldownMs = 1000; - } - if (fetchCount > 200) { - cooldownMs = 1500; - } - - Log.d(TAG, "Setting " + cooldownMs + "ms cooldown after fetching " + - fetchCount + " assets"); - channelManager.setOperationCooldown(cooldownMs); + channelManager.setOperationCooldown(1000); } } } finally { @@ -944,4 +1032,7 @@ public ClockworkNodePreferences getClockworkNodePreferences() { return clockworkNodePreferences; } + public NodeMigrationController getMigrationController() { + return migrationController; + } } diff --git a/play-services-wearable/core/src/main/java/org/microg/gms/wearable/WearableServiceImpl.java b/play-services-wearable/core/src/main/java/org/microg/gms/wearable/WearableServiceImpl.java index 9f9beacb97..4cc8f15c6f 100644 --- a/play-services-wearable/core/src/main/java/org/microg/gms/wearable/WearableServiceImpl.java +++ b/play-services-wearable/core/src/main/java/org/microg/gms/wearable/WearableServiceImpl.java @@ -767,7 +767,6 @@ public void doAncsNegativeAction(IWearableCallbacks callbacks, int i) throws Rem @Override public void openChannel(IWearableCallbacks callbacks, String nodeId, String path) throws RemoteException { Log.d(TAG, "openChannel; " + nodeId + ", " + path); - ChannelManager channelManager = getChannelManager(); if (channelManager == null) { Log.w(TAG, "openChannel: ChannelManager not initialized"); @@ -789,7 +788,7 @@ public void openChannel(IWearableCallbacks callbacks, String nodeId, String path } AppKey appKey = getAppKey(); - + boolean isReliable = true; OpenChannelCallback openCallback = (statusCode, token, path1) -> { try { if (statusCode == ChannelStatusCodes.SUCCESS && token != null) { @@ -802,7 +801,7 @@ public void openChannel(IWearableCallbacks callbacks, String nodeId, String path } }; - channelManager.openChannel(appKey, nodeId, path, openCallback); + channelManager.openChannel(appKey, nodeId, path, isReliable, openCallback); } catch (Exception e) { Log.w(TAG, "openChannel: exception during processing", e); callbacks.onOpenChannelResponse(new OpenChannelResponse(ChannelStatusCodes.INTERNAL_ERROR, null)); diff --git a/play-services-wearable/core/src/main/java/org/microg/gms/wearable/bluetooth/BleDeviceDiscoverer.java b/play-services-wearable/core/src/main/java/org/microg/gms/wearable/bluetooth/BleDeviceDiscoverer.java index 4ea030684e..63c7186f51 100644 --- a/play-services-wearable/core/src/main/java/org/microg/gms/wearable/bluetooth/BleDeviceDiscoverer.java +++ b/play-services-wearable/core/src/main/java/org/microg/gms/wearable/bluetooth/BleDeviceDiscoverer.java @@ -142,48 +142,47 @@ private void updateScanning() { @RequiresPermission(Manifest.permission.BLUETOOTH_SCAN) private void startScanning() { - if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) { - return; - } - - try { - scanCallback = new ScanCallback() { - @Override - public void onScanResult(int callbackType, ScanResult result) { - handleScanResult(result); - } + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { - @Override - public void onBatchScanResults(List results) { - for (ScanResult result : results) { + try { + scanCallback = new ScanCallback() { + @Override + public void onScanResult(int callbackType, ScanResult result) { handleScanResult(result); } - } - @Override - public void onScanFailed(int errorCode) { - Log.e(TAG, "BLE scan failed with error: " + errorCode); - synchronized (lock) { - isScanning = false; + @Override + public void onBatchScanResults(List results) { + for (ScanResult result : results) { + handleScanResult(result); + } } - } - }; - List filters = new ArrayList<>(deviceFilters.values()); + @Override + public void onScanFailed(int errorCode) { + Log.e(TAG, "BLE scan failed with error: " + errorCode); + synchronized (lock) { + isScanning = false; + } + } + }; + + List filters = new ArrayList<>(deviceFilters.values()); - ScanSettings settings = new ScanSettings.Builder() - .setScanMode(ScanSettings.SCAN_MODE_LOW_POWER) - .build(); + ScanSettings settings = new ScanSettings.Builder() + .setScanMode(ScanSettings.SCAN_MODE_LOW_POWER) + .build(); - scanner.startScan(filters, settings, scanCallback); - isScanning = true; + scanner.startScan(filters, settings, scanCallback); + isScanning = true; - Log.d(TAG, String.format("Started BLE scanning for %d devices", filters.size())); + Log.d(TAG, String.format("Started BLE scanning for %d devices", filters.size())); - } catch (SecurityException e) { - Log.e(TAG, "Permission denied for BLE scanning", e); - } catch (Exception e) { - Log.e(TAG, "Error starting BLE scan", e); + } catch (SecurityException e) { + Log.e(TAG, "Permission denied for BLE scanning", e); + } catch (Exception e) { + Log.e(TAG, "Error starting BLE scan", e); + } } } @@ -193,20 +192,18 @@ private void stopScanning() { return; } - if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) { - return; - } - - try { - if (scanner != null && scanCallback != null) { - scanner.stopScan(scanCallback); - Log.d(TAG, "Stopped BLE scanning"); + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { + try { + if (scanner != null && scanCallback != null) { + scanner.stopScan(scanCallback); + Log.d(TAG, "Stopped BLE scanning"); + } + } catch (Exception e) { + Log.w(TAG, "Error stopping BLE scan", e); + } finally { + isScanning = false; + scanCallback = null; } - } catch (Exception e) { - Log.w(TAG, "Error stopping BLE scan", e); - } finally { - isScanning = false; - scanCallback = null; } } diff --git a/play-services-wearable/core/src/main/java/org/microg/gms/wearable/bluetooth/BluetoothConnectionThread.java b/play-services-wearable/core/src/main/java/org/microg/gms/wearable/bluetooth/BluetoothConnectionThread.java index a74fabf03b..c05019db91 100644 --- a/play-services-wearable/core/src/main/java/org/microg/gms/wearable/bluetooth/BluetoothConnectionThread.java +++ b/play-services-wearable/core/src/main/java/org/microg/gms/wearable/bluetooth/BluetoothConnectionThread.java @@ -490,6 +490,8 @@ public ConnectionListener(Context context, ConnectionConfiguration config, Weara @Override public void onConnected(WearableConnection connection) { Log.d(TAG, "Wearable connection established for " + config.address); + thread.markActivity(); + thread.isConnected = true; this.connection = connection; @@ -498,7 +500,6 @@ public void onConnected(WearableConnection connection) { this.messageHandler = new MessageHandler(context, wearableImpl, config); - thread.markActivity(); wearableImpl.onConnectReceived(connection, config.nodeId, peerConnect); } @@ -506,6 +507,7 @@ public void onConnected(WearableConnection connection) { public void onMessage(WearableConnection connection, RootMessage message) { Log.d(TAG, "Message received from " + config.address + ": " + message.toString()); thread.markActivity(); + if (peerConnect != null && messageHandler != null) messageHandler.handleMessage(connection, peerConnect.id, message); } @@ -513,6 +515,7 @@ public void onMessage(WearableConnection connection, RootMessage message) { @Override public void onDisconnected() { Log.d(TAG, "Wearable connection disconnected for " + config.address); + thread.isConnected = false; if (connection != null && peerConnect != null) { wearableImpl.onDisconnectReceived(connection, peerConnect); } diff --git a/play-services-wearable/core/src/main/java/org/microg/gms/wearable/channel/ChannelException.java b/play-services-wearable/core/src/main/java/org/microg/gms/wearable/channel/ChannelException.java new file mode 100644 index 0000000000..4c786e8dc3 --- /dev/null +++ b/play-services-wearable/core/src/main/java/org/microg/gms/wearable/channel/ChannelException.java @@ -0,0 +1,24 @@ +package org.microg.gms.wearable.channel; + +public class ChannelException extends Exception { + private final ChannelToken token; + + public ChannelException(ChannelToken token) { + super("Channel exception for: " + token); + this.token = token; + } + + public ChannelException(ChannelToken token, String message) { + super(message); + this.token = token; + } + + public ChannelException(ChannelToken token, Throwable cause) { + super("Channel exception for: " + token, cause); + this.token = token; + } + + public ChannelToken getToken() { + return token; + } +} \ No newline at end of file diff --git a/play-services-wearable/core/src/main/java/org/microg/gms/wearable/channel/ChannelManager.java b/play-services-wearable/core/src/main/java/org/microg/gms/wearable/channel/ChannelManager.java index 10ba47a6ff..a1ec7d31d2 100644 --- a/play-services-wearable/core/src/main/java/org/microg/gms/wearable/channel/ChannelManager.java +++ b/play-services-wearable/core/src/main/java/org/microg/gms/wearable/channel/ChannelManager.java @@ -1,5 +1,6 @@ package org.microg.gms.wearable.channel; +import android.os.Build; import android.os.Handler; import android.os.IBinder; import android.os.RemoteException; @@ -23,9 +24,10 @@ import org.microg.gms.wearable.proto.RootMessage; import java.io.IOException; -import java.util.Map; +import java.util.Arrays; import java.util.Random; -import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.BlockingQueue; +import java.util.concurrent.LinkedBlockingQueue; import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicInteger; @@ -34,6 +36,9 @@ public class ChannelManager { private static final String TAG = "ChannelManager"; + private static final int PROCESSING_LOOP_DELAY_MS = 10; + private static final long OPEN_TIMEOUT_MS = 30000; + public static final int CHANNEL_CONTROL_TYPE_OPEN = 1; public static final int CHANNEL_CONTROL_TYPE_OPEN_ACK = 2; public static final int CHANNEL_CONTROL_TYPE_CLOSE = 3; @@ -42,11 +47,11 @@ public class ChannelManager { private final WearableImpl wearable; private final String localNodeId; private final Random random; + private final ChannelTransport transport; - private final Object lock = new Object(); - private final Map channels = new ConcurrentHashMap<>(); - private final Map channelIdToToken = new ConcurrentHashMap<>(); + public final ChannelTable channelTable = new ChannelTable(); private final AtomicBoolean isRunning = new AtomicBoolean(false); + private final BlockingQueue taskQueue = new LinkedBlockingQueue<>(); private final AtomicInteger requestIdCounter = new AtomicInteger(1); private final AtomicInteger generationCounter = new AtomicInteger(1); @@ -55,11 +60,49 @@ public class ChannelManager { private volatile long cooldownUntil = 0; + private final Runnable processingLoop = new Runnable() { + @Override + public void run() { + if (!isRunning.get()) { + return; + } + + try { + ChannelTask task; + while ((task = taskQueue.poll()) != null) { + task.run(); + } + + for (ChannelStateMachine channel : channelTable.values()) { + try { + processChannelIO(channel); + } catch (Exception e) { + Log.e(TAG, "Error processing channel I/O", e); + try { + channel.forceClose(); + channelTable.remove(channel.token); + } catch (Exception ex) { + Log.e(TAG, "Error during cleanup", ex); + } + } + } + + } catch (Exception e) { + Log.e(TAG, "Error in processing loop", e); + } + + if (isRunning.get()) { + handler.postDelayed(this, PROCESSING_LOOP_DELAY_MS); + } + } + }; + public ChannelManager(Handler handler, WearableImpl wearable, String localNodeId) { this.handler = handler; this.wearable = wearable; this.localNodeId = localNodeId; this.random = new Random(); + this.transport = new ChannelTransport(); } public void setOperationCooldown(long durationMs) { @@ -77,33 +120,65 @@ private boolean isInCooldown() { return false; } + public boolean isRunning() { + return isRunning.get(); + } + public void start() { - isRunning.set(true); - Log.d(TAG, "ChannelManager started, localNodeId=" + localNodeId); + if (isRunning.compareAndSet(false, true)) { + Log.d(TAG, "ChannelManager started, localNodeId=" + localNodeId); + handler.post(processingLoop); + } } public void stop() { - isRunning.set(false); - synchronized (lock) { - for (ChannelStateMachine channel : channels.values()) { + if (isRunning.compareAndSet(true, false)) { + handler.removeCallbacks(processingLoop); + + for (ChannelStateMachine channel : channelTable.values()) { try { - channel.clearOpenCallback(); - channel.close(); + if (channel.openResultDispatcher != null) { + channel.openResultDispatcher.onResult( + ChannelStatusCodes.INTERNAL_ERROR, null, channel.channelPath); + } + channel.forceClose(); } catch (Exception e) { Log.w(TAG, "Error closing channel on stop", e); } } - channels.clear(); - channelIdToToken.clear(); + + channelTable.clear(); + transport.clear(); + taskQueue.clear(); + + Log.d(TAG, "ChannelManager stopped"); } - Log.d(TAG, "ChannelManager stopped"); } public void setChannelCallbacks(ChannelCallbacks callbacks) { this.channelCallbacks = callbacks; } - public void openChannel(AppKey appKey, String nodeId, String path, OpenChannelCallback callback) { + public ChannelStateMachine getChannel(ChannelToken token) { + return channelTable.get(token); + } + + public void removeChannel(ChannelToken token) { + channelTable.remove(token); + } + + private void processChannelIO(ChannelStateMachine channel) throws IOException { + if (channel.sendingState == ChannelStateMachine.SENDING_STATE_WAITING_TO_READ) { + channel.processOutgoingData(); + } + + if (channel.receivingState == ChannelStateMachine.RECEIVING_STATE_WAITING_TO_WRITE) { + channel.processIncomingBuffer(); + } + } + + + public void openChannel(AppKey appKey, String nodeId, String path, boolean isReliable, OpenChannelCallback callback) { Log.d(TAG, String.format("openChannel(%s, %s, %s)", appKey.packageName, nodeId, path)); if (!isRunning.get()) { @@ -115,116 +190,85 @@ public void openChannel(AppKey appKey, String nodeId, String path, OpenChannelCa if (isInCooldown()) { long delay = cooldownUntil - System.currentTimeMillis() + 100; Log.d(TAG, "Deferring channel open by " + delay + "ms due to cooldown"); - - handler.postDelayed(() -> doOpenChannel(appKey, nodeId, path, callback), delay); + handler.postDelayed(() -> openChannel(appKey, nodeId, path, isReliable, callback), delay); return; } - handler.post(() -> doOpenChannel(appKey, nodeId, path, callback)); + taskQueue.offer(new ChannelTask(this) { + @Override + protected void execute() throws IOException, ChannelException { + doOpenChannel(appKey, nodeId, path, isReliable, callback); + } + }); } - private void doOpenChannel(AppKey appKey, String nodeId, String path, OpenChannelCallback callback) { - if (isInCooldown()) { - long delay = cooldownUntil - System.currentTimeMillis() + 100; - Log.d(TAG, "Cooldown detected in doOpenChannel, deferring by " + delay + "ms"); - handler.postDelayed(() -> doOpenChannel(appKey, nodeId, path, callback), delay); + private void doOpenChannel(AppKey appKey, String nodeId, String path, boolean isReliable, OpenChannelCallback callback) + throws IOException, ChannelException { + + WearableConnection connection = wearable.getActiveConnections().get(nodeId); + if (connection == null) { + Log.w(TAG, "Target node not connected: " + nodeId); + callback.onResult(ChannelStatusCodes.CHANNEL_NOT_CONNECTED, null, path); return; } - try { - WearableConnection connection = wearable.getActiveConnections().get(nodeId); - if (connection == null) { - Log.w(TAG, "Target node not connected: " + nodeId); - callback.onResult(ChannelStatusCodes.CHANNEL_NOT_CONNECTED, null, path); - return; - } - - long channelId = generateChannelId(); + long channelId = generateChannelId(); + ChannelToken token = new ChannelToken(nodeId, appKey, channelId, true, isReliable); - ChannelToken token = new ChannelToken(nodeId, appKey, channelId, true); - String tokenString = token.toTokenString(); + IBinder.DeathRecipient deathRecipient = () -> onBinderDied(token); - IBinder.DeathRecipient deathRecipient = () -> onBinderDied(token); + ChannelStateMachine channel = new ChannelStateMachine( + token, this, transport, channelCallbacks, true, deathRecipient, handler + ); + channel.channelPath = path; + channel.openResultDispatcher = callback; - ChannelStateMachine channel = new ChannelStateMachine( - token, this, channelCallbacks, true, deathRecipient - ); - channel.setPath(path); - channel.setOpenCallback(callback); + channelTable.put(token, channel); - synchronized (lock) { - channels.put(tokenString, channel); - channelIdToToken.put(channelId, tokenString); - } + channel.openTimeoutOp = new PendingOperation(handler, + () -> onOpenTimeout(token), + OPEN_TIMEOUT_MS, + "Open channel"); - channel.setConnectionState(ChannelStateMachine.CONNECTION_STATE_OPEN_SENT); + channel.sendOpenRequest(); + } - ChannelControlRequest controlRequest = new ChannelControlRequest.Builder() - .type(CHANNEL_CONTROL_TYPE_OPEN) - .channelId(channelId) - .fromChannelOperator(true) - .packageName(appKey.packageName) - .signatureDigest(appKey.signatureDigest) - .path(path) - .build(); + private void onOpenTimeout(ChannelToken token) { + taskQueue.offer(new ChannelTask(this) { + @Override + protected void execute() throws IOException, ChannelException { + ChannelStateMachine channel = channelTable.get(token); + if (channel == null) { + return; + } - ChannelRequest channelRequest = new ChannelRequest.Builder() - .channelControlRequest(controlRequest) - .version(1) - .origin(0) - .build(); - Log.d(TAG, "ChannelRequest: " + channelRequest); - int requestId = requestIdCounter.getAndIncrement(); - int generation = generationCounter.get(); + setChannel(channel); - Request request = new Request.Builder() - .targetNodeId(nodeId) - .sourceNodeId(localNodeId) - .packageName(appKey.packageName) - .signatureDigest(appKey.signatureDigest) - .path(path) - .request(channelRequest) - .requestId(requestId) - .generation(generation) - .build(); + if (channel.connectionState == ChannelStateMachine.CONNECTION_STATE_NOT_STARTED) { + Log.w(TAG, "Failed before sending"); + } else if (channel.connectionState == ChannelStateMachine.CONNECTION_STATE_ESTABLISHED) { + Log.d(TAG, "Already opened, ignore timeout"); + return; + } - RootMessage message = new RootMessage.Builder() - .channelRequest(request) - .build(); - Log.d(TAG, "RootMessage: " + message); + channel.openTimeoutOp = null; - try { - connection.writeMessage(message); - Log.d(TAG, "Sent open channel request: " + channel); - } catch (IOException e) { - Log.e(TAG, "Failed to send channel open request", e); - synchronized (lock) { - channels.remove(tokenString); - channelIdToToken.remove(channelId); + if (channel.openResultDispatcher == null) { + Log.w(TAG, "Bad state: CONNECTION_STATE_OPEN_SENT but no callbacks"); + throw new ChannelException(token, "No callback on timeout"); } - callback.onResult(ChannelStatusCodes.CHANNEL_NOT_CONNECTED, null, path); - } - } catch (Exception e) { - Log.e(TAG, "Failed to open channel", e); - callback.onResult(ChannelStatusCodes.INTERNAL_ERROR, null, path); - } + channel.openResultDispatcher.onResult( + ChannelStatusCodes.CLOSE_REASON_REMOTE_CLOSE, null, channel.channelPath); + channel.openResultDispatcher = null; + } + }); } private long generateChannelId() { return System.currentTimeMillis() ^ (random.nextLong() & 0xFFFFFFFFL); } - public ChannelStateMachine getChannel(String tokenString) { - synchronized (lock) { - return channels.get(tokenString); - } - } - - public ChannelStateMachine getChannel(ChannelToken token) { - return getChannel(token.toTokenString()); - } - public void closeChannel(ChannelToken token, int errorCode) { ChannelStateMachine channel = getChannel(token); if (channel == null) { @@ -232,57 +276,21 @@ public void closeChannel(ChannelToken token, int errorCode) { return; } - handler.post(() -> doCloseChannel(channel, errorCode)); + taskQueue.offer(new ChannelTask(this) { + @Override + protected void execute() throws IOException { + setChannel(channel); + doCloseChannel(channel, errorCode); + } + }); } - private void doCloseChannel(ChannelStateMachine channel, int errorCode) { + private void doCloseChannel(ChannelStateMachine channel, int errorCode) throws IOException { try { - WearableConnection connection = wearable.getActiveConnections().get(channel.token.nodeId); - if (connection != null) { - ChannelControlRequest controlRequest = new ChannelControlRequest.Builder() - .type(CHANNEL_CONTROL_TYPE_CLOSE) - .channelId(channel.token.channelId) - .fromChannelOperator(channel.token.thisNodeWasOpener) - .packageName(channel.token.appKey.packageName) - .signatureDigest(channel.token.appKey.signatureDigest) - .closeErrorCode(errorCode) - .build(); - - ChannelRequest channelRequest = new ChannelRequest.Builder() - .channelControlRequest(controlRequest) - .version(1) - .origin(0) - .build(); - - int requestId = requestIdCounter.getAndIncrement(); - - Request request = new Request.Builder() - .requestId(requestId) - .packageName(channel.token.appKey.packageName) - .signatureDigest(channel.token.appKey.signatureDigest) - .targetNodeId(channel.token.nodeId) - .sourceNodeId(localNodeId) - .request(channelRequest) - .generation(generationCounter.get()) - .build(); - - try { - connection.writeMessage(new RootMessage.Builder() - .channelRequest(request) - .build()); - } catch (IOException e) { - Log.e(TAG, "Failed to send close request", e); - } - } - - channel.close(); - } catch (Exception e) { - Log.e(TAG, "Error closing channel", e); + channel.sendCloseRequest(errorCode); + channel.forceClose(); } finally { - synchronized (lock) { - channels.remove(channel.token.toTokenString()); - channelIdToToken.remove(channel.token.channelId); - } + channelTable.remove(channel.token); } } @@ -295,83 +303,74 @@ public void onChannelRequestReceived(WearableConnection connection, String sourc ChannelRequest channelRequest = request.request; if (channelRequest.channelControlRequest != null) { - onChannelControlReceived(connection, sourceNodeId, request, channelRequest.channelControlRequest); + taskQueue.offer(new OnChannelControlTask(this, sourceNodeId, connection, request)); } else if (channelRequest.channelDataRequest != null) { - onChannelDataReceived(channelRequest.channelDataRequest); + taskQueue.offer(new OnChannelDataTask(this, sourceNodeId, channelRequest.channelDataRequest)); } else if (channelRequest.channelDataAckRequest != null) { - onChannelDataAckReceived(channelRequest.channelDataAckRequest); + taskQueue.offer(new OnChannelDataAckTask(this, sourceNodeId, channelRequest.channelDataAckRequest)); } } - - private void onChannelControlReceived(WearableConnection connection, String sourceNodeId, Request request, ChannelControlRequest control) { - int type = control.type; - Log.d(TAG, "onChannelControlReceived: type=" + type + ", channelId=" + control.channelId); - - switch (type) { - case CHANNEL_CONTROL_TYPE_OPEN: - onChannelOpenReceived(connection, sourceNodeId, request, control); - break; - case CHANNEL_CONTROL_TYPE_OPEN_ACK: - onChannelOpenAckReceived(control); - break; - case CHANNEL_CONTROL_TYPE_CLOSE: - onChannelCloseReceived(control); - break; - default: - Log.w(TAG, "Unknown channel control type: " + type); + public void sendOpenRequest(ChannelStateMachine channel) throws IOException { + WearableConnection connection = wearable.getActiveConnections().get(channel.token.nodeId); + if (connection == null) { + throw new IOException("No connection to " + channel.token.nodeId); } - } - - private void onChannelOpenReceived(WearableConnection connection, String sourceNodeId, Request request, ChannelControlRequest control) { - Log.d(TAG, "onChannelOpenReceived: channelId=" + control.channelId + - ", path=" + control.path + ", from=" + sourceNodeId); - handler.post(() -> { - try { - AppKey appKey = new AppKey(control.packageName, control.signatureDigest); - - ChannelToken token = new ChannelToken( - sourceNodeId, appKey, control.channelId, false - ); - String tokenString = token.toTokenString(); + ChannelControlRequest controlRequest = new ChannelControlRequest.Builder() + .type(CHANNEL_CONTROL_TYPE_OPEN) + .channelId(channel.token.channelId) + .fromChannelOperator(channel.token.thisNodeWasOpener) + .packageName(channel.token.appKey.packageName) + .signatureDigest(channel.token.appKey.signatureDigest) + .path(channel.channelPath) + .isReliable(channel.token.isReliable) + .build(); - IBinder.DeathRecipient deathRecipient = () -> onBinderDied(token); + ChannelRequest channelRequest = new ChannelRequest.Builder() + .channelControlRequest(controlRequest) + .version(1) + .origin(0) + .build(); - ChannelStateMachine channel = new ChannelStateMachine( - token, this, channelCallbacks, false, deathRecipient - ); - channel.setPath(control.path); - channel.setConnectionState(ChannelStateMachine.CONNECTION_STATE_ESTABLISHED); + int requestId = requestIdCounter.getAndIncrement(); + int generation = generationCounter.get(); - synchronized (lock) { - channels.put(tokenString, channel); - channelIdToToken.put(control.channelId, tokenString); - } + Request request = new Request.Builder() + .targetNodeId(channel.token.nodeId) + .sourceNodeId(localNodeId) + .packageName(channel.token.appKey.packageName) + .signatureDigest(channel.token.appKey.signatureDigest) + .path(channel.channelPath) + .request(channelRequest) + .requestId(requestId) + .generation(generation) + .build(); - sendOpenAck(connection, sourceNodeId, control, appKey); + RootMessage message = new RootMessage.Builder() + .channelRequest(request) + .build(); - Log.d(TAG, "Channel opened by remote: " + channel); - if (channelCallbacks != null) { - channelCallbacks.onChannelOpened(token, control.path); - } + Log.d(TAG, "Sending channel open message, message: " + message); - } catch (Exception e) { - Log.e(TAG, "Error handling channel open request", e); - } - }); + connection.writeMessage(message); + Log.d(TAG, "Sent open channel request for " + channel.token); } - private void sendOpenAck(WearableConnection connection, String targetNodeId, - ChannelControlRequest originalRequest, AppKey appKey) { + public void sendOpenAck(ChannelStateMachine channel) throws IOException { + WearableConnection connection = wearable.getActiveConnections().get(channel.token.nodeId); + if (connection == null) { + throw new IOException("No connection to " + channel.token.nodeId); + } + ChannelControlRequest ackControl = new ChannelControlRequest.Builder() .type(CHANNEL_CONTROL_TYPE_OPEN_ACK) - .channelId(originalRequest.channelId) - .fromChannelOperator(false) - .packageName(appKey.packageName) - .signatureDigest(appKey.signatureDigest) - .path(originalRequest.path) + .channelId(channel.token.channelId) + .fromChannelOperator(channel.token.thisNodeWasOpener) + .packageName(channel.token.appKey.packageName) + .signatureDigest(channel.token.appKey.signatureDigest) + .path(channel.channelPath) .build(); ChannelRequest channelRequest = new ChannelRequest.Builder() @@ -384,166 +383,67 @@ private void sendOpenAck(WearableConnection connection, String targetNodeId, Request request = new Request.Builder() .requestId(requestId) - .packageName(appKey.packageName) - .signatureDigest(appKey.signatureDigest) - .targetNodeId(targetNodeId) + .packageName(channel.token.appKey.packageName) + .signatureDigest(channel.token.appKey.signatureDigest) + .targetNodeId(channel.token.nodeId) .sourceNodeId(localNodeId) .request(channelRequest) .generation(generationCounter.get()) .build(); - try { - connection.writeMessage(new RootMessage.Builder() - .channelRequest(request) - .build()); - Log.d(TAG, "Sent channel open ack for channelId=" + originalRequest.channelId); - } catch (IOException e) { - Log.e(TAG, "Failed to send open ack", e); - } - } - - private void onChannelOpenAckReceived(ChannelControlRequest control) { - Log.d(TAG, "onChannelOpenAckReceived: channelId=" + control.channelId); - - handler.post(() -> { - String tokenString; - synchronized (lock) { - tokenString = channelIdToToken.get(control.channelId); - } - - if (tokenString == null) { - Log.w(TAG, "Received open ack for unknown channelId: " + control.channelId); - return; - } + connection.writeMessage(new RootMessage.Builder() + .channelRequest(request) + .build()); - ChannelStateMachine channel = getChannel(tokenString); - if (channel == null) { - Log.w(TAG, "Channel not found for token: " + tokenString); - return; - } - - channel.onChannelEstablished(); - Log.d(TAG, "Channel established: " + channel); - }); + Log.d(TAG, "Sent channel open ACK for " + channel.token); } - void onChannelCloseReceived(ChannelControlRequest control) { - Log.d(TAG, "onChannelCloseReceived: channelId=" + control.channelId); - - handler.post(() -> { - String tokenString; - synchronized (lock) { - tokenString = channelIdToToken.get(control.channelId); - } - - if (tokenString == null) { - Log.w(TAG, "Received close for unknown channelId: " + control.channelId); - return; - } - - ChannelStateMachine channel = getChannel(tokenString); - if (channel == null) { - Log.w(TAG, "Channel not found for token: " + tokenString); - return; - } - - try { - int errorCode = control.closeErrorCode; - channel.onRemoteClose(errorCode); - } catch (Exception e) { - Log.e(TAG, "Error handling channel close", e); - } finally { - synchronized (lock) { - channels.remove(tokenString); - channelIdToToken.remove(control.channelId); - } - } - }); - } - - private void onChannelDataReceived(ChannelDataRequest dataRequest) { - if (dataRequest.header == null) { - Log.w(TAG, "Received data request with null header"); + public void sendCloseRequest(ChannelStateMachine channel, int errorCode) throws IOException { + WearableConnection connection = wearable.getActiveConnections().get(channel.token.nodeId); + if (connection == null) { + Log.w(TAG, "Cannot send close - connection not found"); return; } - ChannelDataHeader header = dataRequest.header; - Log.d(TAG, "onChannelDataReceived: channelId=" + header.channelId + - ", size=" + (dataRequest.payload != null ? dataRequest.payload.size() : 0)); - - handler.post(() -> { - String tokenString; - synchronized (lock) { - tokenString = channelIdToToken.get(header.channelId); - } - - if (tokenString == null) { - Log.w(TAG, "Received data for unknown channelId: " + header.channelId); - return; - } - - ChannelStateMachine channel = getChannel(tokenString); - if (channel == null) { - Log.w(TAG, "Channel not found for token: " + tokenString); - return; - } - - try { - byte[] data = dataRequest.payload != null ? dataRequest.payload.toByteArray() : new byte[0]; - boolean isFinal = dataRequest.finalMessage; - long requestId = header.requestId; - - channel.onDataReceived(data, isFinal, requestId); - - sendDataAck(channel, requestId, isFinal); - - } catch (Exception e) { - Log.e(TAG, "Error handling channel data", e); - } - }); - } - - private void onChannelDataAckReceived(ChannelDataAckRequest ackRequest) { - if (ackRequest.header == null) { - Log.w(TAG, "Received data ack with null header"); - return; - } + ChannelControlRequest controlRequest = new ChannelControlRequest.Builder() + .type(CHANNEL_CONTROL_TYPE_CLOSE) + .channelId(channel.token.channelId) + .fromChannelOperator(channel.token.thisNodeWasOpener) + .packageName(channel.token.appKey.packageName) + .signatureDigest(channel.token.appKey.signatureDigest) + .closeErrorCode(errorCode) + .build(); - ChannelDataHeader header = ackRequest.header; - Log.d(TAG, "onChannelDataAckReceived: channelId=" + header.channelId); + ChannelRequest channelRequest = new ChannelRequest.Builder() + .channelControlRequest(controlRequest) + .version(1) + .origin(0) + .build(); - handler.post(() -> { - String tokenString; - synchronized (lock) { - tokenString = channelIdToToken.get(header.channelId); - } + int requestId = requestIdCounter.getAndIncrement(); - if (tokenString == null) { - Log.w(TAG, "Received ack for unknown channelId: " + header.channelId); - return; - } + Request request = new Request.Builder() + .requestId(requestId) + .packageName(channel.token.appKey.packageName) + .signatureDigest(channel.token.appKey.signatureDigest) + .targetNodeId(channel.token.nodeId) + .sourceNodeId(localNodeId) + .request(channelRequest) + .generation(generationCounter.get()) + .build(); - ChannelStateMachine channel = getChannel(tokenString); - if (channel == null) { - Log.w(TAG, "Channel not found for token: " + tokenString); - return; - } + connection.writeMessage(new RootMessage.Builder() + .channelRequest(request) + .build()); - try { - long requestId = header.requestId; - boolean isFinal = ackRequest.finalMessage; - channel.onDataAckReceived(requestId, isFinal); - } catch (Exception e) { - Log.e(TAG, "Error handling data ack", e); - } - }); + Log.d(TAG, "Sent close request for " + channel.token); } - private void sendDataAck(ChannelStateMachine channel, long requestId, boolean isFinal) { + public boolean sendData(ChannelStateMachine channel, byte[] data, boolean isFinal, long requestId) { WearableConnection connection = wearable.getActiveConnections().get(channel.token.nodeId); if (connection == null) { - Log.w(TAG, "Cannot send ack - connection not found"); - return; + Log.w(TAG, "Cannot send data - connection not found"); + return false; } try { @@ -553,13 +453,14 @@ private void sendDataAck(ChannelStateMachine channel, long requestId, boolean is .requestId(requestId) .build(); - ChannelDataAckRequest ackRequest = new ChannelDataAckRequest.Builder() + ChannelDataRequest dataRequest = new ChannelDataRequest.Builder() .header(header) + .payload(ByteString.of(data)) .finalMessage(isFinal) .build(); ChannelRequest channelRequest = new ChannelRequest.Builder() - .channelDataAckRequest(ackRequest) + .channelDataRequest(dataRequest) .version(1) .origin(0) .build(); @@ -577,33 +478,35 @@ private void sendDataAck(ChannelStateMachine channel, long requestId, boolean is connection.writeMessage(new RootMessage.Builder() .channelRequest(request) .build()); + + return true; } catch (IOException e) { - Log.e(TAG, "Failed to send data ack", e); + Log.e(TAG, "Failed to send channel data", e); + return false; } } - public boolean sendData(ChannelStateMachine channel, byte[] data, boolean isFinal, long requestId) { + public void sendDataAck(ChannelStateMachine channel, long offset, boolean isFinal) { WearableConnection connection = wearable.getActiveConnections().get(channel.token.nodeId); if (connection == null) { - Log.w(TAG, "Cannot send data - connection not found"); - return false; + Log.w(TAG, "Cannot send ack - connection not found"); + return; } try { ChannelDataHeader header = new ChannelDataHeader.Builder() .channelId(channel.token.channelId) .fromChannelOperator(channel.token.thisNodeWasOpener) - .requestId(requestId) + .requestId(offset) .build(); - ChannelDataRequest dataRequest = new ChannelDataRequest.Builder() + ChannelDataAckRequest ackRequest = new ChannelDataAckRequest.Builder() .header(header) - .payload(ByteString.of(data)) .finalMessage(isFinal) .build(); ChannelRequest channelRequest = new ChannelRequest.Builder() - .channelDataRequest(dataRequest) + .channelDataAckRequest(ackRequest) .version(1) .origin(0) .build(); @@ -621,14 +524,12 @@ public boolean sendData(ChannelStateMachine channel, byte[] data, boolean isFina connection.writeMessage(new RootMessage.Builder() .channelRequest(request) .build()); - return true; + } catch (IOException e) { - Log.e(TAG, "Failed to send channel data", e); - return false; + Log.e(TAG, "Failed to send data ack", e); } } - private void onBinderDied(ChannelToken token) { Log.w(TAG, "Client died for channel: " + token); closeChannel(token, ChannelStatusCodes.CLOSE_REASON_LOCAL_CLOSE); @@ -665,4 +566,16 @@ public static void getOutputStreamError(IWearableCallbacks callbacks, int status Log.w(TAG, "Failed to send getOutputStream error", e); } } + + public ChannelCallbacks getChannelCallbacks() { + return channelCallbacks; + } + + public ChannelTransport getTransport() { + return transport; + } + + public Handler getHandler() { + return handler; + } } \ No newline at end of file diff --git a/play-services-wearable/core/src/main/java/org/microg/gms/wearable/channel/ChannelStateMachine.java b/play-services-wearable/core/src/main/java/org/microg/gms/wearable/channel/ChannelStateMachine.java index 8ab96e2421..c140b88d09 100644 --- a/play-services-wearable/core/src/main/java/org/microg/gms/wearable/channel/ChannelStateMachine.java +++ b/play-services-wearable/core/src/main/java/org/microg/gms/wearable/channel/ChannelStateMachine.java @@ -1,5 +1,7 @@ package org.microg.gms.wearable.channel; +import android.os.Build; +import android.os.Handler; import android.os.IBinder; import android.os.ParcelFileDescriptor; import android.os.RemoteException; @@ -7,239 +9,499 @@ import com.google.android.gms.wearable.internal.IChannelStreamCallbacks; -import java.io.FileOutputStream; import java.io.IOException; import java.nio.ByteBuffer; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.atomic.AtomicLong; public class ChannelStateMachine { private static final String TAG = "ChannelStateMachine"; + private static final int DEFAULT_CHUNK_SIZE = 8192; + private static final int MAX_CHUNK_SIZE = 65536; + private static final int RECEIVE_BUFFER_SIZE = 65536; + public static final int CONNECTION_STATE_NOT_STARTED = 0; public static final int CONNECTION_STATE_OPEN_SENT = 1; public static final int CONNECTION_STATE_ESTABLISHED = 2; public static final int CONNECTION_STATE_CLOSING = 3; public static final int CONNECTION_STATE_CLOSED = 4; + public static final int SENDING_STATE_NOT_STARTED = 5; public static final int SENDING_STATE_WAITING_TO_READ = 6; public static final int SENDING_STATE_WAITING_FOR_ACK = 7; public static final int SENDING_STATE_CLOSED = 8; + public static final int RECEIVING_STATE_WAITING_FOR_DATA = 9; public static final int RECEIVING_STATE_WAITING_TO_WRITE = 10; public static final int RECEIVING_STATE_CLOSED = 11; public final ChannelToken token; public final boolean isLocalOpener; + public final ChannelTransport transport; private final ChannelManager channelManager; private final ChannelCallbacks callbacks; private final IBinder.DeathRecipient deathRecipient; + private final Handler handler; + + public int connectionState = CONNECTION_STATE_NOT_STARTED; + public int sendingState = SENDING_STATE_NOT_STARTED; + public int receivingState = RECEIVING_STATE_WAITING_FOR_DATA; - private int connectionState = CONNECTION_STATE_NOT_STARTED; - private int sendingState = SENDING_STATE_NOT_STARTED; - private int receivingState = RECEIVING_STATE_WAITING_FOR_DATA; + public String channelPath; - private String path; - private long sequenceNumberIn; - private long sequenceNumberOut; + public long lastAckedOffset = 0; + public long sequenceNumber = 0; - private ParcelFileDescriptor inputFd; - private IChannelStreamCallbacks inputCallbacks; - private ByteBuffer receiveBuffer; + public ParcelFileDescriptor inputFd; + public IChannelStreamCallbacks inputCallbacks; + public ByteBuffer receiveBuffer; + public boolean receivePending = false; - private ParcelFileDescriptor outputFd; - private IChannelStreamCallbacks outputCallbacks; - private ByteBuffer sendBuffer; + public ParcelFileDescriptor outputFd; + public IChannelStreamCallbacks outputCallbacks; + public ByteBuffer sendBuffer; private long sendOffset; - private long sendMaxLength; - private int pendingCloseErrorCode; + long sendMaxLength; + + public PendingOperation openTimeoutOp; + public PendingOperation sendPendingOp; + + public int closeReason; + + public OpenChannelCallback openResultDispatcher; + + private final AtomicBoolean sendInProgress = new AtomicBoolean(false); + private final Object stateLock = new Object(); - private OpenChannelCallback openCallback; + public final long creationTime; + + public long totalDataSize = -1; + public long currentSendOffset = 0; + public long totalBytesSent = 0; + public long totalBytesReceived = 0; public ChannelStateMachine( ChannelToken token, ChannelManager channelManager, + ChannelTransport transport, ChannelCallbacks callbacks, boolean isLocalOpener, - IBinder.DeathRecipient deathRecipient) { + IBinder.DeathRecipient deathRecipient, + Handler handler) { this.token = token; this.channelManager = channelManager; + this.transport = transport; this.callbacks = callbacks; this.isLocalOpener = isLocalOpener; this.deathRecipient = deathRecipient; + this.handler = handler; + this.creationTime = System.currentTimeMillis(); + } + + public boolean hasInputStream() { + return inputFd != null; } - public int getConnectionState() { return connectionState; } - public int getSendingState() { return sendingState; } - public int getReceivingState() { return receivingState; } - public String getPath() { return path; } - public void setPath(String path) { this.path = path; } + public boolean hasOutputStream() { + return outputFd != null; + } + + private boolean isValidConnectionStateTransition(int from, int to) { + switch (from) { + case CONNECTION_STATE_NOT_STARTED: + return to == CONNECTION_STATE_OPEN_SENT || to == CONNECTION_STATE_ESTABLISHED; + case CONNECTION_STATE_OPEN_SENT: + return to == CONNECTION_STATE_ESTABLISHED || to == CONNECTION_STATE_CLOSED; + case CONNECTION_STATE_ESTABLISHED: + return to == CONNECTION_STATE_CLOSING || to == CONNECTION_STATE_CLOSED; + case CONNECTION_STATE_CLOSING: + return to == CONNECTION_STATE_CLOSED; + default: + return false; + } + } public void setConnectionState(int newState) { - if (Log.isLoggable(TAG, Log.VERBOSE)) { + synchronized (stateLock) { + if (connectionState == newState) { + Log.v(TAG, String.format("Channel(%s): Already in state %s", + token, getConnectionStateString(newState))); + return; + } + + if (newState == CONNECTION_STATE_CLOSED) { + this.connectionState = newState; + return; + } + + if (!isValidConnectionStateTransition(connectionState, newState)) { + Log.e(TAG, String.format("Channel(%s): Invalid state transition %s -> %s", + token, getConnectionStateString(connectionState), + getConnectionStateString(newState))); + throw new IllegalStateException("Invalid channel state transition: " + + getConnectionStateString(connectionState) + " -> " + + getConnectionStateString(newState)); + } + Log.v(TAG, String.format("Channel(%s): %s -> %s", - token, getConnectionStateString(connectionState), getConnectionStateString(newState))); + token, getConnectionStateString(connectionState), + getConnectionStateString(newState))); + + this.connectionState = newState; } - this.connectionState = newState; } public void setSendingState(int newState) { - if (Log.isLoggable(TAG, Log.VERBOSE)) { - Log.v(TAG, String.format("Channel(%s): Sender %s -> %s", - token, getSendingStateString(sendingState), getSendingStateString(newState))); - } + Log.v(TAG, String.format("Channel(%s): Sender %s -> %s", + token, getSendingStateString(sendingState), + getSendingStateString(newState))); this.sendingState = newState; } public void setReceivingState(int newState) { - if (Log.isLoggable(TAG, Log.VERBOSE)) { - Log.v(TAG, String.format("Channel(%s): Receiver %s -> %s", - token, getReceivingStateString(receivingState), getReceivingStateString(newState))); - } + Log.v(TAG, String.format("Channel(%s): Receiver %s -> %s", + token, getReceivingStateString(receivingState), + getReceivingStateString(newState))); this.receivingState = newState; } - public boolean hasInputStream() { return inputFd != null; } - public boolean hasOutputStream() { return outputFd != null; } - public boolean isInputClosed() { return receivingState == RECEIVING_STATE_CLOSED; } - public boolean isOutputClosed() { return sendingState == SENDING_STATE_CLOSED; } + public void sendOpenRequest() throws IOException { + synchronized (stateLock) { + + if (connectionState != CONNECTION_STATE_NOT_STARTED) { + throw new IllegalStateException("Cannot send OPEN from state: " + + getConnectionStateString(connectionState)); + } - public void setOpenCallback(OpenChannelCallback callback) { - this.openCallback = callback; + Log.d(TAG, "Sending open request for channel: " + token); + setConnectionState(CONNECTION_STATE_OPEN_SENT); + channelManager.sendOpenRequest(this); + } } - public void onChannelEstablished() { - setConnectionState(CONNECTION_STATE_ESTABLISHED); - if (openCallback != null) { - openCallback.onResult(ChannelStatusCodes.SUCCESS, token, path); - openCallback = null; + public void sendOpenAck() throws IOException { + synchronized (stateLock) { + if (connectionState != CONNECTION_STATE_ESTABLISHED && + connectionState != CONNECTION_STATE_OPEN_SENT) { + Log.w(TAG, "Sending ACK from unexpected state: " + + getConnectionStateString(connectionState)); + } + + Log.d(TAG, "Sending open ACK for channel: " + token); + channelManager.sendOpenAck(this); } } - public void onOpenFailed(int errorCode) { - if (openCallback != null) { - Log.w(TAG, "openChannel failed with error: " + errorCode); - openCallback.onResult(errorCode, null, path); - openCallback = null; + public void onChannelOpenAckReceived() throws ChannelException { + synchronized (stateLock) { + if (connectionState != CONNECTION_STATE_OPEN_SENT) { + Log.w(TAG, "Received OPEN_ACK in wrong state: " + connectionState); + throw new ChannelException(token, "Received OPEN_ACK in wrong state"); + } + + if (openResultDispatcher == null) { + Log.w(TAG, "Bad state: CONNECTION_STATE_OPEN_SENT but no callbacks"); + throw new ChannelException(token, "No open callback set"); + } + + if (openTimeoutOp == null) { + Log.w(TAG, "Bad state: CONNECTION_STATE_OPEN_SENT but no timeout operation"); + throw new ChannelException(token, "No timeout operation"); + } + + if (!openTimeoutOp.cancel()) { + Log.i(TAG, "Received OPEN_ACK but request already timed out"); + return; + } + + openTimeoutOp = null; + try { + openResultDispatcher.onResult(ChannelStatusCodes.SUCCESS, token, channelPath); + } catch (Exception e) { + Log.e(TAG, "Error in open result callback", e); + } finally { + openResultDispatcher = null; + } + + setConnectionState(CONNECTION_STATE_ESTABLISHED); } - setConnectionState(CONNECTION_STATE_CLOSED); } - public void onDataReceived(byte[] data, boolean isFinal, long requestId) throws IOException { - if (inputFd == null) { - Log.w(TAG, "Received data but no input FD set"); + public void processOutgoingData() throws IOException { + if (sendingState != SENDING_STATE_WAITING_TO_READ) { return; } - try { - FileOutputStream fos = new FileOutputStream(inputFd.getFileDescriptor()); - fos.write(data); - } catch (IOException e) { - Log.e(TAG, "Failed to write received data", e); - throw e; + if (outputFd == null) { + Log.e(TAG, "SENDING_STATE_WAITING_TO_READ but no output FD"); + return; } - if (isFinal) { - closeInputStream(ChannelStatusCodes.CLOSE_REASON_NORMAL, 0); + if (!sendInProgress.compareAndSet(false, true)) { + return; } - } - public void onDataAckReceived(long requestId, boolean isFinal) { - if (sendingState == SENDING_STATE_WAITING_FOR_ACK) { - if (isFinal) { - setSendingState(SENDING_STATE_CLOSED); + try { + if (sendBuffer == null) { + sendBuffer = ByteBuffer.allocate(DEFAULT_CHUNK_SIZE); + Log.d(TAG, "Created send buffer of size " + DEFAULT_CHUNK_SIZE); + } + + if (sendOffset > 0) { + skipBytes(sendOffset); + sendOffset = 0; + } + + sendBuffer.clear(); + + if (sendMaxLength >= 0 && sendMaxLength < sendBuffer.capacity()) { + sendBuffer.limit((int) sendMaxLength); + } + + int bytesRead = transport.read(outputFd, sendBuffer.array(), + sendBuffer.position(), sendBuffer.remaining()); + + if (bytesRead > 0) { + sendBuffer.position(sendBuffer.position() + bytesRead); + } + + sendBuffer.flip(); + + if (sendMaxLength >= 0) { + sendMaxLength -= sendBuffer.remaining(); + } + + byte[] data = new byte[sendBuffer.remaining()]; + sendBuffer.get(data); + + boolean isFinal = (bytesRead < 0) || (sendMaxLength == 0); + + long requestId = ++sequenceNumber; + + Log.d(TAG, String.format("Sending chunk: size=%d, isFinal=%b, seq=%d", + data.length, isFinal, requestId)); + + if (channelManager.sendData(this, data, isFinal, requestId)) { + setSendingState(SENDING_STATE_WAITING_FOR_ACK); + + sendPendingOp = new PendingOperation(handler, + () -> onSendTimeout(), 30000, "Send data chunk"); } else { - setSendingState(SENDING_STATE_WAITING_TO_READ); + Log.e(TAG, "Failed to send data chunk"); + onChannelOutputClosed(ChannelStatusCodes.INTERNAL_ERROR, 0); } + currentSendOffset += data.length; + totalBytesSent += data.length; + + Log.d(TAG, String.format("Sent chunk: %d bytes, offset=%d/%d (%.1f%%)", + data.length, + currentSendOffset, + totalDataSize, + totalDataSize > 0 ? (currentSendOffset * 100.0 / totalDataSize) : 0)); + + } finally { + sendInProgress.set(false); } + } - public void onRemoteClose(int errorCode) throws IOException { - closeInputStream(ChannelStatusCodes.CLOSE_REASON_REMOTE_CLOSE, errorCode); - closeOutputStream(ChannelStatusCodes.CLOSE_REASON_REMOTE_CLOSE, errorCode); - setConnectionState(CONNECTION_STATE_CLOSED); + private void skipBytes(long skip) throws IOException { + byte[] temp = new byte[8192]; + long remaining = skip; - if (callbacks != null) { - callbacks.onChannelClosed(token, path, - ChannelStatusCodes.CLOSE_REASON_REMOTE_CLOSE, errorCode); + while (remaining > 0) { + int toRead = (int) Math.min(temp.length, remaining); + int read = transport.read(outputFd, temp, 0, toRead); + if (read < 0) { + throw new IOException("EOF while skipping bytes"); + } + remaining -= read; } } - public void close() throws IOException { - setConnectionState(CONNECTION_STATE_CLOSED); + private void onSendTimeout() { + Log.w(TAG, "Sending data timed out. Closing channel"); + sendPendingOp = null; + + try { + onChannelOutputClosed(ChannelStatusCodes.CLOSE_REASON_REMOTE_CLOSE, 0); + onChannelInputClosed(ChannelStatusCodes.CLOSE_REASON_REMOTE_CLOSE, 0); + sendCloseRequest(0); + channelManager.removeChannel(token); + } catch (IOException e) { + Log.e(TAG, "Error handling send timeout", e); + } + } + + public void onDataAckReceived(long ackOffset, boolean isFinal) { + if (sendingState != SENDING_STATE_WAITING_FOR_ACK) { + Log.w(TAG, "Received ACK but not waiting for it"); + return; + } - if (openCallback != null) { - openCallback.onResult(ChannelStatusCodes.CHANNEL_CLOSED, null, path); - openCallback = null; + if (sendPendingOp != null) { + sendPendingOp.cancel(); + sendPendingOp = null; } - closeInputStream(ChannelStatusCodes.CLOSE_REASON_LOCAL_CLOSE, 0); - closeOutputStream(ChannelStatusCodes.CLOSE_REASON_LOCAL_CLOSE, 0); + lastAckedOffset = ackOffset; - receiveBuffer = null; - sendBuffer = null; + if (isFinal) { + Log.d(TAG, "Received final ACK, closing output"); + setSendingState(SENDING_STATE_CLOSED); + + try { + onChannelOutputClosed(ChannelStatusCodes.CLOSE_REASON_NORMAL, 0); + } catch (IOException e) { + Log.e(TAG, "Error closing output after final ACK", e); + } + } else { + setSendingState(SENDING_STATE_WAITING_TO_READ); + } } - public void closeInputStream(int closeReason, int errorCode) throws IOException { - if (inputFd == null) return; + public void onDataReceived(byte[] data, boolean isFinal, long offset) throws ChannelException { + synchronized (stateLock) { + if (connectionState != CONNECTION_STATE_ESTABLISHED && + connectionState != CONNECTION_STATE_CLOSING) { + throw new ChannelException(token, "Received data in wrong connection state: " + + getConnectionStateString(connectionState)); + } + } - if (inputCallbacks != null) { - unlinkToDeath(inputCallbacks.asBinder()); - if (closeReason != ChannelStatusCodes.CLOSE_REASON_NORMAL) { - try { - inputCallbacks.onChannelClosed(closeReason, errorCode); - } catch (RemoteException e) { - Log.w(TAG, "Failed to notify InputStream of close", e); + switch (receivingState) { + case RECEIVING_STATE_WAITING_FOR_DATA: + if (offset != lastAckedOffset) { + Log.w(TAG, "Received data with wrong offset: expected=" + + lastAckedOffset + ", got=" + offset); + return; } - } + + if (data.length > MAX_CHUNK_SIZE) { + Log.w(TAG, "Received payload longer than max buffer size"); + throw new ChannelException(token, "Payload too large"); + } + + if (receiveBuffer == null) { + receiveBuffer = ByteBuffer.allocate(RECEIVE_BUFFER_SIZE); + } + + receiveBuffer.clear(); + receiveBuffer.put(data); + receiveBuffer.flip(); + + if (isFinal) { + receivePending = true; + } + + if (inputFd != null) { + transport.setMode(inputFd, ChannelTransport.IOMode.WRITE); + } + + setReceivingState(RECEIVING_STATE_WAITING_TO_WRITE); + break; + + case RECEIVING_STATE_WAITING_TO_WRITE: + if (offset <= lastAckedOffset) { + Log.d(TAG, "Ignoring duplicate data packet"); + return; + } + + Log.w(TAG, "Received new data before ACK of last one"); + throw new ChannelException(token, "Data received before ACK"); + + case RECEIVING_STATE_CLOSED: + throw new ChannelException(token, "Received data after close"); } + } - try { - inputFd.close(); - } catch (IOException e) { - Log.w(TAG, "Failed to close receiving FD", e); + public void processIncomingBuffer() throws IOException { + if (receivingState != RECEIVING_STATE_WAITING_TO_WRITE) { + return; } - inputFd = null; - inputCallbacks = null; - setReceivingState(RECEIVING_STATE_CLOSED); + if (inputFd == null || receiveBuffer == null) { + return; + } - if (callbacks != null) { - callbacks.onChannelInputClosed(token, path, closeReason, errorCode); + if (receiveBuffer.hasRemaining()) { + int toWrite = receiveBuffer.remaining(); + int written = transport.write(inputFd, receiveBuffer.array(), + receiveBuffer.position(), toWrite); + + if (written > 0) { + receiveBuffer.position(receiveBuffer.position() + written); + } + } + + if (!receiveBuffer.hasRemaining()) { + lastAckedOffset += receiveBuffer.capacity(); + + channelManager.sendDataAck(this, lastAckedOffset, receivePending); + + if (receivePending) { + onChannelInputClosed(ChannelStatusCodes.CLOSE_REASON_NORMAL, 0); + } else { + transport.setMode(inputFd, ChannelTransport.IOMode.NONE); + setReceivingState(RECEIVING_STATE_WAITING_FOR_DATA); + } } } - public void closeOutputStream(int closeReason, int errorCode) throws IOException { - if (outputFd == null) return; + public void sendCloseRequest(int errorCode) throws IOException { + Log.d(TAG, "Sending close request: errorCode=" + errorCode); + channelManager.sendCloseRequest(this, errorCode); + } - if (outputCallbacks != null) { - unlinkToDeath(outputCallbacks.asBinder()); - if (closeReason != ChannelStatusCodes.CLOSE_REASON_NORMAL) { - try { - outputCallbacks.onChannelClosed(closeReason, errorCode); - } catch (RemoteException e) { - Log.w(TAG, "Failed to notify OutputStream of close", e); - } + public void onRemoteCloseReceived(int errorCode) throws IOException, ChannelException { + Log.d(TAG, "Received remote close: errorCode=" + errorCode); + + if (openResultDispatcher != null) { + try { + openResultDispatcher.onResult(ChannelStatusCodes.CHANNEL_CLOSED, null, channelPath); + } catch (Exception e) { + Log.e(TAG, "Error in open result callback", e); + } finally { + openResultDispatcher = null; } } - try { - outputFd.close(); - } catch (IOException e) { - Log.w(TAG, "Failed to close sending FD", e); + if (outputCallbacks != null) { + try { + outputCallbacks.onChannelClosed(ChannelStatusCodes.CLOSE_REASON_REMOTE_CLOSE, errorCode); + } catch (RemoteException e) { + Log.w(TAG, "Failed to notify output callbacks", e); + } } - outputFd = null; - outputCallbacks = null; - setSendingState(SENDING_STATE_CLOSED); + onChannelInputClosed(ChannelStatusCodes.CLOSE_REASON_REMOTE_CLOSE, errorCode); - if (callbacks != null) { - callbacks.onChannelOutputClosed(token, path, closeReason, errorCode); + if (sendingState == SENDING_STATE_CLOSED || sendingState == SENDING_STATE_NOT_STARTED) { + sendCloseRequest(errorCode); + throw new ChannelException(token, "Remote close"); + } else { + setConnectionState(CONNECTION_STATE_CLOSING); + closeReason = errorCode; + + if (sendingState == SENDING_STATE_WAITING_TO_READ) { + processOutgoingData(); + } } } - public void clearOpenCallback() { - this.openCallback = null; + public void abortOpenChannel() throws IOException, ChannelException { + if (openResultDispatcher != null) { + Log.w(TAG, "openChannel cancelled - remote node not reachable"); + openResultDispatcher.onResult(ChannelStatusCodes.CHANNEL_NOT_CONNECTED, null, channelPath); + openResultDispatcher = null; + } + + onChannelOutputClosed(ChannelStatusCodes.CLOSE_REASON_LOCAL_CLOSE, 0); + onChannelInputClosed(ChannelStatusCodes.CLOSE_REASON_LOCAL_CLOSE, 0); + throw new ChannelException(token, "Open channel aborted"); } public void setInputStream(ParcelFileDescriptor fd, IChannelStreamCallbacks callbacks) @@ -257,7 +519,10 @@ public void setInputStream(ParcelFileDescriptor fd, IChannelStreamCallbacks call this.inputFd = fd; this.inputCallbacks = callbacks; + transport.register(fd); linkToDeath(callbacks); + + Log.d(TAG, "Input stream configured for channel " + token); } public void setOutputStream(ParcelFileDescriptor fd, IChannelStreamCallbacks callbacks, @@ -282,9 +547,134 @@ public void setOutputStream(ParcelFileDescriptor fd, IChannelStreamCallbacks cal this.outputCallbacks = callbacks; this.sendOffset = startOffset; this.sendMaxLength = length; + this.currentSendOffset = startOffset; + if (length >= 0) { + this.totalDataSize = length; + } else { + try { + long fileSize = 0; + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.CUPCAKE) { + fileSize = fd.getStatSize(); + } + this.totalDataSize = fileSize - startOffset; + } catch (Exception e) { + this.totalDataSize = -1; + } + } + + transport.register(fd); + transport.setMode(fd, ChannelTransport.IOMode.READ); setSendingState(SENDING_STATE_WAITING_TO_READ); linkToDeath(callbacks); + + Log.d(TAG, String.format("Output stream configured: offset=%d, length=%d", + startOffset, length)); + } + + public void forceClose() throws IOException { + if (connectionState == CONNECTION_STATE_CLOSED) return; + + + if (openTimeoutOp != null) { + openTimeoutOp.cancel(); + openTimeoutOp = null; + } + + if (sendPendingOp != null) { + sendPendingOp.cancel(); + sendPendingOp = null; + } + + if (openResultDispatcher != null) { + try { + openResultDispatcher.onResult(ChannelStatusCodes.CHANNEL_CLOSED, null, channelPath); + } catch (Exception e) { + Log.e(TAG, "Error in open result callback", e); + } finally { + openResultDispatcher = null; + } + } + + try { + onChannelInputClosed(ChannelStatusCodes.CLOSE_REASON_LOCAL_CLOSE, 0); + } catch (Exception e) { + Log.w(TAG, "Error closing input", e); + } + + onChannelOutputClosed(ChannelStatusCodes.CLOSE_REASON_LOCAL_CLOSE, 0); + + receiveBuffer = null; + sendBuffer = null; + + setConnectionState(CONNECTION_STATE_CLOSED); + } + + public void onChannelInputClosed(int closeReason, int errorCode) throws IOException { + if (inputFd == null) return; + + if (inputCallbacks != null) { + unlinkToDeath(inputCallbacks.asBinder()); + if (closeReason != ChannelStatusCodes.CLOSE_REASON_NORMAL) { + try { + inputCallbacks.onChannelClosed(closeReason, errorCode); + } catch (RemoteException e) { + Log.w(TAG, "Failed to notify InputStream of close", e); + } + } + } + + transport.unregister(inputFd); + + try { + inputFd.close(); + } catch (IOException e) { + Log.w(TAG, "Failed to close receiving FD", e); + } + + inputFd = null; + inputCallbacks = null; + setReceivingState(RECEIVING_STATE_CLOSED); + + if (callbacks != null) { + try { + callbacks.onChannelInputClosed(token, channelPath, closeReason, errorCode); + } catch (Exception e) { + Log.e(TAG, "Error in input closed callback", e); + } + } + } + + public void onChannelOutputClosed(int closeReason, int errorCode) throws IOException { + if (outputFd == null) return; + + if (outputCallbacks != null) { + unlinkToDeath(outputCallbacks.asBinder()); + if (closeReason != ChannelStatusCodes.CLOSE_REASON_NORMAL) { + try { + outputCallbacks.onChannelClosed(closeReason, errorCode); + } catch (RemoteException e) { + Log.w(TAG, "Failed to notify OutputStream of close", e); + } + } + } + + transport.unregister(outputFd); + + try { + outputFd.close(); + } catch (IOException e) { + Log.w(TAG, "Failed to close sending FD", e); + } + + outputFd = null; + outputCallbacks = null; + sendBuffer = null; + setSendingState(SENDING_STATE_CLOSED); + + if (callbacks != null) { + callbacks.onChannelOutputClosed(token, channelPath, closeReason, errorCode); + } } private void linkToDeath(IChannelStreamCallbacks callbacks) throws RemoteException { @@ -336,9 +726,10 @@ public static String getReceivingStateString(int state) { @Override public String toString() { return "ChannelStateMachine{token=" + token + - ", path='" + path + "'" + + ", path='" + channelPath + "'" + ", connection=" + getConnectionStateString(connectionState) + ", sending=" + getSendingStateString(sendingState) + - ", receiving=" + getReceivingStateString(receivingState) + "}"; + ", receiving=" + getReceivingStateString(receivingState) + "}"+ + ", age=" + (System.currentTimeMillis() - creationTime) + "ms}"; } } \ No newline at end of file diff --git a/play-services-wearable/core/src/main/java/org/microg/gms/wearable/channel/ChannelStatusCodes.java b/play-services-wearable/core/src/main/java/org/microg/gms/wearable/channel/ChannelStatusCodes.java index 04c0483333..00de5820f9 100644 --- a/play-services-wearable/core/src/main/java/org/microg/gms/wearable/channel/ChannelStatusCodes.java +++ b/play-services-wearable/core/src/main/java/org/microg/gms/wearable/channel/ChannelStatusCodes.java @@ -13,6 +13,9 @@ public class ChannelStatusCodes { public static final int INVALID_ARGUMENT = 10003; public static final int CHANNEL_NOT_FOUND = 10004; public static final int ALREADY_IN_PROGRESS = 10005; + public static final int CHANNEL_LIMIT_REACHED = 10006; + public static final int INVALID_PACKAGE = 10007; + public static final int INVALID_PATH = 10008; public static String getStatusName(int status) { switch (status) { @@ -25,6 +28,9 @@ public static String getStatusName(int status) { case INVALID_ARGUMENT: return "INVALID_ARGUMENT"; case CHANNEL_NOT_FOUND: return "NOT_FOUND"; case ALREADY_IN_PROGRESS: return "ALREADY_IN_PROGRESS"; + case CHANNEL_LIMIT_REACHED: return "LIMIT_REACHED"; + case INVALID_PACKAGE: return "INVALID_PACKAGE"; + case INVALID_PATH: return "INVALID_PATH"; default: return "UNKNOWN(" + status + ")"; } } diff --git a/play-services-wearable/core/src/main/java/org/microg/gms/wearable/channel/ChannelTable.java b/play-services-wearable/core/src/main/java/org/microg/gms/wearable/channel/ChannelTable.java new file mode 100644 index 0000000000..661e9f4e9a --- /dev/null +++ b/play-services-wearable/core/src/main/java/org/microg/gms/wearable/channel/ChannelTable.java @@ -0,0 +1,62 @@ +package org.microg.gms.wearable.channel; + +import android.util.Log; + +import java.util.Collection; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + +public class ChannelTable { + private static final String TAG = "ChannelTable"; + + private final Map channels = new ConcurrentHashMap<>(); + + public ChannelStateMachine get(String tokenString) { + return channels.get(tokenString); + } + + public ChannelStateMachine get(ChannelToken token) { + return channels.get(token.toTokenString()); + } + + public ChannelStateMachine get(String nodeId, long channelId, boolean isOpener) { + for (ChannelStateMachine channel : channels.values()) { + ChannelToken token = channel.token; + if (token.nodeId.equals(nodeId) && + token.channelId == channelId && + token.thisNodeWasOpener == isOpener) { + return channel; + } + } + return null; + } + + public void put(ChannelToken token, ChannelStateMachine channel) { + String key = token.toTokenString(); + channels.put(key, channel); + Log.d(TAG, "Added channel to table: " + token + " (total: " + channels.size() + ")"); + } + + public ChannelStateMachine remove(ChannelToken token) { + String key = token.toTokenString(); + ChannelStateMachine removed = channels.remove(key); + if (removed != null) { + Log.d(TAG, "Removed channel from table: " + token + " (remaining: " + channels.size() + ")"); + } + return removed; + } + + public Collection values() { + return channels.values(); + } + + public void clear() { + int count = channels.size(); + channels.clear(); + Log.d(TAG, "Cleared " + count + " channels from table"); + } + + public int size() { + return channels.size(); + } +} \ No newline at end of file diff --git a/play-services-wearable/core/src/main/java/org/microg/gms/wearable/channel/ChannelTask.java b/play-services-wearable/core/src/main/java/org/microg/gms/wearable/channel/ChannelTask.java new file mode 100644 index 0000000000..035c73c25e --- /dev/null +++ b/play-services-wearable/core/src/main/java/org/microg/gms/wearable/channel/ChannelTask.java @@ -0,0 +1,70 @@ +package org.microg.gms.wearable.channel; + +import android.util.Log; + +import java.io.IOException; + +public abstract class ChannelTask implements Runnable { + private static final String TAG = "ChannelTask"; + + protected final ChannelManager channelManager; + protected final boolean requiresRunning; + protected ChannelStateMachine channel; + + protected ChannelTask(ChannelManager channelManager) { + this(channelManager, true); + } + + protected ChannelTask(ChannelManager channelManager, boolean requiresRunning) { + this.channelManager = channelManager; + this.requiresRunning = requiresRunning; + } + + protected abstract void execute() throws IOException, ChannelException; + + @Override + public final void run() { + if (requiresRunning && !channelManager.isRunning()) { + Log.d(TAG, "Skipping task - manager not running"); + return; + } + + try { + execute(); + } catch (ChannelException e) { + Log.w(TAG, "Channel exception in task", e); + if (channel != null) { + try { + channel.forceClose(); + channelManager.removeChannel(channel.token); + } catch (Exception ex) { + Log.e(TAG, "Error during cleanup", ex); + } + } + } catch (IOException e) { + Log.w(TAG, "IO exception in task", e); + if (channel != null) { + try { + channel.forceClose(); + channelManager.removeChannel(channel.token); + } catch (Exception ex) { + Log.e(TAG, "Error during cleanup", ex); + } + } + } catch (RuntimeException e) { + Log.e(TAG, "Uncaught runtime exception in task", e); + if (channel != null) { + try { + channel.forceClose(); + channelManager.removeChannel(channel.token); + } catch (Exception ex) { + Log.e(TAG, "Error during cleanup", ex); + } + } + } + } + + protected void setChannel(ChannelStateMachine channel) { + this.channel = channel; + } +} \ No newline at end of file diff --git a/play-services-wearable/core/src/main/java/org/microg/gms/wearable/channel/ChannelToken.java b/play-services-wearable/core/src/main/java/org/microg/gms/wearable/channel/ChannelToken.java index 98d24f0c02..c0150a05d9 100644 --- a/play-services-wearable/core/src/main/java/org/microg/gms/wearable/channel/ChannelToken.java +++ b/play-services-wearable/core/src/main/java/org/microg/gms/wearable/channel/ChannelToken.java @@ -14,8 +14,10 @@ public final class ChannelToken { public final AppKey appKey; public final long channelId; public final boolean thisNodeWasOpener; + public final boolean isReliable; - public ChannelToken(String nodeId, AppKey appKey, long channelId, boolean thisNodeWasOpener) { + public ChannelToken(String nodeId, AppKey appKey, long channelId, + boolean thisNodeWasOpener, boolean isReliable) { if (nodeId == null) throw new NullPointerException("nodeId is null"); if (appKey == null) throw new NullPointerException("appKey is null"); if (channelId < 0) throw new IllegalArgumentException("Negative channelId: " + channelId); @@ -24,6 +26,7 @@ public ChannelToken(String nodeId, AppKey appKey, long channelId, boolean thisNo this.appKey = appKey; this.channelId = channelId; this.thisNodeWasOpener = thisNodeWasOpener; + this.isReliable = isReliable; } public static ChannelToken fromString(AppKey expectedAppKey, String tokenString) @@ -51,7 +54,8 @@ public static ChannelToken fromString(AppKey expectedAppKey, String tokenString) proto.nodeId, tokenAppKey, proto.channelId, - proto.thisNodeWasOpener + proto.thisNodeWasOpener, + proto.isReliable ); } catch (InvalidChannelTokenException e) { throw e; @@ -67,6 +71,7 @@ public String toTokenString() { proto.signatureDigest = appKey.signatureDigest; proto.channelId = channelId; proto.thisNodeWasOpener = thisNodeWasOpener; + proto.isReliable = isReliable; return TOKEN_PREFIX + Base64.encodeToString(proto.toByteArray(), Base64.NO_WRAP); } @@ -105,6 +110,7 @@ static class ChannelTokenProto { String signatureDigest; long channelId; boolean thisNodeWasOpener; + boolean isReliable; static ChannelTokenProto parseFrom(byte[] data) throws Exception { ChannelTokenProto proto = new ChannelTokenProto(); @@ -115,6 +121,13 @@ static ChannelTokenProto parseFrom(byte[] data) throws Exception { proto.signatureDigest = dis.readUTF(); proto.channelId = dis.readLong(); proto.thisNodeWasOpener = dis.readBoolean(); + + if (dis.available() > 0) { + proto.isReliable = dis.readBoolean(); + } else { + proto.isReliable = true; + } + return proto; } @@ -127,6 +140,7 @@ byte[] toByteArray() { dos.writeUTF(signatureDigest); dos.writeLong(channelId); dos.writeBoolean(thisNodeWasOpener); + dos.writeBoolean(isReliable); return baos.toByteArray(); } catch (Exception e) { throw new RuntimeException(e); diff --git a/play-services-wearable/core/src/main/java/org/microg/gms/wearable/channel/ChannelTransport.java b/play-services-wearable/core/src/main/java/org/microg/gms/wearable/channel/ChannelTransport.java new file mode 100644 index 0000000000..a01adf2f1c --- /dev/null +++ b/play-services-wearable/core/src/main/java/org/microg/gms/wearable/channel/ChannelTransport.java @@ -0,0 +1,92 @@ +package org.microg.gms.wearable.channel; + +import android.os.Build; +import android.os.ParcelFileDescriptor; +import android.util.Log; + +import java.io.EOFException; +import java.io.FileInputStream; +import java.io.FileOutputStream; +import java.io.IOException; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + +public class ChannelTransport { + private static final String TAG = "ChannelTransport"; + + private final Map fdModes = new ConcurrentHashMap<>(); + + public enum IOMode { + NONE, + READ, + WRITE + } + + public void register(ParcelFileDescriptor fd) { + if (fd != null) { + fdModes.put(fd, IOMode.NONE); + Log.d(TAG, "Registered FD: " + fd); + } + } + + public void unregister(ParcelFileDescriptor fd) { + if (fd != null) { + fdModes.remove(fd); + Log.d(TAG, "Unregistered FD: " + fd); + } + } + + public void setMode(ParcelFileDescriptor fd, IOMode mode) { + if (fd != null) { + fdModes.put(fd, mode); + if (Log.isLoggable(TAG, Log.VERBOSE)) { + Log.v(TAG, "Set FD mode to " + mode + ": " + fd); + } + } + } + + public int read(ParcelFileDescriptor fd, byte[] buf, int offset, int len) throws IOException { + if (fd == null || !fd.getFileDescriptor().valid()) { + throw new IOException("Invalid file descriptor"); + } + + try { + FileInputStream fis = new FileInputStream(fd.getFileDescriptor()); + int bytesRead = fis.read(buf, offset, len); + + if (Log.isLoggable(TAG, Log.VERBOSE)) { + Log.v(TAG, "Read " + bytesRead + " bytes from FD"); + } + + return bytesRead; + + } catch (EOFException e) { + return -1; + } + } + + public int write(ParcelFileDescriptor fd, byte[] buf, int offset, int len) throws IOException { + if (fd == null || !fd.getFileDescriptor().valid()) { + throw new IOException("Invalid file descriptor"); + } + + try { + FileOutputStream fos = new FileOutputStream(fd.getFileDescriptor()); + fos.write(buf, offset, len); + fos.flush(); + + if (Log.isLoggable(TAG, Log.VERBOSE)) { + Log.v(TAG, "Wrote " + len + " bytes to FD"); + } + + return len; + + } catch (EOFException e) { + return -1; + } + } + + public void clear() { + fdModes.clear(); + } +} \ No newline at end of file diff --git a/play-services-wearable/core/src/main/java/org/microg/gms/wearable/channel/OnChannelControlTask.java b/play-services-wearable/core/src/main/java/org/microg/gms/wearable/channel/OnChannelControlTask.java new file mode 100644 index 0000000000..ef808020ef --- /dev/null +++ b/play-services-wearable/core/src/main/java/org/microg/gms/wearable/channel/OnChannelControlTask.java @@ -0,0 +1,333 @@ +package org.microg.gms.wearable.channel; + +import android.os.IBinder; +import android.util.Log; + +import org.microg.gms.wearable.WearableConnection; +import org.microg.gms.wearable.proto.AppKey; +import org.microg.gms.wearable.proto.ChannelControlRequest; +import org.microg.gms.wearable.proto.Request; + +import java.io.IOException; + +public class OnChannelControlTask extends ChannelTask { + private static final String TAG = "OnChannelControlTask"; + + private final String sourceNodeId; + private final WearableConnection connection; + private final Request request; + + public OnChannelControlTask(ChannelManager manager, String sourceNodeId, + WearableConnection connection, Request request) { + super(manager); + this.sourceNodeId = sourceNodeId; + this.connection = connection; + this.request = request; + } + + @Override + protected void execute() throws IOException, ChannelException { + ChannelControlRequest control = request.request.channelControlRequest; + + if (control == null) { + Log.w(TAG, "Channel control request is null"); + return; + } + + int type = control.type; + Log.d(TAG, "onChannelControlReceived: type=" + type + + ", channelId=" + control.channelId + + ", from=" + sourceNodeId); + + switch (type) { + case ChannelManager.CHANNEL_CONTROL_TYPE_OPEN: + handleChannelOpen(control); + break; + case ChannelManager.CHANNEL_CONTROL_TYPE_OPEN_ACK: + handleChannelOpenAck(control); + break; + case ChannelManager.CHANNEL_CONTROL_TYPE_CLOSE: + handleChannelClose(control); + break; + default: + Log.w(TAG, "Unknown channel control type: " + type); + } + } + + private void handleChannelOpen(ChannelControlRequest control) throws IOException, ChannelException { + Log.d(TAG, "handleChannelOpen: channelId=" + control.channelId + + ", path=" + control.path + ", from=" + sourceNodeId); + + if (connection == null) { + Log.w(TAG, "Received channel open from null connection: " + sourceNodeId); + throw new ChannelException(null, "Connection not active"); + } + + if (control.packageName == null || control.packageName.isEmpty()) { + Log.w(TAG, "Channel open missing package name"); + throw new ChannelException(null, "Missing package name"); + } + + if (control.signatureDigest == null || control.signatureDigest.isEmpty()) { + Log.w(TAG, "Channel open missing signature digest"); + throw new ChannelException(null, "Missing signature digest"); + } + + if (control.path == null || control.path.isEmpty()) { + Log.w(TAG, "Channel open missing path"); + throw new ChannelException(null, "Missing path"); + } + + AppKey appKey = new AppKey(control.packageName, control.signatureDigest); + + boolean isReliable = control.isReliable != null ? control.isReliable : true; + + ChannelToken token = new ChannelToken(sourceNodeId, appKey, control.channelId, false, isReliable); + + ChannelStateMachine channel = channelManager.channelTable.get(token); + + if (channel != null) { + handleDuplicateChannelOpen(channel, control); + return; + } + + if (!checkChannelLimits(sourceNodeId, appKey)) { + Log.w(TAG, "Channel limit reached for " + sourceNodeId + "/" + appKey.packageName); + sendOpenError(token, control.path, ChannelStatusCodes.CHANNEL_LIMIT_REACHED); + return; + } + + IBinder.DeathRecipient deathRecipient = () -> onChannelBinderDied(token); + + ChannelCallbacks callbacks = channelManager.getChannelCallbacks(); + + channel = new ChannelStateMachine( + token, + channelManager, + channelManager.getTransport(), // FIX: Use shared transport + callbacks, + false, + deathRecipient, + channelManager.getHandler() // FIX: Add getter for handler + ); + + channel.channelPath = control.path; + channel.setConnectionState(ChannelStateMachine.CONNECTION_STATE_ESTABLISHED); + + channelManager.channelTable.put(token, channel); + + try { + channel.sendOpenAck(); + } catch (IOException e) { + Log.e(TAG, "Failed to send open ACK", e); + channelManager.channelTable.remove(token); + throw e; + } + + scheduleHealthCheck(channel); + + if (callbacks != null) { + try { + callbacks.onChannelOpened(token, control.path); + } catch (Exception e) { + Log.e(TAG, "Error in channel opened callback", e); + } + } + + Log.d(TAG, "Channel opened successfully: " + channel); + } + + private void handleDuplicateChannelOpen(ChannelStateMachine existingChannel, + ChannelControlRequest control) throws IOException, ChannelException { + Log.d(TAG, "Received duplicate OPEN for existing channel: " + existingChannel.token); + + setChannel(existingChannel); + + if (existingChannel.connectionState == ChannelStateMachine.CONNECTION_STATE_ESTABLISHED) { + if (!existingChannel.channelPath.equals(control.path)) { + Log.w(TAG, "Duplicate OPEN with different path. Expected: " + + existingChannel.channelPath + ", got: " + control.path); + throw new ChannelException(existingChannel.token, "Path mismatch on duplicate OPEN"); + } + + Log.d(TAG, "Resending ACK for duplicate OPEN request"); + existingChannel.sendOpenAck(); + } else { + Log.w(TAG, "Received OPEN for channel in state: " + + ChannelStateMachine.getConnectionStateString(existingChannel.connectionState)); + throw new ChannelException(existingChannel.token, + "Channel exists in unexpected state: " + existingChannel.connectionState); + } + } + + private boolean checkChannelLimits(String nodeId, AppKey appKey) { + int channelsForNode = 0; + int channelsForApp = 0; + + for (ChannelStateMachine channel : channelManager.channelTable.values()) { + if (channel.token.nodeId.equals(nodeId)) { + channelsForNode++; + + if (channel.token.appKey.equals(appKey)) { + channelsForApp++; + } + } + } + + final int MAX_CHANNELS_PER_NODE = 20; + final int MAX_CHANNELS_PER_APP = 10; + + if (channelsForNode >= MAX_CHANNELS_PER_NODE) { + Log.w(TAG, "Node " + nodeId + " has reached channel limit: " + channelsForNode); + return false; + } + + if (channelsForApp >= MAX_CHANNELS_PER_APP) { + Log.w(TAG, "App " + appKey.packageName + " has reached channel limit: " + channelsForApp); + return false; + } + + return true; + } + + private void sendOpenError(ChannelToken token, String path, int errorCode) { + try { + ChannelStateMachine tempChannel = new ChannelStateMachine( + token, + channelManager, + channelManager.getTransport(), + null, + false, + null, + channelManager.getHandler() + ); + + tempChannel.channelPath = path; + tempChannel.sendCloseRequest(errorCode); + } catch (IOException e) { + Log.w(TAG, "Failed to send open error", e); + } + } + + private void scheduleHealthCheck(ChannelStateMachine channel) { + channelManager.getHandler().postDelayed( + () -> performHealthCheck(channel), + 30000 + ); + } + + private void performHealthCheck(ChannelStateMachine channel) { + if (!channelManager.isRunning()) { + return; + } + + ChannelStateMachine current = channelManager.channelTable.get(channel.token); + if (current == null) { + return; + } + + if (current.connectionState == ChannelStateMachine.CONNECTION_STATE_ESTABLISHED) { + long timeSinceCreation = System.currentTimeMillis() - channel.creationTime; + + if (timeSinceCreation > 60000) { + if (current.sendingState == ChannelStateMachine.SENDING_STATE_NOT_STARTED && + current.receivingState == ChannelStateMachine.RECEIVING_STATE_WAITING_FOR_DATA && + !current.hasInputStream() && !current.hasOutputStream()) { + + Log.w(TAG, "Channel appears unused after 1 minute: " + channel.token); + } + } + + channelManager.getHandler().postDelayed( + () -> performHealthCheck(channel), + 60000 + ); + } + } + + private void onChannelBinderDied(ChannelToken token) { + Log.w(TAG, "Channel client died: " + token); + + channelManager.getHandler().post(() -> { + try { + ChannelStateMachine channel = channelManager.channelTable.get(token); + if (channel != null) { + channel.forceClose(); + channelManager.channelTable.remove(token); + } + } catch (Exception e) { + Log.e(TAG, "Error handling binder death", e); + } + }); + } + + + private void handleChannelOpenAck(ChannelControlRequest control) throws ChannelException { + Log.d(TAG, "handleChannelOpenAck: channelId=" + control.channelId); + + ChannelStateMachine channel = channelManager.channelTable.get( + sourceNodeId, control.channelId, true); + + if (channel == null) { + Log.w(TAG, "Received open ACK for unknown channel: " + control.channelId); + return; + } + + setChannel(channel); + + if (!sourceNodeId.equals(channel.token.nodeId)) { + Log.w(TAG, String.format("Got OPEN_ACK from wrong node for channel %d. Expected %s got %s", + control.channelId, channel.token.nodeId, sourceNodeId)); + return; + } + + if (channel.connectionState != ChannelStateMachine.CONNECTION_STATE_OPEN_SENT) { + Log.w(TAG, "Received OPEN_ACK in wrong state: " + + ChannelStateMachine.getConnectionStateString(channel.connectionState)); + + if (channel.connectionState == ChannelStateMachine.CONNECTION_STATE_ESTABLISHED) { + Log.d(TAG, "Ignoring duplicate OPEN_ACK"); + return; + } + + throw new ChannelException(channel.token, "Received OPEN_ACK in wrong state"); + } + + try { + channel.onChannelOpenAckReceived(); + Log.d(TAG, "Channel established: " + channel); + + scheduleHealthCheck(channel); + + } catch (ChannelException e) { + Log.e(TAG, "Error processing OPEN_ACK", e); + channelManager.channelTable.remove(channel.token); + throw e; + } + } + + private void handleChannelClose(ChannelControlRequest control) throws IOException, ChannelException { + Log.d(TAG, "handleChannelClose: channelId=" + control.channelId); + + ChannelStateMachine channel = channelManager.channelTable.get( + sourceNodeId, control.channelId, !control.fromChannelOperator); + + if (channel == null) { + Log.w(TAG, "Received close for unknown channel: " + control.channelId); + return; + } + + setChannel(channel); + + try { + int errorCode = control.closeErrorCode; + channel.onRemoteCloseReceived(errorCode); + Log.d(TAG, "Channel closed by remote: " + channel.token); + } catch (ChannelException e) { + Log.e(TAG, "Error handling remote close", e); + throw e; + } finally { + channelManager.channelTable.remove(channel.token); + } + } +} \ No newline at end of file diff --git a/play-services-wearable/core/src/main/java/org/microg/gms/wearable/channel/OnChannelDataAckTask.java b/play-services-wearable/core/src/main/java/org/microg/gms/wearable/channel/OnChannelDataAckTask.java new file mode 100644 index 0000000000..115d19af64 --- /dev/null +++ b/play-services-wearable/core/src/main/java/org/microg/gms/wearable/channel/OnChannelDataAckTask.java @@ -0,0 +1,50 @@ +package org.microg.gms.wearable.channel; + +import android.util.Log; + +import org.microg.gms.wearable.proto.ChannelDataAckRequest; +import org.microg.gms.wearable.proto.ChannelDataHeader; + +public class OnChannelDataAckTask extends ChannelTask { + private static final String TAG = "OnChannelDataAckTask"; + + private final String sourceNodeId; + private final ChannelDataAckRequest ackRequest; + + public OnChannelDataAckTask(ChannelManager manager, String sourceNodeId, ChannelDataAckRequest ackRequest) { + super(manager); + this.ackRequest = ackRequest; + this.sourceNodeId = sourceNodeId; + } + + @Override + protected void execute() throws ChannelException { + if (ackRequest.header == null) { + Log.w(TAG, "Received data ACK with null header"); + return; + } + + ChannelDataHeader header = ackRequest.header; + Log.d(TAG, "onChannelDataAckReceived: channelId=" + header.channelId + + ", from=" + sourceNodeId); + + ChannelStateMachine channel = channelManager.channelTable.get( + sourceNodeId, + header.channelId, + header.fromChannelOperator + ); + + if (channel == null) { + Log.w(TAG, "Received ACK for unknown channel: " + header.channelId + + " from node: " + sourceNodeId); + return; + } + + setChannel(channel); + + long requestId = header.requestId; + boolean isFinal = ackRequest.finalMessage; + + channel.onDataAckReceived(requestId, isFinal); + } +} \ No newline at end of file diff --git a/play-services-wearable/core/src/main/java/org/microg/gms/wearable/channel/OnChannelDataTask.java b/play-services-wearable/core/src/main/java/org/microg/gms/wearable/channel/OnChannelDataTask.java new file mode 100644 index 0000000000..13d302080b --- /dev/null +++ b/play-services-wearable/core/src/main/java/org/microg/gms/wearable/channel/OnChannelDataTask.java @@ -0,0 +1,51 @@ +package org.microg.gms.wearable.channel; + +import android.util.Log; + +import org.microg.gms.wearable.proto.ChannelDataHeader; +import org.microg.gms.wearable.proto.ChannelDataRequest; + +import java.io.IOException; + +public class OnChannelDataTask extends ChannelTask { + private static final String TAG = "OnChannelDataTask"; + + private final String sourceNodeId; + private final ChannelDataRequest dataRequest; + + public OnChannelDataTask(ChannelManager manager, String sourceNodeId, + ChannelDataRequest dataRequest) { + super(manager); + this.sourceNodeId = sourceNodeId; + this.dataRequest = dataRequest; + } + + @Override + protected void execute() throws IOException, ChannelException { + if (dataRequest.header == null) { + Log.w(TAG, "Received data request with null header"); + return; + } + + ChannelDataHeader header = dataRequest.header; + Log.d(TAG, "onChannelDataReceived: channelId=" + header.channelId + + ", size=" + (dataRequest.payload != null ? dataRequest.payload.size() : 0)); + + ChannelStateMachine channel = channelManager.channelTable.get( + sourceNodeId, header.channelId, !header.fromChannelOperator); + + if (channel == null) { + Log.w(TAG, "Received data for unknown channel: " + header.channelId); + return; + } + + setChannel(channel); + + byte[] data = dataRequest.payload != null ? + dataRequest.payload.toByteArray() : new byte[0]; + boolean isFinal = dataRequest.finalMessage; + long requestId = header.requestId; + + channel.onDataReceived(data, isFinal, requestId); + } +} \ No newline at end of file diff --git a/play-services-wearable/core/src/main/java/org/microg/gms/wearable/channel/PendingOperation.java b/play-services-wearable/core/src/main/java/org/microg/gms/wearable/channel/PendingOperation.java new file mode 100644 index 0000000000..a95ff1859f --- /dev/null +++ b/play-services-wearable/core/src/main/java/org/microg/gms/wearable/channel/PendingOperation.java @@ -0,0 +1,47 @@ +package org.microg.gms.wearable.channel; + +import android.os.Handler; +import android.util.Log; + +import java.util.concurrent.atomic.AtomicBoolean; + +public class PendingOperation { + private static final String TAG = "PendingOperation"; + + private final Handler handler; + private final Runnable timeoutRunnable; + private final AtomicBoolean cancelled = new AtomicBoolean(false); + private final String description; + + public PendingOperation(Handler handler, Runnable onTimeout, long timeoutMs, String description) { + this.handler = handler; + this.timeoutRunnable = onTimeout; + this.description = description; + + handler.postDelayed(timeoutRunnable, timeoutMs); + + if (Log.isLoggable(TAG, Log.VERBOSE)) { + Log.v(TAG, "Created pending operation: " + description + " (timeout=" + timeoutMs + "ms)"); + } + } + + public boolean cancel() { + if (cancelled.compareAndSet(false, true)) { + handler.removeCallbacks(timeoutRunnable); + if (Log.isLoggable(TAG, Log.VERBOSE)) { + Log.v(TAG, "Cancelled pending operation: " + description); + } + return true; + } + return false; + } + + public boolean isCancelled() { + return cancelled.get(); + } + + @Override + public String toString() { + return "PendingOperation{" + description + ", cancelled=" + cancelled.get() + "}"; + } +} \ No newline at end of file diff --git a/play-services-wearable/core/src/main/proto/wearable.proto b/play-services-wearable/core/src/main/proto/wearable.proto index af7f9f6f5f..7c6a74c555 100644 --- a/play-services-wearable/core/src/main/proto/wearable.proto +++ b/play-services-wearable/core/src/main/proto/wearable.proto @@ -40,6 +40,7 @@ message ChannelControlRequest { optional string signatureDigest = 5; optional string path = 6; optional int32 closeErrorCode = 7; + optional bool isReliable = 8; } message ChannelDataAckRequest { diff --git a/play-services-wearable/src/main/java/com/google/android/gms/wearable/WearableListenerService.java b/play-services-wearable/src/main/java/com/google/android/gms/wearable/WearableListenerService.java index 8aa5b091cf..f39ab6c911 100644 --- a/play-services-wearable/src/main/java/com/google/android/gms/wearable/WearableListenerService.java +++ b/play-services-wearable/src/main/java/com/google/android/gms/wearable/WearableListenerService.java @@ -157,105 +157,62 @@ private boolean post(Runnable runnable) { @Override public void onDataChanged(final DataHolder data) throws RemoteException { - post(new Runnable() { - @Override - public void run() { - WearableListenerService.this.onDataChanged(new DataEventBuffer(data)); - } - }); + post(() -> WearableListenerService.this.onDataChanged(new DataEventBuffer(data))); } @Override public void onMessageReceived(final MessageEventParcelable messageEvent) throws RemoteException { - post(new Runnable() { - @Override - public void run() { - WearableListenerService.this.onMessageReceived(messageEvent); - } - }); + post(() -> WearableListenerService.this.onMessageReceived(messageEvent)); } @Override public void onPeerConnected(final NodeParcelable node) throws RemoteException { - post(new Runnable() { - @Override - public void run() { - WearableListenerService.this.onPeerConnected(node); - } - }); + post(() -> WearableListenerService.this.onPeerConnected(node)); } @Override public void onPeerDisconnected(final NodeParcelable node) throws RemoteException { - post(new Runnable() { - @Override - public void run() { - WearableListenerService.this.onPeerDisconnected(node); - } - }); + post(() -> WearableListenerService.this.onPeerDisconnected(node)); } @Override public void onConnectedNodes(final List nodes) throws RemoteException { - post(new Runnable() { - @Override - public void run() { - WearableListenerService.this.onConnectedNodes(new ArrayList(nodes)); - } - }); + post(() -> WearableListenerService.this.onConnectedNodes(new ArrayList(nodes))); } @Override public void onConnectedCapabilityChanged(final CapabilityInfoParcelable capabilityInfo) throws RemoteException { - post(new Runnable() { - @Override - public void run() { - WearableListenerService.this.onCapabilityChanged(capabilityInfo); - } - }); + post(() -> WearableListenerService.this.onCapabilityChanged(capabilityInfo)); } @Override public void onNotificationReceived(final AncsNotificationParcelable notification) throws RemoteException { - post(new Runnable() { - @Override - public void run() { - WearableListenerService.this.onNotificationReceived(notification); - } - }); + post(() -> WearableListenerService.this.onNotificationReceived(notification)); } @Override public void onEntityUpdate(final AmsEntityUpdateParcelable update) throws RemoteException { - post(new Runnable() { - @Override - public void run() { - WearableListenerService.this.onEntityUpdate(update); - } - }); + post(() -> WearableListenerService.this.onEntityUpdate(update)); } @Override public void onChannelEvent(final ChannelEventParcelable channelEvent) throws RemoteException { - post(new Runnable() { - @Override - public void run() { - switch (channelEvent.eventType) { - case 1: - WearableListenerService.this.onChannelOpened(new ChannelImpl(channelEvent.channel)); - break; - case 2: - WearableListenerService.this.onChannelClosed(new ChannelImpl(channelEvent.channel), channelEvent.closeReason, channelEvent.appSpecificErrorCode); - break; - case 3: - WearableListenerService.this.onInputClosed(new ChannelImpl(channelEvent.channel), channelEvent.closeReason, channelEvent.appSpecificErrorCode); - break; - case 4: - WearableListenerService.this.onOutputClosed(new ChannelImpl(channelEvent.channel), channelEvent.closeReason, channelEvent.appSpecificErrorCode); - break; - default: - Log.w(TAG, "Unknown ChannelEvent.eventType"); - } + post(() -> { + switch (channelEvent.eventType) { + case 1: + WearableListenerService.this.onChannelOpened(new ChannelImpl(channelEvent.channel)); + break; + case 2: + WearableListenerService.this.onChannelClosed(new ChannelImpl(channelEvent.channel), channelEvent.closeReason, channelEvent.appSpecificErrorCode); + break; + case 3: + WearableListenerService.this.onInputClosed(new ChannelImpl(channelEvent.channel), channelEvent.closeReason, channelEvent.appSpecificErrorCode); + break; + case 4: + WearableListenerService.this.onOutputClosed(new ChannelImpl(channelEvent.channel), channelEvent.closeReason, channelEvent.appSpecificErrorCode); + break; + default: + Log.w(TAG, "Unknown ChannelEvent.eventType"); } }); } diff --git a/play-services-wearable/src/main/java/com/google/android/gms/wearable/internal/ChannelEventParcelable.java b/play-services-wearable/src/main/java/com/google/android/gms/wearable/internal/ChannelEventParcelable.java index 39dedeeb22..eecedcb8e4 100644 --- a/play-services-wearable/src/main/java/com/google/android/gms/wearable/internal/ChannelEventParcelable.java +++ b/play-services-wearable/src/main/java/com/google/android/gms/wearable/internal/ChannelEventParcelable.java @@ -20,7 +20,6 @@ import org.microg.safeparcel.SafeParceled; public class ChannelEventParcelable extends AutoSafeParcelable { - @SafeParceled(1) private int versionCode = 1; @SafeParceled(2) @@ -32,5 +31,39 @@ public class ChannelEventParcelable extends AutoSafeParcelable { @SafeParceled(5) public int appSpecificErrorCode; + public static final int EVENT_TYPE_CHANNEL_OPENED = 1; + public static final int EVENT_TYPE_CHANNEL_CLOSED = 2; + public static final int EVENT_TYPE_INPUT_CLOSED = 3; + public static final int EVENT_TYPE_OUTPUT_CLOSED = 4; + + private ChannelEventParcelable() {} + + public ChannelEventParcelable(ChannelParcelable channel, int eventType, int closeReason, int appSpecificErrorCode) { + this.channel = channel; + this.eventType = eventType; + this.closeReason = closeReason; + this.appSpecificErrorCode = appSpecificErrorCode; + } + + public String getTypeString() { + switch (eventType) { + case EVENT_TYPE_CHANNEL_OPENED: return "CHANNEL_OPENED"; + case EVENT_TYPE_CHANNEL_CLOSED: return "CHANNEL_CLOSED"; + case EVENT_TYPE_INPUT_CLOSED: return "INPUT_CLOSED"; + case EVENT_TYPE_OUTPUT_CLOSED: return "OUTPUT_CLOSED"; + default: return "UNKNOWN(" + eventType + ")"; + } + } + + @Override + public String toString() { + return "ChannelEventParcelable{" + + "channel=" + channel + + ", type=" + getTypeString() + + ", closeReason=" + closeReason + + ", appErrorCode=" + appSpecificErrorCode + + '}'; + } + public static final Creator CREATOR = new AutoCreator(ChannelEventParcelable.class); } diff --git a/play-services-wearable/src/main/java/com/google/android/gms/wearable/internal/ChannelParcelable.java b/play-services-wearable/src/main/java/com/google/android/gms/wearable/internal/ChannelParcelable.java index 79ffb070ae..c41fed5794 100644 --- a/play-services-wearable/src/main/java/com/google/android/gms/wearable/internal/ChannelParcelable.java +++ b/play-services-wearable/src/main/java/com/google/android/gms/wearable/internal/ChannelParcelable.java @@ -29,8 +29,7 @@ public class ChannelParcelable extends AutoSafeParcelable { @SafeParceled(4) public String path; - private ChannelParcelable() { - } + private ChannelParcelable() {} public ChannelParcelable(String token, String nodeId, String path) { this.token = token; @@ -38,5 +37,26 @@ public ChannelParcelable(String token, String nodeId, String path) { this.path = path; } + public final int hashCode() { + return this.token.hashCode(); + } + + public final String toString() { + int i = 0; + for (char c : this.token.toCharArray()) { + i += c; + } + String strTrim = this.token.trim(); + int length = strTrim.length(); + if (length > 25) { + strTrim = strTrim.substring(0, 10) + "..." + + strTrim.substring(length - 10, length) + "::" + i; + } + return "Channel{token=" +strTrim + + ", nodeId=" + this.nodeId + + ", path=" + this.path + + "}"; + } + public static final Creator CREATOR = new AutoCreator(ChannelParcelable.class); } From 5012986b3627fbde7a33353d9c02e63ffb409e26 Mon Sep 17 00:00:00 2001 From: deadYokai Date: Sat, 7 Feb 2026 20:41:19 +0200 Subject: [PATCH 16/29] implement few unimplemented methods --- .../wearable/ClockworkNodePreferences.java | 11 + .../gms/wearable/WearableServiceImpl.java | 239 +++++++++++++++++- .../wearable/internal/IWearableService.aidl | 2 +- .../wearable/internal/PackageStorageInfo.java | 26 ++ .../internal/StorageInfoResponse.java | 23 +- 5 files changed, 294 insertions(+), 7 deletions(-) create mode 100644 play-services-wearable/src/main/java/com/google/android/gms/wearable/internal/PackageStorageInfo.java diff --git a/play-services-wearable/core/src/main/java/org/microg/gms/wearable/ClockworkNodePreferences.java b/play-services-wearable/core/src/main/java/org/microg/gms/wearable/ClockworkNodePreferences.java index 73dc389e08..ff182f80e1 100644 --- a/play-services-wearable/core/src/main/java/org/microg/gms/wearable/ClockworkNodePreferences.java +++ b/play-services-wearable/core/src/main/java/org/microg/gms/wearable/ClockworkNodePreferences.java @@ -76,4 +76,15 @@ public long getNextSeqId() { return seqIdBlock + seqIdInBlock++; } } + + public void clear() { + synchronized (lock) { + SharedPreferences preferences = context.getSharedPreferences( + CLOCKWORK_NODE_PREFERENCES, Context.MODE_PRIVATE); + preferences.edit().clear().commit(); + + seqIdBlock = 0; + seqIdInBlock = -1; + } + } } diff --git a/play-services-wearable/core/src/main/java/org/microg/gms/wearable/WearableServiceImpl.java b/play-services-wearable/core/src/main/java/org/microg/gms/wearable/WearableServiceImpl.java index 4cc8f15c6f..8f5a9c2908 100644 --- a/play-services-wearable/core/src/main/java/org/microg/gms/wearable/WearableServiceImpl.java +++ b/play-services-wearable/core/src/main/java/org/microg/gms/wearable/WearableServiceImpl.java @@ -18,11 +18,16 @@ import android.Manifest; import android.content.Context; +import android.content.pm.ApplicationInfo; +import android.content.pm.PackageManager; +import android.database.Cursor; +import android.database.sqlite.SQLiteDatabase; import android.net.Uri; import android.os.Handler; import android.os.Parcel; import android.os.ParcelFileDescriptor; import android.os.RemoteException; +import android.text.TextUtils; import android.util.Base64; import android.util.Log; @@ -44,6 +49,7 @@ import org.microg.gms.wearable.channel.OpenChannelCallback; import org.microg.gms.wearable.proto.AppKey; +import java.io.File; import java.io.FileNotFoundException; import java.io.IOException; import java.util.ArrayList; @@ -344,8 +350,51 @@ public void run(IWearableCallbacks callbacks) throws RemoteException { @Override public void getCompanionPackageForNode(IWearableCallbacks callbacks, String nodeId) throws RemoteException { - Log.d(TAG, "unimplemented Method getCompanionPackageForNode"); + Log.d(TAG, "getCompanionPackageForNode: " + nodeId); + postMain(callbacks, () -> { + try { + if (TextUtils.isEmpty(nodeId)) { + Log.e(TAG, "getCompanionPackageForNode: empty nodeId"); + callbacks.onGetCompanionPackageForNodeResponse( + new GetCompanionPackageForNodeResponse(CommonStatusCodes.ERROR, "")); + return; + } + + if ("cloud".equals(nodeId)) { + Log.d(TAG, "getCompanionPackageForNode: cloud node has no package"); + callbacks.onGetCompanionPackageForNodeResponse( + new GetCompanionPackageForNodeResponse(CommonStatusCodes.ERROR, "")); + return; + } + + ConnectionConfiguration[] configurations = wearable.getConfigurations(); + if (configurations != null) { + for (ConnectionConfiguration config : configurations) { + if (nodeId.equals(config.nodeId) || nodeId.equals(config.peerNodeId)) { + String packageName = config.packageName != null ? config.packageName : ""; + Log.d(TAG, "getCompanionPackageForNode: found package " + packageName + " for node " + nodeId); + callbacks.onGetCompanionPackageForNodeResponse( + new GetCompanionPackageForNodeResponse(CommonStatusCodes.SUCCESS, packageName)); + return; + } + } + } + + Log.w(TAG, "getCompanionPackageForNode: node " + nodeId + " not found"); + callbacks.onGetCompanionPackageForNodeResponse( + new GetCompanionPackageForNodeResponse(CommonStatusCodes.ERROR, "")); + + } catch (Exception e) { + Log.e(TAG, "getCompanionPackageForNode: exception during processing", e); + try { + callbacks.onGetCompanionPackageForNodeResponse( + new GetCompanionPackageForNodeResponse(CommonStatusCodes.INTERNAL_ERROR, "")); + } catch (RemoteException re) { + Log.w(TAG, "Failed to send error response", re); + } + } + }); } @Override @@ -398,18 +447,26 @@ public void setCloudSyncSetting(IWearableCallbacks callbacks, boolean enable) th @Override public void getCloudSyncSetting(IWearableCallbacks callbacks) throws RemoteException { + Log.d(TAG, "unimplemented Method: getCloudSyncSetting"); + // disabled by default callbacks.onGetCloudSyncSettingResponse(new GetCloudSyncSettingResponse(0, false)); } @Override public void getCloudSyncOptInStatus(IWearableCallbacks callbacks) throws RemoteException { Log.d(TAG, "unimplemented Method: getCloudSyncOptInStatus"); + // opt out by default callbacks.onGetCloudSyncOptInStatusResponse(new GetCloudSyncOptInStatusResponse(0, false, true)); } @Override - public void sendRemoteCommand(IWearableCallbacks callbacks, byte b) throws RemoteException { - Log.d(TAG, "unimplemented Method: sendRemoteCommand: " + b); + public void sendAmsRemoteCommand(IWearableCallbacks callbacks, byte command) throws RemoteException { + Log.d(TAG, "unimplemented Method sendAmsRemoteCommand: " + command); + + postMain(callbacks, () -> { + // return error, because we dont have AMS handling + callbacks.onStatus(new Status(CommonStatusCodes.INTERNAL_ERROR)); + }); } @Override @@ -718,12 +775,184 @@ public void removeListener(IWearableCallbacks callbacks, RemoveListenerRequest r @Override public void getStorageInformation(IWearableCallbacks callbacks) throws RemoteException { - Log.d(TAG, "unimplemented Method: getStorageInformation"); + Log.d(TAG, "getStorageInformation"); + postMain(callbacks, () -> { + try { + NodeDatabaseHelper nodeDatabase = wearable.getNodeDatabase(); + PackageManager packageManager = context.getPackageManager(); + SQLiteDatabase db = nodeDatabase.getReadableDatabase(); + + File databasePath = context.getDatabasePath("node.db"); + long totalDatabaseSize = databasePath != null ? databasePath.length() : 0L; + + Map packageInfoMap = new HashMap<>(); + + Map packageIdToName = new HashMap<>(); + Cursor appKeysCursor = db.query("appkeys", new String[]{"_id", "packageName"}, + null, null, null, null, null); + + while (appKeysCursor.moveToNext()) { + String id = appKeysCursor.getString(0); + String packageName = appKeysCursor.getString(1); + packageIdToName.put(id, packageName); + } + appKeysCursor.close(); + + for (Map.Entry entry : packageIdToName.entrySet()) { + String appKeyId = entry.getKey(); + String packageName = entry.getValue(); + + long dataItemsSize = getTableSizeForAppKey(db, "dataitems", "appkeys_id", appKeyId); + + long assetsSize = getAssetsSizeForPackage(db, nodeDatabase, appKeyId); + + String appLabel = packageName; + try { + ApplicationInfo appInfo = packageManager.getApplicationInfo(packageName, 0); + CharSequence label = packageManager.getApplicationLabel(appInfo); + if (label != null) { + appLabel = label.toString(); + } + } catch (PackageManager.NameNotFoundException e) { + Log.w(TAG, "Package not found: " + packageName); + } + + long totalSize = dataItemsSize + assetsSize; + + if (totalSize > 0) { + PackageStorageInfo info = new PackageStorageInfo( + packageName, + appLabel, + totalSize + ); + packageInfoMap.put(packageName, info); + } + } + + List packageInfoList = new ArrayList<>(packageInfoMap.values()); + PackageStorageInfo[] packageInfoArray = packageInfoList.toArray( + new PackageStorageInfo[packageInfoList.size()]); + + StorageInfoResponse response = new StorageInfoResponse( + CommonStatusCodes.SUCCESS, + totalDatabaseSize, + packageInfoArray + ); + + Log.d(TAG, "getStorageInformation: total db size=" + totalDatabaseSize + + ", packages=" + packageInfoArray.length); + callbacks.onStorageInfoResponse(response); + + } catch (Exception e) { + Log.e(TAG, "getStorageInformation: exception during processing", e); + try { + callbacks.onStorageInfoResponse(new StorageInfoResponse( + CommonStatusCodes.INTERNAL_ERROR, 0L, new PackageStorageInfo[0])); + } catch (RemoteException re) { + Log.w(TAG, "Failed to send error response", re); + } + } + }); + } + + private long getTableSizeForAppKey(SQLiteDatabase db, String tableName, + String keyColumn, String appKeyId) { + long totalSize = 0; + + Cursor cursor = db.query(tableName, null, keyColumn + "=?", + new String[]{appKeyId}, null, null, null); + + int rowCount = cursor.getCount(); + cursor.close(); + + totalSize = rowCount * 1024L; + + return totalSize; + } + + private long getAssetsSizeForPackage(SQLiteDatabase db, NodeDatabaseHelper nodeDatabase, + String appKeyId) { + long totalSize = 0; + + try { + Set assetDigests = new HashSet<>(); + Cursor aclCursor = db.query("assetsacls", new String[]{"assets_digest"}, + "appkeys_id=?", new String[]{appKeyId}, null, null, null); + + while (aclCursor.moveToNext()) { + String digest = aclCursor.getString(0); + assetDigests.add(digest); + } + aclCursor.close(); + + for (String digest : assetDigests) { + File assetFile = new File(context.getFilesDir(), "assets/" + digest); + if (assetFile.exists()) { + totalSize += assetFile.length(); + } + } + } catch (Exception e) { + Log.w(TAG, "Error calculating assets size", e); + } + + return totalSize; } @Override public void clearStorage(IWearableCallbacks callbacks) throws RemoteException { - Log.d(TAG, "unimplemented Method: clearStorage"); + Log.d(TAG, "clearStorage"); + + postMain(callbacks, () -> { + try { + Log.d(TAG, "clearStorage: starting storage clear"); + + NodeDatabaseHelper nodeDatabase = wearable.getNodeDatabase(); + SQLiteDatabase db = nodeDatabase.getWritableDatabase(); + + db.execSQL("DELETE FROM dataitems"); + db.execSQL("DELETE FROM assets"); + db.execSQL("DELETE FROM assetrefs"); + db.execSQL("DELETE FROM assetsacls"); + db.execSQL("DELETE FROM nodeinfo"); + db.execSQL("DELETE FROM appkeys"); + db.execSQL("DELETE FROM archiveDataItems"); + db.execSQL("DELETE FROM archiveAssetRefs"); + + Log.d(TAG, "clearStorage: database tables cleared"); + + File assetsDir = new File(context.getFilesDir(), "assets"); + if (assetsDir.exists() && assetsDir.isDirectory()) { + File[] assetFiles = assetsDir.listFiles(); + if (assetFiles != null) { + for (File file : assetFiles) { + if (file.isFile()) { + file.delete(); + } + } + } + } + Log.d(TAG, "clearStorage: asset files cleared"); + + ClockworkNodePreferences prefs = wearable.getClockworkNodePreferences(); + if (prefs != null) { + prefs.clear(); + } + Log.d(TAG, "clearStorage: preferences cleared"); + + callbacks.onStatus(Status.SUCCESS); + + Log.d(TAG, "clearStorage: complete"); + + } catch (Exception e) { + Log.e(TAG, "clearStorage: exception during clearing storage", e); + try { + callbacks.onStatus(new Status(CommonStatusCodes.INTERNAL_ERROR)); + } catch (RemoteException re) { + Log.w(TAG, "Failed to send error response", re); + } + } + }); + } @Override diff --git a/play-services-wearable/src/main/aidl/com/google/android/gms/wearable/internal/IWearableService.aidl b/play-services-wearable/src/main/aidl/com/google/android/gms/wearable/internal/IWearableService.aidl index e04142fbe8..203743aafa 100644 --- a/play-services-wearable/src/main/aidl/com/google/android/gms/wearable/internal/IWearableService.aidl +++ b/play-services-wearable/src/main/aidl/com/google/android/gms/wearable/internal/IWearableService.aidl @@ -88,7 +88,7 @@ interface IWearableService { void getCloudSyncSetting(IWearableCallbacks callbacks) = 50; void getCloudSyncOptInStatus(IWearableCallbacks callbacks) = 51; - void sendRemoteCommand(IWearableCallbacks callbacks, byte b) = 52; + void sendAmsRemoteCommand(IWearableCallbacks callbacks, byte command) = 52; void getConsentStatus(IWearableCallbacks callbacks) = 64; void addAccountToConsent(IWearableCallbacks callbacks, in AddAccountToConsentRequest request) = 65; diff --git a/play-services-wearable/src/main/java/com/google/android/gms/wearable/internal/PackageStorageInfo.java b/play-services-wearable/src/main/java/com/google/android/gms/wearable/internal/PackageStorageInfo.java new file mode 100644 index 0000000000..c711641050 --- /dev/null +++ b/play-services-wearable/src/main/java/com/google/android/gms/wearable/internal/PackageStorageInfo.java @@ -0,0 +1,26 @@ +package com.google.android.gms.wearable.internal; + +import org.microg.safeparcel.AutoSafeParcelable; +import org.microg.safeparcel.SafeParceled; + +public class PackageStorageInfo extends AutoSafeParcelable { + @SafeParceled(1) + private final int version = 1; + @SafeParceled(2) + public String packageName; + @SafeParceled(3) + public String appLabel; + @SafeParceled(4) + public long size; + + private PackageStorageInfo() {} + + public PackageStorageInfo(String packageName, String appLabel, long size) { + this.packageName = packageName; + this.appLabel = appLabel; + this.size = size; + } + + public static final Creator CREATOR = + new AutoCreator<>(PackageStorageInfo.class); +} \ No newline at end of file diff --git a/play-services-wearable/src/main/java/com/google/android/gms/wearable/internal/StorageInfoResponse.java b/play-services-wearable/src/main/java/com/google/android/gms/wearable/internal/StorageInfoResponse.java index 53738b20cb..790236a15b 100644 --- a/play-services-wearable/src/main/java/com/google/android/gms/wearable/internal/StorageInfoResponse.java +++ b/play-services-wearable/src/main/java/com/google/android/gms/wearable/internal/StorageInfoResponse.java @@ -17,7 +17,28 @@ package com.google.android.gms.wearable.internal; import org.microg.safeparcel.AutoSafeParcelable; +import org.microg.safeparcel.SafeParceled; public class StorageInfoResponse extends AutoSafeParcelable { - public static final Creator CREATOR = new AutoCreator(StorageInfoResponse.class); + @SafeParceled(1) + private final int version = 1; + @SafeParceled(2) + public int statusCode; + @SafeParceled(3) + public long totalSize; + @SafeParceled(4) + public PackageStorageInfo[] packageStorageInfo; + + private StorageInfoResponse() {} + + public StorageInfoResponse(int statusCode, long totalSize, + PackageStorageInfo[] packageStorageInfo) { + this.statusCode = statusCode; + this.totalSize = totalSize; + this.packageStorageInfo = packageStorageInfo; + } + + public static final Creator CREATOR = + new AutoCreator<>(StorageInfoResponse.class); } + From 68131171837b5f36b4694179a2ed927c66edd65a Mon Sep 17 00:00:00 2001 From: Teccheck Date: Sat, 28 Feb 2026 18:23:51 +0100 Subject: [PATCH 17/29] Make use of packageName in ConnectionConfiguration --- .../org/microg/gms/wearable/ConfigurationDatabaseHelper.java | 4 +++- .../src/main/java/org/microg/gms/wearable/WearableImpl.java | 3 ++- .../java/org/microg/gms/wearable/WearableServiceImpl.java | 2 ++ .../google/android/gms/wearable/ConnectionConfiguration.java | 4 ++-- 4 files changed, 9 insertions(+), 4 deletions(-) diff --git a/play-services-wearable/core/src/main/java/org/microg/gms/wearable/ConfigurationDatabaseHelper.java b/play-services-wearable/core/src/main/java/org/microg/gms/wearable/ConfigurationDatabaseHelper.java index 5b96cdcc53..e4d78579bd 100644 --- a/play-services-wearable/core/src/main/java/org/microg/gms/wearable/ConfigurationDatabaseHelper.java +++ b/play-services-wearable/core/src/main/java/org/microg/gms/wearable/ConfigurationDatabaseHelper.java @@ -166,11 +166,12 @@ private static ConnectionConfiguration configFromCursor(final Cursor cursor) { int role = cursor.getInt(cursor.getColumnIndexOrThrow(COLUMN_ROLE)); int enabled = cursor.getInt(cursor.getColumnIndexOrThrow(COLUMN_CONNECTION_ENABLED)); String nodeId = cursor.getString(cursor.getColumnIndexOrThrow(COLUMN_NODE_ID)); + String packageName = cursor.getString(cursor.getColumnIndexOrThrow(COLUMN_PACKAGE_NAME)); if (NULL_STRING.equals(name)) name = null; if (NULL_STRING.equals(pairedBtAddress)) pairedBtAddress = null; - return new ConnectionConfiguration(name, pairedBtAddress, connectionType, role, enabled > 0, nodeId); + return new ConnectionConfiguration(name, pairedBtAddress, connectionType, role, enabled > 0, nodeId, packageName); } public ConnectionConfiguration getConfiguration(String name) { @@ -205,6 +206,7 @@ public void putConfiguration(ConnectionConfiguration config, String oldNodeId) { contentValues.put(COLUMN_PAIRED_BT_ADDRESS, NULL_STRING); } + contentValues.put(COLUMN_PACKAGE_NAME, config.packageName); contentValues.put(COLUMN_CONNECTION_TYPE, config.type); contentValues.put(COLUMN_ROLE, config.role); contentValues.put(COLUMN_CONNECTION_ENABLED, config.enabled ? 1 : 0); diff --git a/play-services-wearable/core/src/main/java/org/microg/gms/wearable/WearableImpl.java b/play-services-wearable/core/src/main/java/org/microg/gms/wearable/WearableImpl.java index 9eb7b10299..860910d63e 100644 --- a/play-services-wearable/core/src/main/java/org/microg/gms/wearable/WearableImpl.java +++ b/play-services-wearable/core/src/main/java/org/microg/gms/wearable/WearableImpl.java @@ -400,7 +400,8 @@ public synchronized ConnectionConfiguration[] getConfigurations() { c.type, c.role, c.enabled, - c.nodeId + c.nodeId, + c.packageName ); configurations[i].connected = c.connected; configurations[i].peerNodeId = c.peerNodeId; diff --git a/play-services-wearable/core/src/main/java/org/microg/gms/wearable/WearableServiceImpl.java b/play-services-wearable/core/src/main/java/org/microg/gms/wearable/WearableServiceImpl.java index 8f5a9c2908..c90ac6be7b 100644 --- a/play-services-wearable/core/src/main/java/org/microg/gms/wearable/WearableServiceImpl.java +++ b/play-services-wearable/core/src/main/java/org/microg/gms/wearable/WearableServiceImpl.java @@ -109,6 +109,8 @@ public void run(IWearableCallbacks callbacks) throws RemoteException { @RequiresPermission(Manifest.permission.BLUETOOTH_CONNECT) @Override public void putConfig(IWearableCallbacks callbacks, final ConnectionConfiguration config) throws RemoteException { + config.packageName = this.packageName; + postMain(callbacks, () -> { wearable.createConnection(config); callbacks.onStatus(Status.SUCCESS); diff --git a/play-services-wearable/src/main/java/com/google/android/gms/wearable/ConnectionConfiguration.java b/play-services-wearable/src/main/java/com/google/android/gms/wearable/ConnectionConfiguration.java index cc2dc8f742..fb4fda1076 100644 --- a/play-services-wearable/src/main/java/com/google/android/gms/wearable/ConnectionConfiguration.java +++ b/play-services-wearable/src/main/java/com/google/android/gms/wearable/ConnectionConfiguration.java @@ -69,8 +69,8 @@ public ConnectionConfiguration(String name, String address, int type, int role, this(name, address, type, role, enabled, false, null, false, null, null, 0, null, false, false, null, false, null, 0); } - public ConnectionConfiguration(String name, String address, int type, int role, boolean enabled, String nodeId) { - this(name, address, type, role, enabled, false, null, false, nodeId, null, 0, null, false, false, null, false, null, 0); + public ConnectionConfiguration(String name, String address, int type, int role, boolean enabled, String nodeId, String packageName) { + this(name, address, type, role, enabled, false, null, false, nodeId, packageName, 0, null, false, false, null, false, null, 0); } public ConnectionConfiguration(String name, String address, int type, int role, boolean enabled, From f8c8d6960d570e81719692141a74ed7173168cd0 Mon Sep 17 00:00:00 2001 From: Teccheck Date: Sat, 28 Feb 2026 18:29:41 +0100 Subject: [PATCH 18/29] Add WearOS config UI --- play-services-core/build.gradle | 1 + .../org/microg/gms/ui/SettingsFragment.kt | 5 + .../org/microg/gms/ui/WearConfigFragment.kt | 134 ++++++++++++++++++ .../org/microg/gms/ui/WearConfigPreference.kt | 61 ++++++++ .../kotlin/org/microg/gms/ui/WearFragment.kt | 92 ++++++++++++ .../src/main/res/drawable/ic_watch.xml | 12 ++ .../src/main/res/navigation/nav_settings.xml | 23 +++ .../src/main/res/values/strings.xml | 11 ++ .../src/main/res/xml/preferences_start.xml | 4 + .../src/main/res/xml/preferences_wear.xml | 29 ++++ .../main/res/xml/preferences_wear_config.xml | 61 ++++++++ 11 files changed, 433 insertions(+) create mode 100644 play-services-core/src/main/kotlin/org/microg/gms/ui/WearConfigFragment.kt create mode 100644 play-services-core/src/main/kotlin/org/microg/gms/ui/WearConfigPreference.kt create mode 100644 play-services-core/src/main/kotlin/org/microg/gms/ui/WearFragment.kt create mode 100644 play-services-core/src/main/res/drawable/ic_watch.xml create mode 100644 play-services-core/src/main/res/xml/preferences_wear.xml create mode 100644 play-services-core/src/main/res/xml/preferences_wear_config.xml diff --git a/play-services-core/build.gradle b/play-services-core/build.gradle index 5648333872..683c1bf506 100644 --- a/play-services-core/build.gradle +++ b/play-services-core/build.gradle @@ -68,6 +68,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') diff --git a/play-services-core/src/main/kotlin/org/microg/gms/ui/SettingsFragment.kt b/play-services-core/src/main/kotlin/org/microg/gms/ui/SettingsFragment.kt index 70335535ce..84b28f7292 100644 --- a/play-services-core/src/main/kotlin/org/microg/gms/ui/SettingsFragment.kt +++ b/play-services-core/src/main/kotlin/org/microg/gms/ui/SettingsFragment.kt @@ -54,6 +54,10 @@ class SettingsFragment : ResourceSettingsFragment() { findNavController().navigate(requireContext(), R.id.openWorkProfileSettings) true } + findPreference(PREF_WEAR)!!.onPreferenceClickListener = Preference.OnPreferenceClickListener { + findNavController().navigate(requireContext(), R.id.openWearSettings) + true + } findPreference(PREF_ABOUT)!!.apply { onPreferenceClickListener = Preference.OnPreferenceClickListener { @@ -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" } diff --git a/play-services-core/src/main/kotlin/org/microg/gms/ui/WearConfigFragment.kt b/play-services-core/src/main/kotlin/org/microg/gms/ui/WearConfigFragment.kt new file mode 100644 index 0000000000..aecbbf106d --- /dev/null +++ b/play-services-core/src/main/kotlin/org/microg/gms/ui/WearConfigFragment.kt @@ -0,0 +1,134 @@ +/* + * SPDX-FileCopyrightText: 2026, microG Project Team + * SPDX-License-Identifier: Apache-2.0 + */ + +package org.microg.gms.ui + +import android.annotation.SuppressLint +import android.os.Bundle +import android.util.Log +import androidx.lifecycle.lifecycleScope +import androidx.preference.EditTextPreference +import androidx.preference.Preference +import androidx.preference.PreferenceFragmentCompat +import androidx.preference.SwitchPreferenceCompat +import com.google.android.gms.R +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext +import org.microg.gms.wearable.ConfigurationDatabaseHelper + +class WearConfigFragment : PreferenceFragmentCompat() { + + private lateinit var appHeadingPreference: AppHeadingPreference + private lateinit var configNamePreference: EditTextPreference + private lateinit var configEnabledPreference: SwitchPreferenceCompat + private lateinit var configAddressPreference: EditTextPreference + private lateinit var configPackageNamePreference: EditTextPreference + private lateinit var configDeletePreference: Preference + + private lateinit var database: ConfigurationDatabaseHelper + private val configName: String? + get() = arguments?.getString("name") + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + database = ConfigurationDatabaseHelper(context) + } + + override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) { + addPreferencesFromResource(R.xml.preferences_wear_config) + } + + @SuppressLint("RestrictedApi") + override fun onBindPreferences() { + appHeadingPreference = + preferenceScreen.findPreference("pref_wear_config_heading") ?: appHeadingPreference + configNamePreference = + preferenceScreen.findPreference("pref_wear_config_name") ?: configNamePreference + configEnabledPreference = + preferenceScreen.findPreference("pref_wear_config_enabled") ?: configEnabledPreference + configAddressPreference = + preferenceScreen.findPreference("pref_wear_config_address") ?: configAddressPreference + configPackageNamePreference = + preferenceScreen.findPreference("pref_wear_config_package_name") + ?: configPackageNamePreference + configDeletePreference = preferenceScreen.findPreference("pref_wear_config_delete") + ?: configDeletePreference + + configEnabledPreference.onPreferenceChangeListener = + Preference.OnPreferenceChangeListener { _, newValue -> + val config = database.getConfiguration(configName) + config.enabled = newValue as Boolean + database.putConfiguration(config) + database.close() + true + } + + configAddressPreference.onPreferenceChangeListener = + Preference.OnPreferenceChangeListener { _, newValue -> + val config = database.getConfiguration(configName) + config.address = newValue as String + database.putConfiguration(config) + database.close() + true + } + + configPackageNamePreference.onPreferenceChangeListener = + Preference.OnPreferenceChangeListener { _, newValue -> + Log.d(TAG, "PackageName changed: $newValue") + val config = database.getConfiguration(configName) + config.packageName = newValue as String + database.putConfiguration(config) + database.close() + true + } + + configDeletePreference.onPreferenceClickListener = Preference.OnPreferenceClickListener { + showDeleteConfirm() + true + } + } + + private fun showDeleteConfirm() { + requireContext().buildAlertDialog() + .setTitle(getString(R.string.wear_delete_confirm_title, configName)) + .setPositiveButton(android.R.string.yes) { _, _ -> delete() } + .setNegativeButton(android.R.string.no) { _, _ -> } + .show() + } + + private fun delete() { + lifecycleScope.launchWhenResumed { + withContext(Dispatchers.IO) { + database.deleteConfiguration(configName) + database.close() + // TODO: Leave fragment + } + } + } + + override fun onResume() { + super.onResume() + updateContent() + } + + override fun onPause() { + super.onPause() + database.close() + } + + private fun updateContent() { + lifecycleScope.launchWhenResumed { + configNamePreference.text = configName + val config = + configName?.let { database.getConfiguration(it) } ?: return@launchWhenResumed + appHeadingPreference.packageName = config.packageName + configEnabledPreference.isChecked = config.enabled + configAddressPreference.text = config.address + configPackageNamePreference.text = config.packageName + + database.close() + } + } +} diff --git a/play-services-core/src/main/kotlin/org/microg/gms/ui/WearConfigPreference.kt b/play-services-core/src/main/kotlin/org/microg/gms/ui/WearConfigPreference.kt new file mode 100644 index 0000000000..67ae71f0c0 --- /dev/null +++ b/play-services-core/src/main/kotlin/org/microg/gms/ui/WearConfigPreference.kt @@ -0,0 +1,61 @@ +package org.microg.gms.ui + +import android.content.Context +import android.util.AttributeSet +import android.util.DisplayMetrics +import android.widget.ImageView +import androidx.appcompat.content.res.AppCompatResources +import androidx.preference.Preference +import androidx.preference.PreferenceViewHolder +import com.google.android.gms.wearable.ConnectionConfiguration + +class WearConfigPreference: Preference { + constructor( + context: Context, + attrs: AttributeSet?, + defStyleAttr: Int, + defStyleRes: Int + ) : super(context, attrs, defStyleAttr, defStyleRes) + + constructor(context: Context, attrs: AttributeSet?, defStyleAttr: Int) : super( + context, + attrs, + defStyleAttr + ) + + constructor(context: Context, attrs: AttributeSet?) : super(context, attrs) + constructor(context: Context) : super(context) + + init { + isPersistent = false + } + + private var configField: ConnectionConfiguration? = null + + var connectionConfig: ConnectionConfiguration? + get() = configField + set(value) { + if (value == null && configField != null) { + title = null + icon = null + } else if (value != null) { + val pm = context.packageManager + val applicationInfo = pm.getApplicationInfoIfExists(value.packageName) + + title = value.name + summary = value.packageName + icon = applicationInfo?.loadIcon(pm) ?: AppCompatResources.getDrawable(context, android.R.mipmap.sym_def_app_icon) + } + configField = value + } + + override fun onBindViewHolder(holder: PreferenceViewHolder) { + super.onBindViewHolder(holder) + val icon = holder.findViewById(android.R.id.icon) + if (icon is ImageView) { + icon.adjustViewBounds = true + icon.scaleType = ImageView.ScaleType.CENTER_INSIDE + icon.maxHeight = (32.0 * context.resources.displayMetrics.densityDpi / DisplayMetrics.DENSITY_DEFAULT).toInt() + } + } +} \ No newline at end of file diff --git a/play-services-core/src/main/kotlin/org/microg/gms/ui/WearFragment.kt b/play-services-core/src/main/kotlin/org/microg/gms/ui/WearFragment.kt new file mode 100644 index 0000000000..9bcd95b9ee --- /dev/null +++ b/play-services-core/src/main/kotlin/org/microg/gms/ui/WearFragment.kt @@ -0,0 +1,92 @@ +/* + * SPDX-FileCopyrightText: 2026, microG Project Team + * SPDX-License-Identifier: Apache-2.0 + */ + +package org.microg.gms.ui + +import android.annotation.SuppressLint +import android.os.Bundle +import androidx.core.os.bundleOf +import androidx.lifecycle.lifecycleScope +import androidx.navigation.fragment.findNavController +import androidx.preference.Preference +import androidx.preference.PreferenceCategory +import androidx.preference.PreferenceFragmentCompat +import com.google.android.gms.R +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext +import org.microg.gms.wearable.ConfigurationDatabaseHelper + +class WearFragment : PreferenceFragmentCompat() { + + private lateinit var wearConnections: PreferenceCategory + private lateinit var wearConnectionsAll: Preference + private lateinit var wearConnectionsNone: Preference + private lateinit var database: ConfigurationDatabaseHelper + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + database = ConfigurationDatabaseHelper(context) + } + + override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) { + addPreferencesFromResource(R.xml.preferences_wear) + } + + @SuppressLint("RestrictedApi") + override fun onBindPreferences() { + wearConnections = + preferenceScreen.findPreference("prefcat_wear_connections") ?: wearConnections + wearConnectionsAll = + preferenceScreen.findPreference("pref_wear_connections_all") ?: wearConnectionsAll + wearConnectionsNone = + preferenceScreen.findPreference("pref_wear_connections_none") ?: wearConnectionsNone + } + + override fun onResume() { + super.onResume() + updateContent() + } + + override fun onPause() { + super.onPause() + database.close() + } + + private fun updateContent() { + lifecycleScope.launchWhenResumed { + val context = requireContext() + + val (configs, showAll) = withContext(Dispatchers.IO) { + val configs = database.allConfigurations + val res = configs.take(3).mapIndexed { idx, config -> + val pref = WearConfigPreference(context) + pref.order = idx + pref.connectionConfig = config + pref.onPreferenceClickListener = Preference.OnPreferenceClickListener { + findNavController().navigate(requireContext(), R.id.openWearConfigDetails, bundleOf( + "name" to config.name + )) + true + } + pref.key = "pref_wear_conection_" + config.packageName + pref + }.let { it to (it.size < configs.size) } + database.close() + res + } + + wearConnectionsAll.isVisible = showAll + wearConnections.removeAll() + for (config in configs) { + wearConnections.addPreference(config) + } + if (showAll) { + wearConnections.addPreference(wearConnectionsAll) + } else if (configs.isEmpty()) { + wearConnections.addPreference(wearConnectionsNone) + } + } + } +} diff --git a/play-services-core/src/main/res/drawable/ic_watch.xml b/play-services-core/src/main/res/drawable/ic_watch.xml new file mode 100644 index 0000000000..2a9da57eae --- /dev/null +++ b/play-services-core/src/main/res/drawable/ic_watch.xml @@ -0,0 +1,12 @@ + + + + + diff --git a/play-services-core/src/main/res/navigation/nav_settings.xml b/play-services-core/src/main/res/navigation/nav_settings.xml index 4e202752d1..056f126fda 100644 --- a/play-services-core/src/main/res/navigation/nav_settings.xml +++ b/play-services-core/src/main/res/navigation/nav_settings.xml @@ -29,6 +29,9 @@ + @@ -188,6 +191,26 @@ android:name="org.microg.gms.ui.WorkProfileFragment" android:label="@string/service_name_work_profile" /> + + + + + + + + + + Google SafetyNet Play Store services Work profile + WearOS Google Play Games %1$s would like to use Play Games @@ -314,6 +315,16 @@ Please set up a password, PIN, or pattern lock screen." It is your responsibility to ensure that your usage of microG is in line with corporate policies. microG is provided on a best-effort basis and cannot guarantee to behave exactly as expected. + This is for debugging WearOS connections. + WearOS connections + Name + Enabled + Whether to use this connection configuration or not + Address + Package + Delete + Delete %1$s? + Allow Play Games account registration When playing games, you need to use a Play Games account to log in and record game-related functions, such as achievements, leaderboards, archives, etc. After disabling, Google accounts that are not bound to a Play Games account will not be automatically registered, and the game will not be able to log in and play. Allow uploading of game played diff --git a/play-services-core/src/main/res/xml/preferences_start.xml b/play-services-core/src/main/res/xml/preferences_start.xml index 8d15cd0773..f8c1af0f18 100644 --- a/play-services-core/src/main/res/xml/preferences_start.xml +++ b/play-services-core/src/main/res/xml/preferences_start.xml @@ -67,6 +67,10 @@ android:icon="@drawable/ic_map_marker" android:key="pref_location" android:title="@string/service_name_location"/> + + + + + + + + + + + + diff --git a/play-services-core/src/main/res/xml/preferences_wear_config.xml b/play-services-core/src/main/res/xml/preferences_wear_config.xml new file mode 100644 index 0000000000..991dc47c32 --- /dev/null +++ b/play-services-core/src/main/res/xml/preferences_wear_config.xml @@ -0,0 +1,61 @@ + + + + + + + + + + + + + + + + + + + + + From b828883fde777fcc53344c598f760cb696cf719b Mon Sep 17 00:00:00 2001 From: Teccheck Date: Sun, 1 Mar 2026 17:17:15 +0100 Subject: [PATCH 19/29] Fix channel messages --- .../gms/wearable/WearableConnection.java | 1 - .../org/microg/gms/wearable/WearableImpl.java | 2 + .../gms/wearable/channel/ChannelManager.java | 146 +++++------------- .../channel/OnChannelDataAckTask.java | 4 +- .../core/src/main/proto/wearable.proto | 10 +- 5 files changed, 46 insertions(+), 117 deletions(-) diff --git a/play-services-wearable/core/src/main/java/org/microg/gms/wearable/WearableConnection.java b/play-services-wearable/core/src/main/java/org/microg/gms/wearable/WearableConnection.java index e55ac4e864..615e620da5 100644 --- a/play-services-wearable/core/src/main/java/org/microg/gms/wearable/WearableConnection.java +++ b/play-services-wearable/core/src/main/java/org/microg/gms/wearable/WearableConnection.java @@ -66,7 +66,6 @@ public void writeMessage(RootMessage message) throws IOException { protected RootMessage readMessage() throws IOException { while (true) { - System.out.println("Waiting for new message..."); MessagePiece piece = readMessagePiece(); if (piece.totalPieces == 1) { byte[] payload = piece.data.toByteArray(); diff --git a/play-services-wearable/core/src/main/java/org/microg/gms/wearable/WearableImpl.java b/play-services-wearable/core/src/main/java/org/microg/gms/wearable/WearableImpl.java index 860910d63e..f428f97d07 100644 --- a/play-services-wearable/core/src/main/java/org/microg/gms/wearable/WearableImpl.java +++ b/play-services-wearable/core/src/main/java/org/microg/gms/wearable/WearableImpl.java @@ -987,6 +987,8 @@ public int sendMessage(String packageName, String targetNodeId, String path, byt .sourceNodeId(getLocalNodeId()) .generation(state.generation) .requestId(state.lastRequestId) + .requiresResponse(true) + .unknown5(0) .build()).build()); } catch (IOException e) { Log.w(TAG, "Error while writing, closing link", e); diff --git a/play-services-wearable/core/src/main/java/org/microg/gms/wearable/channel/ChannelManager.java b/play-services-wearable/core/src/main/java/org/microg/gms/wearable/channel/ChannelManager.java index a1ec7d31d2..581bfac681 100644 --- a/play-services-wearable/core/src/main/java/org/microg/gms/wearable/channel/ChannelManager.java +++ b/play-services-wearable/core/src/main/java/org/microg/gms/wearable/channel/ChannelManager.java @@ -43,6 +43,8 @@ public class ChannelManager { public static final int CHANNEL_CONTROL_TYPE_OPEN_ACK = 2; public static final int CHANNEL_CONTROL_TYPE_CLOSE = 3; + public static final int CHANNEL_ORIGIN_CHANNEL_API = 0; + private final Handler handler; private final WearableImpl wearable; private final String localNodeId; @@ -295,6 +297,7 @@ private void doCloseChannel(ChannelStateMachine channel, int errorCode) throws I } public void onChannelRequestReceived(WearableConnection connection, String sourceNodeId, Request request) { + Log.d(TAG, String.format("onChannelRequestReceived (%s): %s", sourceNodeId, request.toString())); if (request.request == null) { Log.w(TAG, "Received channel request with null ChannelRequest"); return; @@ -327,34 +330,8 @@ public void sendOpenRequest(ChannelStateMachine channel) throws IOException { .isReliable(channel.token.isReliable) .build(); - ChannelRequest channelRequest = new ChannelRequest.Builder() - .channelControlRequest(controlRequest) - .version(1) - .origin(0) - .build(); + sendMessage(connection, channel, null, null, controlRequest); - int requestId = requestIdCounter.getAndIncrement(); - int generation = generationCounter.get(); - - Request request = new Request.Builder() - .targetNodeId(channel.token.nodeId) - .sourceNodeId(localNodeId) - .packageName(channel.token.appKey.packageName) - .signatureDigest(channel.token.appKey.signatureDigest) - .path(channel.channelPath) - .request(channelRequest) - .requestId(requestId) - .generation(generation) - .build(); - - RootMessage message = new RootMessage.Builder() - .channelRequest(request) - .build(); - - - Log.d(TAG, "Sending channel open message, message: " + message); - - connection.writeMessage(message); Log.d(TAG, "Sent open channel request for " + channel.token); } @@ -373,27 +350,7 @@ public void sendOpenAck(ChannelStateMachine channel) throws IOException { .path(channel.channelPath) .build(); - ChannelRequest channelRequest = new ChannelRequest.Builder() - .channelControlRequest(ackControl) - .version(1) - .origin(0) - .build(); - - int requestId = requestIdCounter.getAndIncrement(); - - Request request = new Request.Builder() - .requestId(requestId) - .packageName(channel.token.appKey.packageName) - .signatureDigest(channel.token.appKey.signatureDigest) - .targetNodeId(channel.token.nodeId) - .sourceNodeId(localNodeId) - .request(channelRequest) - .generation(generationCounter.get()) - .build(); - - connection.writeMessage(new RootMessage.Builder() - .channelRequest(request) - .build()); + sendMessage(connection, channel, null, null, ackControl); Log.d(TAG, "Sent channel open ACK for " + channel.token); } @@ -414,27 +371,7 @@ public void sendCloseRequest(ChannelStateMachine channel, int errorCode) throws .closeErrorCode(errorCode) .build(); - ChannelRequest channelRequest = new ChannelRequest.Builder() - .channelControlRequest(controlRequest) - .version(1) - .origin(0) - .build(); - - int requestId = requestIdCounter.getAndIncrement(); - - Request request = new Request.Builder() - .requestId(requestId) - .packageName(channel.token.appKey.packageName) - .signatureDigest(channel.token.appKey.signatureDigest) - .targetNodeId(channel.token.nodeId) - .sourceNodeId(localNodeId) - .request(channelRequest) - .generation(generationCounter.get()) - .build(); - - connection.writeMessage(new RootMessage.Builder() - .channelRequest(request) - .build()); + sendMessage(connection, channel, null, null, controlRequest); Log.d(TAG, "Sent close request for " + channel.token); } @@ -450,7 +387,7 @@ public boolean sendData(ChannelStateMachine channel, byte[] data, boolean isFina ChannelDataHeader header = new ChannelDataHeader.Builder() .channelId(channel.token.channelId) .fromChannelOperator(channel.token.thisNodeWasOpener) - .requestId(requestId) + .requestId(0L) .build(); ChannelDataRequest dataRequest = new ChannelDataRequest.Builder() @@ -459,25 +396,7 @@ public boolean sendData(ChannelStateMachine channel, byte[] data, boolean isFina .finalMessage(isFinal) .build(); - ChannelRequest channelRequest = new ChannelRequest.Builder() - .channelDataRequest(dataRequest) - .version(1) - .origin(0) - .build(); - - Request request = new Request.Builder() - .requestId(requestIdCounter.getAndIncrement()) - .targetNodeId(channel.token.nodeId) - .sourceNodeId(localNodeId) - .packageName(channel.token.appKey.packageName) - .signatureDigest(channel.token.appKey.signatureDigest) - .request(channelRequest) - .generation(generationCounter.get()) - .build(); - - connection.writeMessage(new RootMessage.Builder() - .channelRequest(request) - .build()); + sendMessage(connection, channel, dataRequest, null, null); return true; } catch (IOException e) { @@ -497,7 +416,7 @@ public void sendDataAck(ChannelStateMachine channel, long offset, boolean isFina ChannelDataHeader header = new ChannelDataHeader.Builder() .channelId(channel.token.channelId) .fromChannelOperator(channel.token.thisNodeWasOpener) - .requestId(offset) + .requestId(0L) .build(); ChannelDataAckRequest ackRequest = new ChannelDataAckRequest.Builder() @@ -505,31 +424,38 @@ public void sendDataAck(ChannelStateMachine channel, long offset, boolean isFina .finalMessage(isFinal) .build(); - ChannelRequest channelRequest = new ChannelRequest.Builder() - .channelDataAckRequest(ackRequest) - .version(1) - .origin(0) - .build(); - - Request request = new Request.Builder() - .requestId(requestIdCounter.getAndIncrement()) - .targetNodeId(channel.token.nodeId) - .sourceNodeId(localNodeId) - .packageName(channel.token.appKey.packageName) - .signatureDigest(channel.token.appKey.signatureDigest) - .request(channelRequest) - .generation(generationCounter.get()) - .build(); - - connection.writeMessage(new RootMessage.Builder() - .channelRequest(request) - .build()); - + sendMessage(connection, channel, null, ackRequest, null); } catch (IOException e) { Log.e(TAG, "Failed to send data ack", e); } } + private void sendMessage(WearableConnection connection, ChannelStateMachine channel, ChannelDataRequest dataRequest, ChannelDataAckRequest channelDataAckRequest, ChannelControlRequest channelControlRequest) throws IOException { + ChannelRequest channelRequest = new ChannelRequest.Builder() + .channelDataRequest(dataRequest) + .channelDataAckRequest(channelDataAckRequest) + .channelControlRequest(channelControlRequest) + .version(0) + .origin(CHANNEL_ORIGIN_CHANNEL_API) + .build(); + + Request request = new Request.Builder() + .requestId(requestIdCounter.getAndIncrement()) + .targetNodeId(channel.token.nodeId) + .sourceNodeId(localNodeId) + .packageName(channel.token.appKey.packageName) + .signatureDigest(channel.token.appKey.signatureDigest) + .request(channelRequest) + .generation(generationCounter.get()) + .unknown5(0) + .path("") + .build(); + + connection.writeMessage(new RootMessage.Builder() + .channelRequest(request) + .build()); + } + private void onBinderDied(ChannelToken token) { Log.w(TAG, "Client died for channel: " + token); closeChannel(token, ChannelStatusCodes.CLOSE_REASON_LOCAL_CLOSE); diff --git a/play-services-wearable/core/src/main/java/org/microg/gms/wearable/channel/OnChannelDataAckTask.java b/play-services-wearable/core/src/main/java/org/microg/gms/wearable/channel/OnChannelDataAckTask.java index 115d19af64..bbe37fac15 100644 --- a/play-services-wearable/core/src/main/java/org/microg/gms/wearable/channel/OnChannelDataAckTask.java +++ b/play-services-wearable/core/src/main/java/org/microg/gms/wearable/channel/OnChannelDataAckTask.java @@ -31,7 +31,9 @@ protected void execute() throws ChannelException { ChannelStateMachine channel = channelManager.channelTable.get( sourceNodeId, header.channelId, - header.fromChannelOperator + // If we receive and ack, the message does not come from the operator, + // but we have that stored with operator set to true (and vice versa) + !header.fromChannelOperator ); if (channel == null) { diff --git a/play-services-wearable/core/src/main/proto/wearable.proto b/play-services-wearable/core/src/main/proto/wearable.proto index 7c6a74c555..64695a4bba 100644 --- a/play-services-wearable/core/src/main/proto/wearable.proto +++ b/play-services-wearable/core/src/main/proto/wearable.proto @@ -34,7 +34,7 @@ message AssetEntry { message ChannelControlRequest { optional int32 type = 1; - optional int64 channelId = 2; + optional sfixed64 channelId = 2; optional bool fromChannelOperator = 3; optional string packageName = 4; optional string signatureDigest = 5; @@ -49,7 +49,7 @@ message ChannelDataAckRequest { } message ChannelDataHeader { - optional int64 channelId = 1; + optional sfixed64 channelId = 1; optional bool fromChannelOperator = 2; optional int64 requestId = 3; } @@ -109,12 +109,14 @@ message Request { optional string packageName = 2; optional string signatureDigest = 3; optional string targetNodeId = 4; - optional int32 unknown5 = 5; + optional int32 unknown5 = 5; // always has to be 0 optional string path = 6; optional bytes rawData = 7; optional string sourceNodeId = 8; optional ChannelRequest request = 9; optional int32 generation = 10; + optional bool requiresResponse = 13; + optional int32 senderRequestId = 14; } message RootMessage { @@ -160,5 +162,3 @@ message SyncTableEntry { optional string key = 1; optional int64 value = 2; } - - From a724559a563b08e52ffcde4ff72eb1369b0f6fd5 Mon Sep 17 00:00:00 2001 From: deadYokai Date: Sun, 1 Mar 2026 18:52:27 +0200 Subject: [PATCH 20/29] code update --- .../org/microg/gms/wearable/AssetFetcher.java | 321 ++++++++++++++++++ .../wearable/ConfigurationDatabaseHelper.java | 1 + .../microg/gms/wearable/MessageHandler.java | 49 ++- .../org/microg/gms/wearable/WearableImpl.java | 77 +---- .../gms/wearable/WearableServiceImpl.java | 10 +- .../wearable/channel/ChannelAssetApiEnum.java | 24 ++ .../gms/wearable/channel/ChannelManager.java | 251 +++++++++----- .../wearable/channel/ChannelStateMachine.java | 56 +-- .../gms/wearable/channel/ChannelToken.java | 8 +- .../channel/OnChannelControlTask.java | 73 ++-- .../wearable/channel/TrustedPeersService.java | 201 +++++++++++ 11 files changed, 830 insertions(+), 241 deletions(-) create mode 100644 play-services-wearable/core/src/main/java/org/microg/gms/wearable/AssetFetcher.java create mode 100644 play-services-wearable/core/src/main/java/org/microg/gms/wearable/channel/ChannelAssetApiEnum.java create mode 100644 play-services-wearable/core/src/main/java/org/microg/gms/wearable/channel/TrustedPeersService.java diff --git a/play-services-wearable/core/src/main/java/org/microg/gms/wearable/AssetFetcher.java b/play-services-wearable/core/src/main/java/org/microg/gms/wearable/AssetFetcher.java new file mode 100644 index 0000000000..cf8f60af60 --- /dev/null +++ b/play-services-wearable/core/src/main/java/org/microg/gms/wearable/AssetFetcher.java @@ -0,0 +1,321 @@ +package org.microg.gms.wearable; + +import android.database.Cursor; +import android.os.Handler; +import android.util.Log; + +import com.google.android.gms.wearable.Asset; + +import org.microg.gms.wearable.channel.ChannelManager; +import org.microg.gms.wearable.proto.FetchAsset; +import org.microg.gms.wearable.proto.RootMessage; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; + +public class AssetFetcher { + private static final String TAG = "GmsWearAssetFetch"; + + private final NodeDatabaseHelper nodeDatabase; + private final Handler networkHandler; + + private final Set fetchingAssets = Collections.newSetFromMap(new ConcurrentHashMap()); + + private final Map failedAssets = new ConcurrentHashMap<>(); + + private static final int ASSET_BATCH_SIZE = 10; + private static final int MAX_RETRY_COUNT = 3; + private static final long RETRY_COOLDOWN_MS = 5000; // 5 seconds before retry + private static final long FAILED_ASSET_EXPIRY_MS = 300000; // 5 minutes + + public AssetFetcher(NodeDatabaseHelper nodeDatabase, Handler networkHandler) { + this.nodeDatabase = nodeDatabase; + this.networkHandler = networkHandler; + } + + public void fetchMissingAssets(String nodeId, WearableConnection connection, + Map activeConnections, + ChannelManager channelManager) { + if (connection == null) { + Log.d(TAG, "Connection no longer active for node: " + nodeId); + return; + } + + cleanupExpiredFailures(); + + Cursor cursor = nodeDatabase.listMissingAssets(); + if (cursor == null) { + return; + } + + try { + int fetchCount = 0; + int skippedCount = 0; + int alreadyFetchingCount = 0; + + while (cursor.moveToNext()) { + if (!activeConnections.containsKey(nodeId)) { + Log.d(TAG, "Connection closed during asset fetch, stopping (fetched=" + + fetchCount + ", skipped=" + skippedCount + ")"); + break; + } + + String assetDigest = cursor.getString(13); + String assetName = cursor.getString(12); + String packageName = cursor.getString(1); + String signatureDigest = cursor.getString(2); + + if (fetchingAssets.contains(assetDigest)) { + alreadyFetchingCount++; + continue; + } + + AssetFetchAttempt attempt = failedAssets.get(assetDigest); + if (attempt != null) { + if (attempt.retryCount >= MAX_RETRY_COUNT) { + skippedCount++; + continue; + } + + long timeSinceLastAttempt = System.currentTimeMillis() - attempt.lastAttemptTime; + if (timeSinceLastAttempt < RETRY_COOLDOWN_MS) { + skippedCount++; + continue; + } + } + + try { + fetchingAssets.add(assetDigest); + + connection.writeMessage(new RootMessage.Builder() + .fetchAsset(new FetchAsset.Builder() + .assetName(assetName) + .packageName(packageName) + .signatureDigest(signatureDigest) + .build()) + .build()); + + fetchCount++; + + failedAssets.remove(assetDigest); + + if (fetchCount % ASSET_BATCH_SIZE == 0) { + try { + Thread.sleep(100); + } catch (InterruptedException e) { + Log.d(TAG, "Asset fetch interrupted"); + break; + } + } + + } catch (IOException e) { + Log.w(TAG, "Error fetching asset " + assetDigest + + " (fetched " + fetchCount + " so far): " + e.getMessage()); + + recordFailure(assetDigest); + + fetchingAssets.remove(assetDigest); + + if (isConnectionError(e)) { + break; + } + } + } + + if (fetchCount > 0 || skippedCount > 0 || alreadyFetchingCount > 0) { + Log.d(TAG, "Asset fetch summary: fetched=" + fetchCount + + ", skipped=" + skippedCount + + ", alreadyFetching=" + alreadyFetchingCount); + } + + if (fetchCount > 100 && channelManager != null) { + Log.d(TAG, "Large asset batch (" + fetchCount + "), applying cooldown"); + channelManager.setOperationCooldown(1000); + } + + } finally { + cursor.close(); + } + } + + public void fetchMissingAssetsForRecord(WearableConnection connection, + DataItemRecord record, + List missingAssets) { + int successCount = 0; + int skipCount = 0; + + for (Asset asset : missingAssets) { + String digest = asset.getDigest(); + + if (fetchingAssets.contains(digest)) { + skipCount++; + continue; + } + + AssetFetchAttempt attempt = failedAssets.get(digest); + if (attempt != null) { + if (attempt.retryCount >= MAX_RETRY_COUNT) { + Log.d(TAG, "Asset " + digest + " failed too many times, skipping"); + skipCount++; + continue; + } + + long timeSinceLastAttempt = System.currentTimeMillis() - attempt.lastAttemptTime; + if (timeSinceLastAttempt < RETRY_COOLDOWN_MS) { + skipCount++; + continue; + } + } + + try { + Log.d(TAG, "Fetching missing asset for record: " + digest); + + fetchingAssets.add(digest); + + FetchAsset fetchAsset = new FetchAsset.Builder() + .assetName(digest) + .packageName(record.packageName) + .signatureDigest(record.signatureDigest) + .permission(false) + .build(); + + connection.writeMessage(new RootMessage.Builder() + .fetchAsset(fetchAsset) + .build()); + + successCount++; + + failedAssets.remove(digest); + + } catch (IOException e) { + Log.w(TAG, "Error fetching asset " + digest + " for record", e); + + recordFailure(digest); + fetchingAssets.remove(digest); + } + } + + if (successCount > 0 || skipCount > 0) { + Log.d(TAG, "Record asset fetch: success=" + successCount + ", skipped=" + skipCount); + } + } + + public void onAssetReceived(String digest) { + fetchingAssets.remove(digest); + failedAssets.remove(digest); + Log.v(TAG, "Asset received and tracked: " + digest); + } + + public void onAssetFetchFailed(String digest) { + fetchingAssets.remove(digest); + recordFailure(digest); + Log.d(TAG, "Asset fetch failed: " + digest); + } + + private void recordFailure(String digest) { + AssetFetchAttempt attempt = failedAssets.get(digest); + if (attempt == null) { + attempt = new AssetFetchAttempt(digest); + failedAssets.put(digest, attempt); + } + attempt.recordFailure(); + } + + private boolean isConnectionError(IOException e) { + String message = e.getMessage(); + if (message == null) return false; + + return message.contains("Connection") || + message.contains("Broken pipe") || + message.contains("Socket closed") || + message.contains("Connection reset"); + } + + private void cleanupExpiredFailures() { + long now = System.currentTimeMillis(); + List toRemove = new ArrayList<>(); + + for (Map.Entry entry : failedAssets.entrySet()) { + if (now - entry.getValue().firstAttemptTime > FAILED_ASSET_EXPIRY_MS) { + toRemove.add(entry.getKey()); + } + } + + for (String digest : toRemove) { + failedAssets.remove(digest); + } + + if (!toRemove.isEmpty()) { + Log.d(TAG, "Cleaned up " + toRemove.size() + " expired failed asset records"); + } + } + + public AssetFetchStats getStats() { + int failedCount = 0; + int retryingCount = 0; + + for (AssetFetchAttempt attempt : failedAssets.values()) { + if (attempt.retryCount >= MAX_RETRY_COUNT) { + failedCount++; + } else { + retryingCount++; + } + } + + return new AssetFetchStats( + fetchingAssets.size(), + retryingCount, + failedCount + ); + } + + public void resetTracking() { + fetchingAssets.clear(); + failedAssets.clear(); + Log.d(TAG, "Asset fetch tracking reset"); + } + + private static class AssetFetchAttempt { + final String digest; + final long firstAttemptTime; + long lastAttemptTime; + int retryCount; + + AssetFetchAttempt(String digest) { + this.digest = digest; + this.firstAttemptTime = System.currentTimeMillis(); + this.lastAttemptTime = firstAttemptTime; + this.retryCount = 0; + } + + void recordFailure() { + this.lastAttemptTime = System.currentTimeMillis(); + this.retryCount++; + } + } + + public static class AssetFetchStats { + public final int currentlyFetching; + public final int retrying; + public final int failed; + + AssetFetchStats(int currentlyFetching, int retrying, int failed) { + this.currentlyFetching = currentlyFetching; + this.retrying = retrying; + this.failed = failed; + } + + @Override + public String toString() { + return "AssetFetchStats" + + "{fetching="+currentlyFetching+", " + + "retrying="+retrying+", " + + "failed="+failed+"}"; + } + } +} \ No newline at end of file diff --git a/play-services-wearable/core/src/main/java/org/microg/gms/wearable/ConfigurationDatabaseHelper.java b/play-services-wearable/core/src/main/java/org/microg/gms/wearable/ConfigurationDatabaseHelper.java index e4d78579bd..79422c90a5 100644 --- a/play-services-wearable/core/src/main/java/org/microg/gms/wearable/ConfigurationDatabaseHelper.java +++ b/play-services-wearable/core/src/main/java/org/microg/gms/wearable/ConfigurationDatabaseHelper.java @@ -211,6 +211,7 @@ public void putConfiguration(ConnectionConfiguration config, String oldNodeId) { contentValues.put(COLUMN_ROLE, config.role); contentValues.put(COLUMN_CONNECTION_ENABLED, config.enabled ? 1 : 0); contentValues.put(COLUMN_NODE_ID, config.nodeId); + contentValues.put(COLUMN_PACKAGE_NAME, config.packageName); if (oldNodeId == null) { getWritableDatabase().insert(TABLE_NAME, null, contentValues); diff --git a/play-services-wearable/core/src/main/java/org/microg/gms/wearable/MessageHandler.java b/play-services-wearable/core/src/main/java/org/microg/gms/wearable/MessageHandler.java index a31207574b..18a2180a3c 100644 --- a/play-services-wearable/core/src/main/java/org/microg/gms/wearable/MessageHandler.java +++ b/play-services-wearable/core/src/main/java/org/microg/gms/wearable/MessageHandler.java @@ -234,10 +234,19 @@ public void handleMessage(WearableConnection connection, String sourceNodeId, Ro private void handleSetAsset(WearableConnection connection, String sourceNodeId, SetAsset setAsset, Boolean hasAsset) { - Log.d(TAG, "handleSetAsset: digest=" + setAsset.digest + ", hasAsset=" + hasAsset); + Log.d(TAG, "handleSetAsset: digest=" + setAsset.digest + + ", hasAsset=" + hasAsset); - if (setAsset.appkeys != null && setAsset.appkeys.appKeys != null && - !setAsset.appkeys.appKeys.isEmpty()) { + + boolean hasAppKeys = setAsset.appkeys != null && + setAsset.appkeys.appKeys != null && + !setAsset.appkeys.appKeys.isEmpty(); + + if (!hasAppKeys) { + Log.w(TAG, "SetAsset missing AppKeys for digest: " + setAsset.digest); + } + + if (hasAppKeys) { for (AppKey appKey : setAsset.appkeys.appKeys) { wearable.getNodeDatabase().allowAssetAccess( setAsset.digest, @@ -251,16 +260,22 @@ private void handleSetAsset(WearableConnection connection, String sourceNodeId, if (assetExistsLocally) { wearable.getNodeDatabase().markAssetAsPresent(setAsset.digest); + wearable.getAssetFetcher().onAssetReceived(setAsset.digest); Log.d(TAG, "Asset already present locally: " + setAsset.digest); } else { - if (setAsset.appkeys != null && setAsset.appkeys.appKeys != null && - !setAsset.appkeys.appKeys.isEmpty()) { + if (hasAppKeys) { AppKey firstKey = setAsset.appkeys.appKeys.get(0); wearable.getNodeDatabase().markAssetAsMissing( setAsset.digest, firstKey.packageName, firstKey.signatureDigest ); + } else { + wearable.getNodeDatabase().markAssetAsMissing( + setAsset.digest, + "*", + "*" + ); } } } @@ -351,30 +366,9 @@ private void handleSetDataItem(WearableConnection connection, String sourceNodeI } } - - private void fetchMissingAssets(WearableConnection connection, DataItemRecord record, List missingAssets) { - for (Asset asset : missingAssets) { - try { - String digest = asset.getDigest(); - Log.d(TAG, "Fetching missing asset: " + digest); - - FetchAsset fetchAsset = new FetchAsset.Builder() - .assetName(digest) - .packageName(record.packageName) - .signatureDigest(record.signatureDigest) - .permission(false) - .build(); - - connection.writeMessage(new RootMessage.Builder() - .fetchAsset(fetchAsset) - .build()); - - } catch (IOException e) { - Log.w(TAG, "Error fetching asset " + asset.getDigest(), e); - } - } + wearable.getAssetFetcher().fetchMissingAssetsForRecord(connection, record, missingAssets); } @@ -462,6 +456,7 @@ public void handleFilePiece(WearableConnection connection, String fileName, byte synchronized (wearable.getNodeDatabase()) { wearable.getNodeDatabase().markAssetAsPresent(digest); + wearable.getAssetFetcher().onAssetReceived(digest); Cursor cursor = wearable.getNodeDatabase().getDataItemsWaitingForAsset(digest); if (cursor != null) { diff --git a/play-services-wearable/core/src/main/java/org/microg/gms/wearable/WearableImpl.java b/play-services-wearable/core/src/main/java/org/microg/gms/wearable/WearableImpl.java index f428f97d07..68bb4d8e03 100644 --- a/play-services-wearable/core/src/main/java/org/microg/gms/wearable/WearableImpl.java +++ b/play-services-wearable/core/src/main/java/org/microg/gms/wearable/WearableImpl.java @@ -47,9 +47,11 @@ import org.microg.gms.common.RemoteListenerProxy; import org.microg.gms.common.Utils; import org.microg.gms.wearable.bluetooth.BluetoothClient; +import org.microg.gms.wearable.channel.ChannelAssetApiEnum; import org.microg.gms.wearable.channel.ChannelCallbacks; import org.microg.gms.wearable.channel.ChannelManager; import org.microg.gms.wearable.channel.ChannelToken; +import org.microg.gms.wearable.channel.TrustedPeersService; import org.microg.gms.wearable.proto.AppKey; import org.microg.gms.wearable.proto.AppKeys; import org.microg.gms.wearable.proto.Connect; @@ -107,13 +109,15 @@ public class WearableImpl { public static final int ROLE_CLIENT = 1; public static final int ROLE_SERVER = 2; - private ChannelManager channelManager; + private volatile ChannelManager channelManager; private NodeMigrationController migrationController; private static final long ASSET_FETCH_COOLDOWN_MS = 500; private static final int ASSET_BATCH_SIZE = 10; private volatile long lastAssetFetchTime = 0; + private AssetFetcher assetFetcher; + public WearableImpl(Context context, NodeDatabaseHelper nodeDatabase, ConfigurationDatabaseHelper configDatabase) { this.context = context; this.nodeDatabase = nodeDatabase; @@ -130,8 +134,11 @@ public WearableImpl(Context context, NodeDatabaseHelper nodeDatabase, Configurat new Thread(() -> { try { networkHandlerLock.await(); - channelManager = new ChannelManager(networkHandler, this, getLocalNodeId()); - channelManager.setChannelCallbacks(new WearableChannelCallbacks()); + TrustedPeersService trustedPeers = new TrustedPeersService(context); + channelManager = new ChannelManager(networkHandler, this, getLocalNodeId(), trustedPeers); + WearableChannelCallbacks callbacks = new WearableChannelCallbacks(); + channelManager.setChannelCallbacks(callbacks); + channelManager.setCallbacks(ChannelAssetApiEnum.ORIGIN_CHANNEL_API, callbacks); channelManager.start(); } catch (InterruptedException e) { Log.w(TAG, "Failed to initialize ChannelManager", e); @@ -139,7 +146,7 @@ public WearableImpl(Context context, NodeDatabaseHelper nodeDatabase, Configurat }).start(); this.migrationController = new NodeMigrationController(); - + this.assetFetcher = new AssetFetcher(nodeDatabase, networkHandler); } public ChannelManager getChannelManager() { @@ -589,66 +596,13 @@ private void fetchMissingAssets(String nodeId) { private void doFetchMissingAssets(String nodeId) { WearableConnection connection = activeConnections.get(nodeId); - if (connection == null) { - Log.d(TAG, "Connection no longer active for node: " + nodeId); - return; - } - - Cursor cursor = nodeDatabase.listMissingAssets(); - if (cursor != null) { - try { - int fetchCount = 0; - while (cursor.moveToNext()) { - // Check if connection is still active before each write attempt - if (!activeConnections.containsKey(nodeId)) { - Log.d(TAG, "Connection closed during asset fetch, stopping (fetched " - + fetchCount + " assets)"); - break; - } - - String assetName = cursor.getString(12); - String packageName = cursor.getString(1); - String signatureDigest = cursor.getString(2); - try { - connection.writeMessage(new RootMessage.Builder() - .fetchAsset(new FetchAsset.Builder() - .assetName(assetName) - .packageName(packageName) - .signatureDigest(signatureDigest) - .build()) - .build()); - - fetchCount++; - - // Add small delay between requests to avoid overwhelming the connection - if (fetchCount % ASSET_BATCH_SIZE == 0) { - try { - Thread.sleep(100); - } catch (InterruptedException e) { - Log.d(TAG, "Asset fetch interrupted"); - break; - } - } - } catch (IOException e) { - Log.w(TAG, "Error fetching asset (fetched " + fetchCount + " so far): " - + e.getMessage()); - closeConnection(nodeId); - break; // Stop fetching on first error - } - } - - if (fetchCount > 100) { - if (channelManager != null) { - channelManager.setOperationCooldown(1000); - } - } - } finally { - cursor.close(); - } - } + assetFetcher.fetchMissingAssets(nodeId, connection, activeConnections, channelManager); } + public AssetFetcher getAssetFetcher() { + return assetFetcher; + } public void onDisconnectReceived(WearableConnection connection, Connect connect) { for (ConnectionConfiguration config : getConfigurations()) { @@ -988,7 +942,6 @@ public int sendMessage(String packageName, String targetNodeId, String path, byt .generation(state.generation) .requestId(state.lastRequestId) .requiresResponse(true) - .unknown5(0) .build()).build()); } catch (IOException e) { Log.w(TAG, "Error while writing, closing link", e); diff --git a/play-services-wearable/core/src/main/java/org/microg/gms/wearable/WearableServiceImpl.java b/play-services-wearable/core/src/main/java/org/microg/gms/wearable/WearableServiceImpl.java index c90ac6be7b..af6ce6a21b 100644 --- a/play-services-wearable/core/src/main/java/org/microg/gms/wearable/WearableServiceImpl.java +++ b/play-services-wearable/core/src/main/java/org/microg/gms/wearable/WearableServiceImpl.java @@ -24,6 +24,7 @@ import android.database.sqlite.SQLiteDatabase; import android.net.Uri; import android.os.Handler; +import android.os.Looper; import android.os.Parcel; import android.os.ParcelFileDescriptor; import android.os.RemoteException; @@ -68,6 +69,8 @@ public class WearableServiceImpl extends IWearableService.Stub { private final Handler mainHandler; private final CapabilityManager capabilities; + private final Handler handler = new Handler(Looper.getMainLooper()); + public WearableServiceImpl(Context context, WearableImpl wearable, String packageName) { this.context = context; this.wearable = wearable; @@ -80,8 +83,10 @@ private AppKey getAppKey() { return wearable.getAppKey(packageName); } - private ChannelManager getChannelManager() { - return wearable.getChannelManager(); + private ChannelManager getChannelManager() throws RemoteException { + ChannelManager cm = wearable.getChannelManager(); + if (cm == null) throw new RemoteException("ChannelManager not yet initialized"); + return cm; } private void postMain(IWearableCallbacks callbacks, RemoteExceptionRunnable runnable) { @@ -110,7 +115,6 @@ public void run(IWearableCallbacks callbacks) throws RemoteException { @Override public void putConfig(IWearableCallbacks callbacks, final ConnectionConfiguration config) throws RemoteException { config.packageName = this.packageName; - postMain(callbacks, () -> { wearable.createConnection(config); callbacks.onStatus(Status.SUCCESS); diff --git a/play-services-wearable/core/src/main/java/org/microg/gms/wearable/channel/ChannelAssetApiEnum.java b/play-services-wearable/core/src/main/java/org/microg/gms/wearable/channel/ChannelAssetApiEnum.java new file mode 100644 index 0000000000..f79ab55657 --- /dev/null +++ b/play-services-wearable/core/src/main/java/org/microg/gms/wearable/channel/ChannelAssetApiEnum.java @@ -0,0 +1,24 @@ +package org.microg.gms.wearable.channel; + +public enum ChannelAssetApiEnum { + ORIGIN_CHANNEL_API(0), + ORIGIN_LARGE_ASSET_API(1); + + public final int id; + + ChannelAssetApiEnum(int id) { + this.id = id; + } + + public static ChannelAssetApiEnum fromId(int id) { + for (ChannelAssetApiEnum v : values()) { + if (v.id == id) return v; + } + return ORIGIN_CHANNEL_API; + } + + @Override + public String toString() { + return Integer.toString(id); + } +} diff --git a/play-services-wearable/core/src/main/java/org/microg/gms/wearable/channel/ChannelManager.java b/play-services-wearable/core/src/main/java/org/microg/gms/wearable/channel/ChannelManager.java index 581bfac681..75d332ede0 100644 --- a/play-services-wearable/core/src/main/java/org/microg/gms/wearable/channel/ChannelManager.java +++ b/play-services-wearable/core/src/main/java/org/microg/gms/wearable/channel/ChannelManager.java @@ -25,6 +25,7 @@ import java.io.IOException; import java.util.Arrays; +import java.util.EnumMap; import java.util.Random; import java.util.concurrent.BlockingQueue; import java.util.concurrent.LinkedBlockingQueue; @@ -37,7 +38,7 @@ public class ChannelManager { private static final String TAG = "ChannelManager"; private static final int PROCESSING_LOOP_DELAY_MS = 10; - private static final long OPEN_TIMEOUT_MS = 30000; + private static final long OPEN_TIMEOUT_MS = 15000; public static final int CHANNEL_CONTROL_TYPE_OPEN = 1; public static final int CHANNEL_CONTROL_TYPE_OPEN_ACK = 2; @@ -61,6 +62,10 @@ public class ChannelManager { private ChannelCallbacks channelCallbacks; private volatile long cooldownUntil = 0; + private final Object callbacksLock = new Object(); + private final EnumMap callbacksMap = + new EnumMap<>(ChannelAssetApiEnum.class); + public final TrustedPeersService trustedPeers; private final Runnable processingLoop = new Runnable() { @Override @@ -99,12 +104,43 @@ public void run() { } }; - public ChannelManager(Handler handler, WearableImpl wearable, String localNodeId) { + public ChannelManager(Handler handler, WearableImpl wearable, String localNodeId, TrustedPeersService trustedPeers) { this.handler = handler; this.wearable = wearable; this.localNodeId = localNodeId; this.random = new Random(); this.transport = new ChannelTransport(); + this.trustedPeers = trustedPeers; + } + + public void setCallbacks(ChannelAssetApiEnum origin, ChannelCallbacks callbacks) { + synchronized (callbacksLock) { + if (callbacks == null) { + callbacksMap.remove(origin); + } else { + if (callbacksMap.containsKey(origin)) { + throw new IllegalStateException( + "setCallbacks called twice for the same origin: " + origin); + } + callbacksMap.put(origin, callbacks); + } + } + } + + public ChannelCallbacks getCallbacks(ChannelAssetApiEnum origin) { + synchronized (callbacksLock) { + ChannelCallbacks cb = callbacksMap.get(origin); + if (cb == null) { + throw new IllegalStateException("No callbacks set for origin: " + origin); + } + return cb; + } + } + + public ChannelCallbacks getCallbacksOrNull(ChannelAssetApiEnum origin) { + synchronized (callbacksLock) { + return callbacksMap.get(origin); + } } public void setOperationCooldown(long durationMs) { @@ -179,32 +215,47 @@ private void processChannelIO(ChannelStateMachine channel) throws IOException { } } + public void openChannel(AppKey appKey, String nodeId, String path, + boolean isReliable, OpenChannelCallback callback) { + openChannel(ChannelAssetApiEnum.ORIGIN_CHANNEL_API, appKey, nodeId, path, isReliable, callback); + } - public void openChannel(AppKey appKey, String nodeId, String path, boolean isReliable, OpenChannelCallback callback) { + public void openChannel(ChannelAssetApiEnum origin, AppKey appKey, String nodeId, + String path, boolean isReliable, OpenChannelCallback callback) { Log.d(TAG, String.format("openChannel(%s, %s, %s)", appKey.packageName, nodeId, path)); if (!isRunning.get()) { - Log.w(TAG, "openChannel called while not running"); - callback.onResult(ChannelStatusCodes.INTERNAL_ERROR, null, path); + Log.w(TAG, "openChannel called while not running, deferring 500ms"); + handler.postDelayed(() -> { + if (!isRunning.get()) { + Log.e(TAG, "openChannel: still not running after delay, failing"); + callback.onResult(ChannelStatusCodes.INTERNAL_ERROR, null, path); + } else { + openChannel(origin, appKey, nodeId, path, isReliable, callback); + } + }, 500); return; } if (isInCooldown()) { long delay = cooldownUntil - System.currentTimeMillis() + 100; Log.d(TAG, "Deferring channel open by " + delay + "ms due to cooldown"); - handler.postDelayed(() -> openChannel(appKey, nodeId, path, isReliable, callback), delay); + handler.postDelayed( + () -> openChannel(origin, appKey, nodeId, path, isReliable, callback), delay); return; } taskQueue.offer(new ChannelTask(this) { @Override protected void execute() throws IOException, ChannelException { - doOpenChannel(appKey, nodeId, path, isReliable, callback); + doOpenChannel(origin, appKey, nodeId, path, isReliable, callback); } }); } - private void doOpenChannel(AppKey appKey, String nodeId, String path, boolean isReliable, OpenChannelCallback callback) + + private void doOpenChannel(ChannelAssetApiEnum origin, AppKey appKey, String nodeId, + String path, boolean isReliable, OpenChannelCallback callback) throws IOException, ChannelException { WearableConnection connection = wearable.getActiveConnections().get(nodeId); @@ -214,61 +265,55 @@ private void doOpenChannel(AppKey appKey, String nodeId, String path, boolean is return; } - long channelId = generateChannelId(); - ChannelToken token = new ChannelToken(nodeId, appKey, channelId, true, isReliable); + long channelId = generateChannelId(nodeId); + ChannelToken token = new ChannelToken(nodeId, appKey, channelId, true); IBinder.DeathRecipient deathRecipient = () -> onBinderDied(token); + ChannelCallbacks callbacks = getCallbacksOrNull(origin); + ChannelStateMachine channel = new ChannelStateMachine( - token, this, transport, channelCallbacks, true, deathRecipient, handler - ); + token, this, transport, callbacks, origin, isReliable, false, deathRecipient, handler); channel.channelPath = path; channel.openResultDispatcher = callback; channelTable.put(token, channel); channel.openTimeoutOp = new PendingOperation(handler, - () -> onOpenTimeout(token), - OPEN_TIMEOUT_MS, - "Open channel"); + () -> onOpenTimeout(token), OPEN_TIMEOUT_MS, "Open channel"); channel.sendOpenRequest(); } + private void onOpenTimeout(ChannelToken token) { taskQueue.offer(new ChannelTask(this) { @Override protected void execute() throws IOException, ChannelException { - ChannelStateMachine channel = channelTable.get(token); - if (channel == null) { - return; - } - - setChannel(channel); - - if (channel.connectionState == ChannelStateMachine.CONNECTION_STATE_NOT_STARTED) { - Log.w(TAG, "Failed before sending"); - } else if (channel.connectionState == ChannelStateMachine.CONNECTION_STATE_ESTABLISHED) { - Log.d(TAG, "Already opened, ignore timeout"); - return; - } + ChannelStateMachine ch = channelTable.get(token); + if (ch == null) return; + setChannel(ch); - channel.openTimeoutOp = null; + if (ch.connectionState == ChannelStateMachine.CONNECTION_STATE_ESTABLISHED) return; - if (channel.openResultDispatcher == null) { - Log.w(TAG, "Bad state: CONNECTION_STATE_OPEN_SENT but no callbacks"); + ch.openTimeoutOp = null; + if (ch.openResultDispatcher == null) { throw new ChannelException(token, "No callback on timeout"); } - - channel.openResultDispatcher.onResult( - ChannelStatusCodes.CLOSE_REASON_REMOTE_CLOSE, null, channel.channelPath); - channel.openResultDispatcher = null; + ch.openResultDispatcher.onResult( + ChannelStatusCodes.CLOSE_REASON_REMOTE_CLOSE, null, ch.channelPath); + ch.openResultDispatcher = null; } }); } - private long generateChannelId() { - return System.currentTimeMillis() ^ (random.nextLong() & 0xFFFFFFFFL); + private long generateChannelId(String nodeId) { + for (int i = 0; i < 5; i++) { + long id = random.nextLong() & Long.MAX_VALUE; + if (channelTable.get(nodeId, id, true) == null) return id; + } + throw new IllegalStateException( + "Failed to generate a free channel ID. Table size: " + channelTable.size()); } public void closeChannel(ChannelToken token, int errorCode) { @@ -315,33 +360,27 @@ public void onChannelRequestReceived(WearableConnection connection, String sourc } public void sendOpenRequest(ChannelStateMachine channel) throws IOException { - WearableConnection connection = wearable.getActiveConnections().get(channel.token.nodeId); - if (connection == null) { - throw new IOException("No connection to " + channel.token.nodeId); - } + WearableConnection conn = requireConnection(channel.token.nodeId); - ChannelControlRequest controlRequest = new ChannelControlRequest.Builder() + ChannelControlRequest ctrl = new ChannelControlRequest.Builder() .type(CHANNEL_CONTROL_TYPE_OPEN) .channelId(channel.token.channelId) .fromChannelOperator(channel.token.thisNodeWasOpener) .packageName(channel.token.appKey.packageName) .signatureDigest(channel.token.appKey.signatureDigest) .path(channel.channelPath) - .isReliable(channel.token.isReliable) + .isReliable(channel.isReliable) .build(); - sendMessage(connection, channel, null, null, controlRequest); - - Log.d(TAG, "Sent open channel request for " + channel.token); + conn.writeMessage(buildRootMessage(channel, ctrl)); + Log.d(TAG, "Sent open request for " + channel.token); } + public void sendOpenAck(ChannelStateMachine channel) throws IOException { - WearableConnection connection = wearable.getActiveConnections().get(channel.token.nodeId); - if (connection == null) { - throw new IOException("No connection to " + channel.token.nodeId); - } + WearableConnection conn = requireConnection(channel.token.nodeId); - ChannelControlRequest ackControl = new ChannelControlRequest.Builder() + ChannelControlRequest ctrl = new ChannelControlRequest.Builder() .type(CHANNEL_CONTROL_TYPE_OPEN_ACK) .channelId(channel.token.channelId) .fromChannelOperator(channel.token.thisNodeWasOpener) @@ -350,19 +389,18 @@ public void sendOpenAck(ChannelStateMachine channel) throws IOException { .path(channel.channelPath) .build(); - sendMessage(connection, channel, null, null, ackControl); - - Log.d(TAG, "Sent channel open ACK for " + channel.token); + conn.writeMessage(buildRootMessage(channel, ctrl)); + Log.d(TAG, "Sent open ACK for " + channel.token); } public void sendCloseRequest(ChannelStateMachine channel, int errorCode) throws IOException { - WearableConnection connection = wearable.getActiveConnections().get(channel.token.nodeId); - if (connection == null) { - Log.w(TAG, "Cannot send close - connection not found"); + WearableConnection conn = wearable.getActiveConnections().get(channel.token.nodeId); + if (conn == null) { + Log.w(TAG, "Cannot send close — connection not found for " + channel.token.nodeId); return; } - ChannelControlRequest controlRequest = new ChannelControlRequest.Builder() + ChannelControlRequest ctrl = new ChannelControlRequest.Builder() .type(CHANNEL_CONTROL_TYPE_CLOSE) .channelId(channel.token.channelId) .fromChannelOperator(channel.token.thisNodeWasOpener) @@ -371,33 +409,31 @@ public void sendCloseRequest(ChannelStateMachine channel, int errorCode) throws .closeErrorCode(errorCode) .build(); - sendMessage(connection, channel, null, null, controlRequest); - + conn.writeMessage(buildRootMessage(channel, ctrl)); Log.d(TAG, "Sent close request for " + channel.token); } public boolean sendData(ChannelStateMachine channel, byte[] data, boolean isFinal, long requestId) { - WearableConnection connection = wearable.getActiveConnections().get(channel.token.nodeId); - if (connection == null) { - Log.w(TAG, "Cannot send data - connection not found"); + WearableConnection conn = wearable.getActiveConnections().get(channel.token.nodeId); + if (conn == null) { + Log.w(TAG, "Cannot send data — connection not found"); return false; } try { - ChannelDataHeader header = new ChannelDataHeader.Builder() + ChannelDataHeader hdr = new ChannelDataHeader.Builder() .channelId(channel.token.channelId) .fromChannelOperator(channel.token.thisNodeWasOpener) .requestId(0L) .build(); - ChannelDataRequest dataRequest = new ChannelDataRequest.Builder() - .header(header) + ChannelDataRequest dataReq = new ChannelDataRequest.Builder() + .header(hdr) .payload(ByteString.of(data)) .finalMessage(isFinal) .build(); - sendMessage(connection, channel, dataRequest, null, null); - + conn.writeMessage(buildDataRootMessage(channel, dataReq)); return true; } catch (IOException e) { Log.e(TAG, "Failed to send channel data", e); @@ -406,54 +442,87 @@ public boolean sendData(ChannelStateMachine channel, byte[] data, boolean isFina } public void sendDataAck(ChannelStateMachine channel, long offset, boolean isFinal) { - WearableConnection connection = wearable.getActiveConnections().get(channel.token.nodeId); - if (connection == null) { - Log.w(TAG, "Cannot send ack - connection not found"); + WearableConnection conn = wearable.getActiveConnections().get(channel.token.nodeId); + if (conn == null) { + Log.w(TAG, "Cannot send ack — connection not found"); return; } try { - ChannelDataHeader header = new ChannelDataHeader.Builder() + ChannelDataHeader hdr = new ChannelDataHeader.Builder() .channelId(channel.token.channelId) .fromChannelOperator(channel.token.thisNodeWasOpener) .requestId(0L) .build(); - ChannelDataAckRequest ackRequest = new ChannelDataAckRequest.Builder() - .header(header) + ChannelDataAckRequest ack = new ChannelDataAckRequest.Builder() + .header(hdr) .finalMessage(isFinal) .build(); - sendMessage(connection, channel, null, ackRequest, null); + conn.writeMessage(buildAckRootMessage(channel, ack)); } catch (IOException e) { Log.e(TAG, "Failed to send data ack", e); } } - private void sendMessage(WearableConnection connection, ChannelStateMachine channel, ChannelDataRequest dataRequest, ChannelDataAckRequest channelDataAckRequest, ChannelControlRequest channelControlRequest) throws IOException { - ChannelRequest channelRequest = new ChannelRequest.Builder() - .channelDataRequest(dataRequest) - .channelDataAckRequest(channelDataAckRequest) - .channelControlRequest(channelControlRequest) + private WearableConnection requireConnection(String nodeId) throws IOException { + WearableConnection conn = wearable.getActiveConnections().get(nodeId); + if (conn == null) throw new IOException("No connection to " + nodeId); + return conn; + } + + + private RootMessage buildRootMessage(ChannelStateMachine ch, ChannelControlRequest ctrl) { + ChannelRequest cr = new ChannelRequest.Builder() + .channelControlRequest(ctrl) .version(0) - .origin(CHANNEL_ORIGIN_CHANNEL_API) + .origin(0) .build(); - Request request = new Request.Builder() + Request req = new Request.Builder() .requestId(requestIdCounter.getAndIncrement()) - .targetNodeId(channel.token.nodeId) + .targetNodeId(ch.token.nodeId) .sourceNodeId(localNodeId) - .packageName(channel.token.appKey.packageName) - .signatureDigest(channel.token.appKey.signatureDigest) - .request(channelRequest) - .generation(generationCounter.get()) + .packageName(ch.token.appKey.packageName) + .signatureDigest(ch.token.appKey.signatureDigest) + .path(ch.channelPath) + .request(cr) .unknown5(0) - .path("") + .generation(generationCounter.get()) .build(); - connection.writeMessage(new RootMessage.Builder() - .channelRequest(request) - .build()); + return new RootMessage.Builder().channelRequest(req).build(); + } + + private RootMessage buildDataRootMessage(ChannelStateMachine ch, ChannelDataRequest dataReq) { + ChannelRequest cr = new ChannelRequest.Builder() + .channelDataRequest(dataReq).version(1).origin(0).build(); + Request req = new Request.Builder() + .requestId(requestIdCounter.getAndIncrement()) + .targetNodeId(ch.token.nodeId) + .sourceNodeId(localNodeId) + .packageName(ch.token.appKey.packageName) + .signatureDigest(ch.token.appKey.signatureDigest) + .request(cr) + .generation(generationCounter.get()) + .build(); + return new RootMessage.Builder().channelRequest(req).build(); + } + + private RootMessage buildAckRootMessage(ChannelStateMachine ch, ChannelDataAckRequest ack) { + ChannelRequest cr = new ChannelRequest.Builder() + .channelDataAckRequest(ack).version(1).origin(0).build(); + Request req = new Request.Builder() + .requestId(requestIdCounter.getAndIncrement()) + .targetNodeId(ch.token.nodeId) + .sourceNodeId(localNodeId) + .packageName(ch.token.appKey.packageName) + .signatureDigest(ch.token.appKey.signatureDigest) + .request(cr) + .generation(generationCounter.get()) + .build(); + return new RootMessage.Builder().channelRequest(req).build(); } private void onBinderDied(ChannelToken token) { diff --git a/play-services-wearable/core/src/main/java/org/microg/gms/wearable/channel/ChannelStateMachine.java b/play-services-wearable/core/src/main/java/org/microg/gms/wearable/channel/ChannelStateMachine.java index c140b88d09..84574dc028 100644 --- a/play-services-wearable/core/src/main/java/org/microg/gms/wearable/channel/ChannelStateMachine.java +++ b/play-services-wearable/core/src/main/java/org/microg/gms/wearable/channel/ChannelStateMachine.java @@ -11,6 +11,7 @@ import java.io.IOException; import java.nio.ByteBuffer; +import java.util.Objects; import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicLong; @@ -82,11 +83,17 @@ public class ChannelStateMachine { public long totalBytesSent = 0; public long totalBytesReceived = 0; + public final ChannelAssetApiEnum apiOrigin; + + public final boolean isReliable; + public ChannelStateMachine( ChannelToken token, ChannelManager channelManager, ChannelTransport transport, ChannelCallbacks callbacks, + ChannelAssetApiEnum apiOrigin, + boolean isReliable, boolean isLocalOpener, IBinder.DeathRecipient deathRecipient, Handler handler) { @@ -98,7 +105,14 @@ public ChannelStateMachine( this.isLocalOpener = isLocalOpener; this.deathRecipient = deathRecipient; this.handler = handler; + this.apiOrigin = Objects.requireNonNull(apiOrigin, "apiOrigin"); this.creationTime = System.currentTimeMillis(); + this.isReliable = isReliable; + } + + private ChannelCallbacks resolveCallbacks() { + if (callbacks != null) return callbacks; + return channelManager.getCallbacksOrNull(apiOrigin); } public boolean hasInputStream() { @@ -138,18 +152,15 @@ public void setConnectionState(int newState) { } if (!isValidConnectionStateTransition(connectionState, newState)) { - Log.e(TAG, String.format("Channel(%s): Invalid state transition %s -> %s", + Log.w(TAG, String.format("Channel(%s): Unexpected state transition %s -> %s (allowing)", + token, getConnectionStateString(connectionState), + getConnectionStateString(newState))); + } else { + Log.v(TAG, String.format("Channel(%s): %s -> %s", token, getConnectionStateString(connectionState), getConnectionStateString(newState))); - throw new IllegalStateException("Invalid channel state transition: " + - getConnectionStateString(connectionState) + " -> " + - getConnectionStateString(newState)); } - Log.v(TAG, String.format("Channel(%s): %s -> %s", - token, getConnectionStateString(connectionState), - getConnectionStateString(newState))); - this.connectionState = newState; } } @@ -288,7 +299,7 @@ public void processOutgoingData() throws IOException { setSendingState(SENDING_STATE_WAITING_FOR_ACK); sendPendingOp = new PendingOperation(handler, - () -> onSendTimeout(), 30000, "Send data chunk"); + this::onSendTimeout, 30000, "Send data chunk"); } else { Log.e(TAG, "Failed to send data chunk"); onChannelOutputClosed(ChannelStatusCodes.INTERNAL_ERROR, 0); @@ -573,41 +584,36 @@ public void setOutputStream(ParcelFileDescriptor fd, IChannelStreamCallbacks cal } public void forceClose() throws IOException { - if (connectionState == CONNECTION_STATE_CLOSED) return; - + synchronized (stateLock) { + if (connectionState == CONNECTION_STATE_CLOSED) return; + this.connectionState = CONNECTION_STATE_CLOSED; + } if (openTimeoutOp != null) { openTimeoutOp.cancel(); openTimeoutOp = null; } - if (sendPendingOp != null) { sendPendingOp.cancel(); sendPendingOp = null; } - if (openResultDispatcher != null) { try { openResultDispatcher.onResult(ChannelStatusCodes.CHANNEL_CLOSED, null, channelPath); } catch (Exception e) { - Log.e(TAG, "Error in open result callback", e); + Log.e(TAG, "Error in open result callback during forceClose", e); } finally { openResultDispatcher = null; } } - try { onChannelInputClosed(ChannelStatusCodes.CLOSE_REASON_LOCAL_CLOSE, 0); } catch (Exception e) { - Log.w(TAG, "Error closing input", e); + Log.w(TAG, "Error closing input during forceClose", e); } - onChannelOutputClosed(ChannelStatusCodes.CLOSE_REASON_LOCAL_CLOSE, 0); - receiveBuffer = null; sendBuffer = null; - - setConnectionState(CONNECTION_STATE_CLOSED); } public void onChannelInputClosed(int closeReason, int errorCode) throws IOException { @@ -636,9 +642,10 @@ public void onChannelInputClosed(int closeReason, int errorCode) throws IOExcept inputCallbacks = null; setReceivingState(RECEIVING_STATE_CLOSED); - if (callbacks != null) { + ChannelCallbacks cb = resolveCallbacks(); + if (cb != null) { try { - callbacks.onChannelInputClosed(token, channelPath, closeReason, errorCode); + cb.onChannelInputClosed(token, channelPath, closeReason, errorCode); } catch (Exception e) { Log.e(TAG, "Error in input closed callback", e); } @@ -672,8 +679,9 @@ public void onChannelOutputClosed(int closeReason, int errorCode) throws IOExcep sendBuffer = null; setSendingState(SENDING_STATE_CLOSED); - if (callbacks != null) { - callbacks.onChannelOutputClosed(token, channelPath, closeReason, errorCode); + ChannelCallbacks cb = resolveCallbacks(); + if (cb != null) { + cb.onChannelOutputClosed(token, channelPath, closeReason, errorCode); } } diff --git a/play-services-wearable/core/src/main/java/org/microg/gms/wearable/channel/ChannelToken.java b/play-services-wearable/core/src/main/java/org/microg/gms/wearable/channel/ChannelToken.java index c0150a05d9..1be51bdef4 100644 --- a/play-services-wearable/core/src/main/java/org/microg/gms/wearable/channel/ChannelToken.java +++ b/play-services-wearable/core/src/main/java/org/microg/gms/wearable/channel/ChannelToken.java @@ -14,10 +14,9 @@ public final class ChannelToken { public final AppKey appKey; public final long channelId; public final boolean thisNodeWasOpener; - public final boolean isReliable; public ChannelToken(String nodeId, AppKey appKey, long channelId, - boolean thisNodeWasOpener, boolean isReliable) { + boolean thisNodeWasOpener) { if (nodeId == null) throw new NullPointerException("nodeId is null"); if (appKey == null) throw new NullPointerException("appKey is null"); if (channelId < 0) throw new IllegalArgumentException("Negative channelId: " + channelId); @@ -26,7 +25,6 @@ public ChannelToken(String nodeId, AppKey appKey, long channelId, this.appKey = appKey; this.channelId = channelId; this.thisNodeWasOpener = thisNodeWasOpener; - this.isReliable = isReliable; } public static ChannelToken fromString(AppKey expectedAppKey, String tokenString) @@ -54,8 +52,7 @@ public static ChannelToken fromString(AppKey expectedAppKey, String tokenString) proto.nodeId, tokenAppKey, proto.channelId, - proto.thisNodeWasOpener, - proto.isReliable + proto.thisNodeWasOpener ); } catch (InvalidChannelTokenException e) { throw e; @@ -71,7 +68,6 @@ public String toTokenString() { proto.signatureDigest = appKey.signatureDigest; proto.channelId = channelId; proto.thisNodeWasOpener = thisNodeWasOpener; - proto.isReliable = isReliable; return TOKEN_PREFIX + Base64.encodeToString(proto.toByteArray(), Base64.NO_WRAP); } diff --git a/play-services-wearable/core/src/main/java/org/microg/gms/wearable/channel/OnChannelControlTask.java b/play-services-wearable/core/src/main/java/org/microg/gms/wearable/channel/OnChannelControlTask.java index ef808020ef..8a3cb7303d 100644 --- a/play-services-wearable/core/src/main/java/org/microg/gms/wearable/channel/OnChannelControlTask.java +++ b/play-services-wearable/core/src/main/java/org/microg/gms/wearable/channel/OnChannelControlTask.java @@ -13,6 +13,9 @@ public class OnChannelControlTask extends ChannelTask { private static final String TAG = "OnChannelControlTask"; + private static final ChannelAssetApiEnum DEFAULT_INBOUND_ORIGIN = + ChannelAssetApiEnum.ORIGIN_CHANNEL_API; + private final String sourceNodeId; private final WearableConnection connection; private final Request request; @@ -78,11 +81,16 @@ private void handleChannelOpen(ChannelControlRequest control) throws IOException throw new ChannelException(null, "Missing path"); } - AppKey appKey = new AppKey(control.packageName, control.signatureDigest); + AppKey rawKey = new AppKey(control.packageName, control.signatureDigest); + AppKey resolvedKey = channelManager.trustedPeers.resolveAppKey(sourceNodeId, rawKey); + if (!resolvedKey.equals(rawKey)) { + Log.d(TAG, "Trusted-peer resolution: " + rawKey.packageName + + " → " + resolvedKey.packageName + " for node " + sourceNodeId); + } boolean isReliable = control.isReliable != null ? control.isReliable : true; - ChannelToken token = new ChannelToken(sourceNodeId, appKey, control.channelId, false, isReliable); + ChannelToken token = new ChannelToken(sourceNodeId, resolvedKey, control.channelId, false); ChannelStateMachine channel = channelManager.channelTable.get(token); @@ -91,24 +99,27 @@ private void handleChannelOpen(ChannelControlRequest control) throws IOException return; } - if (!checkChannelLimits(sourceNodeId, appKey)) { - Log.w(TAG, "Channel limit reached for " + sourceNodeId + "/" + appKey.packageName); + if (!checkChannelLimits(sourceNodeId, resolvedKey)) { + Log.w(TAG, "Channel limit reached for " + sourceNodeId + "/" + resolvedKey.packageName); sendOpenError(token, control.path, ChannelStatusCodes.CHANNEL_LIMIT_REACHED); return; } - IBinder.DeathRecipient deathRecipient = () -> onChannelBinderDied(token); + ChannelAssetApiEnum origin = inferOrigin(control); + ChannelCallbacks callbacks = channelManager.getCallbacksOrNull(origin); - ChannelCallbacks callbacks = channelManager.getChannelCallbacks(); + IBinder.DeathRecipient deathRecipient = () -> onChannelBinderDied(token); channel = new ChannelStateMachine( token, channelManager, - channelManager.getTransport(), // FIX: Use shared transport + channelManager.getTransport(), callbacks, + origin, + isReliable, false, deathRecipient, - channelManager.getHandler() // FIX: Add getter for handler + channelManager.getHandler() ); channel.channelPath = control.path; @@ -161,35 +172,27 @@ private void handleDuplicateChannelOpen(ChannelStateMachine existingChannel, } private boolean checkChannelLimits(String nodeId, AppKey appKey) { - int channelsForNode = 0; - int channelsForApp = 0; - - for (ChannelStateMachine channel : channelManager.channelTable.values()) { - if (channel.token.nodeId.equals(nodeId)) { - channelsForNode++; - - if (channel.token.appKey.equals(appKey)) { - channelsForApp++; - } + final int MAX_PER_NODE = 20; + final int MAX_PER_APP = 10; + int forNode = 0, forApp = 0; + for (ChannelStateMachine ch : channelManager.channelTable.values()) { + if (ch.token.nodeId.equals(nodeId)) { + forNode++; + if (ch.token.appKey.equals(appKey)) forApp++; } } - - final int MAX_CHANNELS_PER_NODE = 20; - final int MAX_CHANNELS_PER_APP = 10; - - if (channelsForNode >= MAX_CHANNELS_PER_NODE) { - Log.w(TAG, "Node " + nodeId + " has reached channel limit: " + channelsForNode); + if (forNode >= MAX_PER_NODE) { + Log.w(TAG, "Node " + nodeId + " hit channel limit: " + forNode); return false; } - - if (channelsForApp >= MAX_CHANNELS_PER_APP) { - Log.w(TAG, "App " + appKey.packageName + " has reached channel limit: " + channelsForApp); + if (forApp >= MAX_PER_APP) { + Log.w(TAG, "App " + appKey.packageName + " hit channel limit: " + forApp); return false; } - return true; } + private void sendOpenError(ChannelToken token, String path, int errorCode) { try { ChannelStateMachine tempChannel = new ChannelStateMachine( @@ -197,6 +200,8 @@ private void sendOpenError(ChannelToken token, String path, int errorCode) { channelManager, channelManager.getTransport(), null, + ChannelAssetApiEnum.ORIGIN_CHANNEL_API, + false, false, null, channelManager.getHandler() @@ -330,4 +335,16 @@ private void handleChannelClose(ChannelControlRequest control) throws IOExceptio channelManager.channelTable.remove(channel.token); } } + + private ChannelAssetApiEnum inferOrigin(ChannelControlRequest ctrl) { + if (channelManager.getCallbacksOrNull(ChannelAssetApiEnum.ORIGIN_LARGE_ASSET_API) != null) { + String path = ctrl.path != null ? ctrl.path : ""; + if (path.startsWith("/asset/") || path.startsWith("/largeAsset/")) { + return ChannelAssetApiEnum.ORIGIN_LARGE_ASSET_API; + } + } + return DEFAULT_INBOUND_ORIGIN; + } + + } \ No newline at end of file diff --git a/play-services-wearable/core/src/main/java/org/microg/gms/wearable/channel/TrustedPeersService.java b/play-services-wearable/core/src/main/java/org/microg/gms/wearable/channel/TrustedPeersService.java new file mode 100644 index 0000000000..af89135f2d --- /dev/null +++ b/play-services-wearable/core/src/main/java/org/microg/gms/wearable/channel/TrustedPeersService.java @@ -0,0 +1,201 @@ +package org.microg.gms.wearable.channel; + +import android.content.Context; +import android.content.pm.ApplicationInfo; +import android.content.pm.PackageInfo; +import android.content.pm.PackageManager; +import android.util.Log; + +import org.microg.gms.wearable.proto.AppKey; + +import java.util.Collections; +import java.util.HashSet; +import java.util.Iterator; +import java.util.Locale; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; + +import okio.ByteString; + +public class TrustedPeersService { + private static final String TAG = "TrustedPeersService"; + private static final String META_TRUSTED_PACKAGES = "wear-trusted-peer-packages"; + + private final Context context; + + private final ConcurrentHashMap> localTrustMap = new ConcurrentHashMap<>(); + private final ConcurrentHashMap>> remotePeerMaps = new ConcurrentHashMap<>(); + + private final ConcurrentHashMap> resolvedPairs = new ConcurrentHashMap<>(); + + public TrustedPeersService(Context context) { + this.context = context.getApplicationContext(); + } + public AppKey resolveAppKey(String nodeId, AppKey appKey) { + Map pairs = resolvedPairs.get(nodeId); + if (pairs != null) { + for (Map.Entry entry : pairs.entrySet()) { + if (appKey.equals(entry.getValue())) { + Log.d(TAG, "Resolved trusted peer " + appKey.packageName + + " → " + entry.getKey().packageName + " for node " + nodeId); + return entry.getKey(); + } + } + } + return appKey; + } + + public Set getTrustedRemoteKeysFor(AppKey localAppKey) { + Set set = localTrustMap.get(localAppKey); + return set != null ? Collections.unmodifiableSet(set) : Collections.emptySet(); + } + + public void onPackageUpdated(String packageName) { + try { + PackageInfo pi = context.getPackageManager() + .getPackageInfo(packageName, PackageManager.GET_META_DATA | PackageManager.GET_SIGNATURES); + AppKey localKey = buildAppKey(pi); + if (localKey == null) return; + + ApplicationInfo ai = pi.applicationInfo; + if (ai == null || ai.metaData == null || !ai.metaData.containsKey(META_TRUSTED_PACKAGES)) { + localTrustMap.remove(localKey); + recalculateAllNodes(); + return; + } + + String raw = ai.metaData.getString(META_TRUSTED_PACKAGES); + if (raw == null || raw.isEmpty()) { + localTrustMap.remove(localKey); + recalculateAllNodes(); + return; + } + + Set trusted = parseTrustedPackages(raw); + if (trusted.isEmpty()) { + localTrustMap.remove(localKey); + } else { + localTrustMap.put(localKey, trusted); + Log.d(TAG, "Loaded " + trusted.size() + " trusted peers for " + packageName); + } + recalculateAllNodes(); + + } catch (PackageManager.NameNotFoundException e) { + Log.d(TAG, "Package not found during trust scan: " + packageName); + Iterator it = localTrustMap.keySet().iterator(); + while (it.hasNext()) { + AppKey key = it.next(); + if (key.packageName.equals(packageName)) { + it.remove(); + } + } + recalculateAllNodes(); + } + } + + public void onPackageRemoved(String packageName) { + boolean changed = false; + + Iterator>> it = localTrustMap.entrySet().iterator(); + while (it.hasNext()) { + Map.Entry> entry = it.next(); + if (entry.getKey().packageName.equals(packageName)) { + it.remove(); + changed = true; + } + } + + if (changed) recalculateAllNodes(); + } + + public void updateRemotePeerMap(String nodeId, Map> remoteMap) { + if (remoteMap == null || remoteMap.isEmpty()) { + remotePeerMaps.remove(nodeId); + } else { + remotePeerMaps.put(nodeId, remoteMap); + } + recalculateNode(nodeId); + } + + public void onNodeDisconnected(String nodeId) { + remotePeerMaps.remove(nodeId); + resolvedPairs.remove(nodeId); + } + + private void recalculateAllNodes() { + for (String nodeId : remotePeerMaps.keySet()) { + recalculateNode(nodeId); + } + resolvedPairs.keySet().retainAll(remotePeerMaps.keySet()); + } + + private void recalculateNode(String nodeId) { + Map> remoteMap = remotePeerMaps.get(nodeId); + if (remoteMap == null || remoteMap.isEmpty()) { + resolvedPairs.remove(nodeId); + return; + } + + ConcurrentHashMap resolved = new ConcurrentHashMap<>(); + + for (Map.Entry> localEntry : localTrustMap.entrySet()) { + AppKey localKey = localEntry.getKey(); + for (AppKey remoteCandidate : localEntry.getValue()) { + Set remotelyTrusted = remoteMap.get(remoteCandidate); + if (remotelyTrusted != null && remotelyTrusted.contains(localKey)) { + resolved.put(localKey, remoteCandidate); + Log.d(TAG, "Resolved bidirectional trust: " + + localKey.packageName + " ↔ " + remoteCandidate.packageName + + " on node " + nodeId); + break; + } + } + } + + if (resolved.isEmpty()) { + resolvedPairs.remove(nodeId); + } else { + resolvedPairs.put(nodeId, resolved); + } + } + + private static Set parseTrustedPackages(String raw) { + Set result = new HashSet<>(); + for (String entry : raw.split(",")) { + entry = entry.trim(); + int colon = entry.lastIndexOf(':'); + if (colon < 1 || colon == entry.length() - 1) { + Log.w(TAG, "Skipping malformed trusted-package entry: " + entry); + continue; + } + String pkg = entry.substring(0, colon).trim(); + String digest = entry.substring(colon + 1).trim().toLowerCase(Locale.ROOT); + if (!pkg.isEmpty() && !digest.isEmpty()) { + result.add(new AppKey(pkg, digest)); + } + } + return result; + } + + private static AppKey buildAppKey(PackageInfo pi) { + if (pi.signatures == null || pi.signatures.length == 0) return null; + try { + java.security.MessageDigest md = java.security.MessageDigest.getInstance("SHA-1"); + byte[] digest = md.digest(pi.signatures[0].toByteArray()); + return new AppKey(pi.packageName, bytesToHex(digest)); + } catch (java.security.NoSuchAlgorithmException e) { + Log.e(TAG, "SHA-1 unavailable", e); + return null; + } + } + + private static String bytesToHex(byte[] bytes) { + StringBuilder sb = new StringBuilder(bytes.length * 2); + for (byte b : bytes) { + sb.append(String.format("%02x", b)); + } + return sb.toString(); + } + +} From 80792b4b4e3be538bc27f1ff373c1d8db821a518 Mon Sep 17 00:00:00 2001 From: deadYokai Date: Sun, 1 Mar 2026 19:54:50 +0200 Subject: [PATCH 21/29] node migration update --- .../wearable/ClockworkNodePreferences.java | 9 +++ .../microg/gms/wearable/MessageHandler.java | 80 ++++++++++++++++--- .../gms/wearable/NodeDatabaseHelper.java | 8 ++ .../gms/wearable/NodeMigrationController.java | 42 +++++++++- .../gms/wearable/NodeMigrationTracker.java | 78 ++++++++++++++++++ .../org/microg/gms/wearable/WearableImpl.java | 71 +++++++++++++++- .../core/src/main/proto/wearable.proto | 2 + 7 files changed, 273 insertions(+), 17 deletions(-) create mode 100644 play-services-wearable/core/src/main/java/org/microg/gms/wearable/NodeMigrationTracker.java diff --git a/play-services-wearable/core/src/main/java/org/microg/gms/wearable/ClockworkNodePreferences.java b/play-services-wearable/core/src/main/java/org/microg/gms/wearable/ClockworkNodePreferences.java index ff182f80e1..b877a4b692 100644 --- a/play-services-wearable/core/src/main/java/org/microg/gms/wearable/ClockworkNodePreferences.java +++ b/play-services-wearable/core/src/main/java/org/microg/gms/wearable/ClockworkNodePreferences.java @@ -27,6 +27,7 @@ public class ClockworkNodePreferences { private static final String CLOCKWORK_NODE_PREFERENCES = "cw_node"; private static final String CLOCKWORK_NODE_PREFERENCE_NODE_ID = "node_id"; private static final String CLOCKWORK_NODE_PREFERENCE_NEXT_SEQ_ID_BLOCK = "nextSeqIdBlock"; + private static final String CLOCKWORK_NODE_PREFERENCE_PEER_NODE_ID = "peer_node_id"; private static final Object lock = new Object(); private static long seqIdBlock; @@ -77,6 +78,14 @@ public long getNextSeqId() { } } + public String getPeerNodeId() { + return context.getSharedPreferences(CLOCKWORK_NODE_PREFERENCES, Context.MODE_PRIVATE).getString(CLOCKWORK_NODE_PREFERENCE_PEER_NODE_ID, null); + } + + public void setPeerNodeId(String peerNodeId) { + context.getSharedPreferences(CLOCKWORK_NODE_PREFERENCES, Context.MODE_PRIVATE).edit().putString(CLOCKWORK_NODE_PREFERENCE_PEER_NODE_ID, peerNodeId).apply(); + } + public void clear() { synchronized (lock) { SharedPreferences preferences = context.getSharedPreferences( diff --git a/play-services-wearable/core/src/main/java/org/microg/gms/wearable/MessageHandler.java b/play-services-wearable/core/src/main/java/org/microg/gms/wearable/MessageHandler.java index 18a2180a3c..6dc10839cc 100644 --- a/play-services-wearable/core/src/main/java/org/microg/gms/wearable/MessageHandler.java +++ b/play-services-wearable/core/src/main/java/org/microg/gms/wearable/MessageHandler.java @@ -17,6 +17,8 @@ package org.microg.gms.wearable; import static org.microg.gms.wearable.WearableConnection.calculateDigest; +import static org.microg.gms.wearable.WearableImpl.ROLE_CLIENT; +import static org.microg.gms.wearable.WearableImpl.ROLE_SERVER; import android.content.Context; import android.content.Intent; @@ -62,21 +64,12 @@ public class MessageHandler extends ServerMessageListener { private final WearableImpl wearable; private final String oldConfigNodeId; private String peerNodeId; + private final ConnectionConfiguration config; - public MessageHandler(Context context, WearableImpl wearable, ConnectionConfiguration config) { - this(wearable, config, Build.MODEL, config.nodeId, SettingsContract.getSettings(context, SettingsContract.CheckIn.INSTANCE.getContentUri(context), new String[]{SettingsContract.CheckIn.ANDROID_ID}, cursor -> cursor.getLong(0))); - } - - private MessageHandler(WearableImpl wearable, ConnectionConfiguration config, String name, String networkId, long androidId) { - super(new Connect.Builder() - .name(name) - .id(wearable.getLocalNodeId()) - .networkId(networkId) - .peerAndroidId(androidId) - .unknown4(3) - .peerVersion(2) - .build()); + public MessageHandler(Context ctx, WearableImpl wearable, ConnectionConfiguration config) { + super(buildConnect(ctx, wearable, config)); this.wearable = wearable; + this.config = config; this.oldConfigNodeId = config.nodeId; } @@ -84,6 +77,37 @@ private MessageHandler(WearableImpl wearable, ConnectionConfiguration config, St public void onConnect(Connect connect) { super.onConnect(connect); peerNodeId = connect.id; + + if (config.migrating) { + if (!Boolean.TRUE.equals(connect.migrating)) { + Log.e(TAG, "Migration state mismatch: local=true, peer=false for node " + + peerNodeId + ". Aborting."); + try { getConnection().close(); } catch (IOException ignored) {} + return; + } + + if (config.role == ROLE_CLIENT) { + String migratingFrom = connect.migratingFromNodeId; + if (TextUtils.isEmpty(migratingFrom)) { + Log.e(TAG, "Attempting to migrate but Connect is missing migratingFromNodeId"); + try { getConnection().close(); } catch (IOException ignored) {} + return; + } + Log.i(TAG, "Starting migration: node=" + peerNodeId + + " migratingFrom=" + migratingFrom); + wearable.startNodeMigration(peerNodeId, migratingFrom); + } + } else if (Boolean.TRUE.equals(connect.migrating)) { + Log.e(TAG, "Migration state mismatch: local=false, peer=true for node " + + peerNodeId + ". Aborting."); + try { getConnection().close(); } catch (IOException ignored) {} + return; + } + + if (config.role == ROLE_SERVER) { + wearable.getClockworkNodePreferences().setPeerNodeId(peerNodeId); + } + wearable.onConnectReceived(getConnection(), oldConfigNodeId, connect); try { getConnection().writeMessage(new RootMessage.Builder().syncStart(new SyncStart.Builder() @@ -504,4 +528,34 @@ public void sendMessageReceived(String packageName, MessageEventParcelable messa intent.setData(Uri.parse("wear://" + wearable.getLocalNodeId() + "/" + messageEvent.getPath())); wearable.invokeListeners(intent, listener -> listener.onMessageReceived(messageEvent)); } + + private static Connect buildConnect(Context ctx, WearableImpl wearable, + ConnectionConfiguration config) { + long androidId = SettingsContract.getSettings(ctx, + SettingsContract.CheckIn.INSTANCE.getContentUri(ctx), + new String[]{SettingsContract.CheckIn.ANDROID_ID}, + c -> c.getLong(0)); + + Connect.Builder b = new Connect.Builder() + .name(Build.MODEL) + .id(wearable.getLocalNodeId()) + .networkId(config.nodeId) + .peerAndroidId(androidId) + .unknown4(3) + .peerVersion(2); + + if (config.migrating && config.role == ROLE_SERVER) { + String prevPeerNodeId = wearable.getClockworkNodePreferences().getPeerNodeId(); + b.migrating(true); + if (prevPeerNodeId != null) { + Log.i(TAG, "Migration handshake: migratingFromNodeId=" + prevPeerNodeId); + b.migratingFromNodeId(prevPeerNodeId); + } else { + Log.w(TAG, "Migration requested but no previous peer nodeId stored"); + } + } + + return b.build(); + } + } diff --git a/play-services-wearable/core/src/main/java/org/microg/gms/wearable/NodeDatabaseHelper.java b/play-services-wearable/core/src/main/java/org/microg/gms/wearable/NodeDatabaseHelper.java index 3c732b6efa..0ea16fce18 100644 --- a/play-services-wearable/core/src/main/java/org/microg/gms/wearable/NodeDatabaseHelper.java +++ b/play-services-wearable/core/src/main/java/org/microg/gms/wearable/NodeDatabaseHelper.java @@ -100,6 +100,11 @@ public void onCreate(SQLiteDatabase db) { "migratingFrom TEXT DEFAULT NULL, " + "enrollmentId TEXT DEFAULT NULL);"); + db.execSQL("CREATE TABLE nodeMigration (" + + "nodeId TEXT NOT NULL PRIMARY KEY, " + + "migratingFromNodeId TEXT NOT NULL, " + + "complete INTEGER NOT NULL DEFAULT 0);"); + db.execSQL("CREATE VIEW appKeyDataItems AS SELECT " + "appkeys._id AS appkeys_id, " + "appkeys.packageName AS packageName, " + @@ -219,6 +224,8 @@ public void onCreate(SQLiteDatabase db) { db.execSQL("CREATE UNIQUE INDEX archiveDataItems_NODE_APPPKEY_PATH ON archiveDataItems(" + "migratingNode, appkeys_id, path);"); + + } public synchronized Cursor getDataItemsForDataHolder(String packageName, String signatureDigest) { @@ -287,6 +294,7 @@ public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) { db.execSQL("DROP TABLE IF EXISTS assetrefs;"); db.execSQL("DROP TABLE IF EXISTS assetsacls;"); db.execSQL("DROP TABLE IF EXISTS nodeinfo;"); + db.execSQL("DROP TABLE IF EXISTS nodeMigration;"); db.execSQL("DROP VIEW IF EXISTS appKeyDataItems;"); db.execSQL("DROP VIEW IF EXISTS appKeyAcls;"); db.execSQL("DROP VIEW IF EXISTS dataItemsAndAssets;"); diff --git a/play-services-wearable/core/src/main/java/org/microg/gms/wearable/NodeMigrationController.java b/play-services-wearable/core/src/main/java/org/microg/gms/wearable/NodeMigrationController.java index 42ca9a6f3b..6e2ea803a7 100644 --- a/play-services-wearable/core/src/main/java/org/microg/gms/wearable/NodeMigrationController.java +++ b/play-services-wearable/core/src/main/java/org/microg/gms/wearable/NodeMigrationController.java @@ -1,14 +1,16 @@ package org.microg.gms.wearable; import android.os.Build; +import android.util.Log; +import java.util.Collections; import java.util.Map; import java.util.Set; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.locks.ReentrantReadWriteLock; public class NodeMigrationController { - + private static final String TAG = "NodeMigrationController"; public final ReentrantReadWriteLock migrationLock = new ReentrantReadWriteLock(); public final ReentrantReadWriteLock archiveLock = new ReentrantReadWriteLock(); @@ -26,12 +28,26 @@ public NodeMigrationController() { } } + public void startMigrationForNode(String nodeId) { + Log.d(TAG, "Starting migration for node " + nodeId); + Set slot = Collections.newSetFromMap(new ConcurrentHashMap<>()); + migrationLock.writeLock().lock(); + try { + Set existing = nodeToCompletedAppsMap.putIfAbsent(nodeId, slot); + if (existing != null) { + Log.d(TAG, "Node " + nodeId + " already migrating with completed apps: " + existing); + } + } finally { + migrationLock.writeLock().unlock(); + } + } + public void markNodeMigrationCompleted(String nodeId) { migrationLock.writeLock().lock(); try { Set completedApps = nodeToCompletedAppsMap.remove(nodeId); if (completedApps != null) { - android.util.Log.d("NodeMigration", "Marking " + nodeId + " as completed with apps: " + completedApps); + Log.d(TAG, "Marking " + nodeId + " as completed with apps: " + completedApps); } } finally { migrationLock.writeLock().unlock(); @@ -42,6 +58,19 @@ public void markNodeMigrationCompleted(String nodeId) { } } + public void markAppMigrationComplete(String nodeId, String packageName) { + migrationLock.writeLock().lock(); + try { + Set completedApps = nodeToCompletedAppsMap.get(nodeId); + if (completedApps != null) { + completedApps.add(packageName); + Log.d(TAG, "App " + packageName + " migration complete for node " + nodeId); + } + } finally { + migrationLock.writeLock().unlock(); + } + } + public void addCompletedNode(String nodeId) { completedNodes.add(nodeId); } @@ -62,4 +91,13 @@ public boolean shouldDeliverEvents(String packageName, String sourceNodeId) { migrationLock.readLock().unlock(); } } + + public boolean isMigrating(String nodeId) { + migrationLock.readLock().lock(); + try { + return nodeToCompletedAppsMap.containsKey(nodeId); + } finally { + migrationLock.readLock().unlock(); + } + } } \ No newline at end of file diff --git a/play-services-wearable/core/src/main/java/org/microg/gms/wearable/NodeMigrationTracker.java b/play-services-wearable/core/src/main/java/org/microg/gms/wearable/NodeMigrationTracker.java new file mode 100644 index 0000000000..3f22c2e627 --- /dev/null +++ b/play-services-wearable/core/src/main/java/org/microg/gms/wearable/NodeMigrationTracker.java @@ -0,0 +1,78 @@ +package org.microg.gms.wearable; + +import android.content.ContentValues; +import android.database.Cursor; +import android.database.sqlite.SQLiteDatabase; +import android.util.Log; + +import java.util.HashMap; +import java.util.Map; + +public class NodeMigrationTracker { + private static final String TAG = "NodeMigrationTracker"; + + static final String TABLE = "nodeMigration"; + static final String COL_NODE_ID = "nodeId"; + static final String COL_MIGRATING_FROM = "migratingFromNodeId"; + static final String COL_COMPLETE = "complete"; + + private final Map migrationMap = new HashMap<>(); + + private final NodeDatabaseHelper db; + + public NodeMigrationTracker(NodeDatabaseHelper db) { + this.db = db; + } + + public void updateMigrationInfo(SQLiteDatabase writable, String newNodeId, String migratingFromNodeId) { + Log.i(TAG, "setNodeMigratingFrom(" + newNodeId + ", " + migratingFromNodeId + ")"); + ContentValues cv = new ContentValues(); + cv.put(COL_NODE_ID, newNodeId); + cv.put(COL_MIGRATING_FROM, migratingFromNodeId); + cv.put(COL_COMPLETE, 0); + writable.insertWithOnConflict(TABLE, null, cv, SQLiteDatabase.CONFLICT_REPLACE); + + if (!migrationMap.containsKey(newNodeId)) { + migrationMap.put(newNodeId, migratingFromNodeId); + } + } + + public String getMigratingFromNodeId(String nodeId) { + if (migrationMap.containsKey(nodeId)) return migrationMap.get(nodeId); + Cursor c = db.getReadableDatabase().query(TABLE, + new String[]{COL_MIGRATING_FROM}, + COL_NODE_ID + "=?", new String[]{nodeId}, + null, null, null); + try { + if (c.moveToFirst()) return c.getString(0); + } finally { + c.close(); + } + return null; + } + + public static boolean isNodeMigrationComplete(SQLiteDatabase db, String nodeId) { + Cursor c = db.query(TABLE, + new String[]{COL_COMPLETE}, + COL_NODE_ID + "=?", new String[]{nodeId}, + null, null, null); + try { + return c.moveToFirst() && c.getInt(0) == 1; + } finally { + c.close(); + } + } + + public void setMigrationComplete(String nodeId) { + Log.i(TAG, "setMigrationComplete(" + nodeId + ")"); + ContentValues cv = new ContentValues(); + cv.put(COL_COMPLETE, 1); + db.getWritableDatabase().update(TABLE, cv, + COL_NODE_ID + "=?", new String[]{nodeId}); + migrationMap.remove(nodeId); + } + + public boolean isMigrating(String nodeId) { + return migrationMap.containsKey(nodeId); + } +} \ No newline at end of file diff --git a/play-services-wearable/core/src/main/java/org/microg/gms/wearable/WearableImpl.java b/play-services-wearable/core/src/main/java/org/microg/gms/wearable/WearableImpl.java index 68bb4d8e03..45d3187764 100644 --- a/play-services-wearable/core/src/main/java/org/microg/gms/wearable/WearableImpl.java +++ b/play-services-wearable/core/src/main/java/org/microg/gms/wearable/WearableImpl.java @@ -111,6 +111,7 @@ public class WearableImpl { private volatile ChannelManager channelManager; private NodeMigrationController migrationController; + private NodeMigrationTracker migrationTracker; private static final long ASSET_FETCH_COOLDOWN_MS = 500; private static final int ASSET_BATCH_SIZE = 10; @@ -124,6 +125,7 @@ public WearableImpl(Context context, NodeDatabaseHelper nodeDatabase, Configurat this.configDatabase = configDatabase; this.clockworkNodePreferences = new ClockworkNodePreferences(context); this.rpcHelper = new RpcHelper(context); + this.migrationTracker = new NodeMigrationTracker(nodeDatabase); new Thread(() -> { Looper.prepare(); networkHandler = new Handler(Looper.myLooper()); @@ -149,6 +151,10 @@ public WearableImpl(Context context, NodeDatabaseHelper nodeDatabase, Configurat this.assetFetcher = new AssetFetcher(nodeDatabase, networkHandler); } + public NodeMigrationTracker getMigrationTracker() { + return migrationTracker; + } + public ChannelManager getChannelManager() { return channelManager; } @@ -246,6 +252,62 @@ public void onChannelOutputClosed(ChannelToken token, String path, int closeReas } + public void startNodeMigration(String newNodeId, String migratingFromId) { + Log.d(TAG, "startNodeMigration: node=" + newNodeId + " from=" + migratingFromId); + try { + android.database.sqlite.SQLiteDatabase db = nodeDatabase.getWritableDatabase(); + migrationTracker.updateMigrationInfo(db, newNodeId, migratingFromId); + } catch (android.database.sqlite.SQLiteException e) { + Log.e(TAG, "DB error while recording node migration state", e); + return; + } + migrationController.startMigrationForNode(newNodeId); + } + + public void terminateAssociation(String nodeId, boolean removeBond, String reason) { + Log.d(TAG, "terminateAssociation: node=" + nodeId + + ", removeBond=" + removeBond + ", reason=" + reason); + + ConnectionConfiguration target = null; + for (ConnectionConfiguration cfg : getConfigurations()) { + if (nodeId.equals(cfg.nodeId) || nodeId.equals(cfg.peerNodeId)) { + target = cfg; + break; + } + } + + if (target == null) { + Log.i(TAG, "terminateAssociation: no config found for node " + nodeId); + return; + } + + WearableConnection conn = activeConnections.get(nodeId); + if (conn != null) { + try { conn.close(); } catch (java.io.IOException ignored) {} + activeConnections.remove(nodeId); + onPeerDisconnected(new com.google.android.gms.wearable.internal.NodeParcelable(nodeId, target.name)); + } + + deleteConnection(target.name); + + if (removeBond && target.address != null) { + try { + android.bluetooth.BluetoothAdapter adapter = android.bluetooth.BluetoothAdapter.getDefaultAdapter(); + if (adapter != null) { + android.bluetooth.BluetoothDevice device = adapter.getRemoteDevice(target.address); + if (device != null) { + java.lang.reflect.Method m = device.getClass().getMethod("removeBond"); + m.invoke(device); + Log.d(TAG, "Removed BT bond for " + target.address); + } + } + } catch (Exception e) { + Log.w(TAG, "Failed to remove BT bond for " + target.address, e); + } + } + } + + public String getLocalNodeId() { return clockworkNodePreferences.getLocalNodeId(); } @@ -280,7 +342,12 @@ public DataItemRecord putDataItem(DataItemRecord record) { Intent intent = new Intent("com.google.android.gms.wearable.DATA_CHANGED"); intent.setPackage(record.packageName); intent.setData(record.dataItem.uri); - invokeListeners(intent, listener -> listener.onDataChanged(record.toEventDataHolder())); + if (migrationController.shouldDeliverEvents(record.packageName, record.source)) { + invokeListeners(intent, listener -> listener.onDataChanged(record.toEventDataHolder())); + } else { + Log.d(TAG, "Suppressing DATA_CHANGED for " + record.packageName + + " from migrating node " + record.source); + } return record; } @@ -908,7 +975,7 @@ private void closeConnection(String nodeId) { } - if (connection == sct.getWearableConnection() && sct != null) { + if (sct != null && connection == sct.getWearableConnection()) { sct.close(); sct = null; } diff --git a/play-services-wearable/core/src/main/proto/wearable.proto b/play-services-wearable/core/src/main/proto/wearable.proto index 64695a4bba..a18592e6ac 100644 --- a/play-services-wearable/core/src/main/proto/wearable.proto +++ b/play-services-wearable/core/src/main/proto/wearable.proto @@ -76,6 +76,8 @@ message Connect { optional int32 peerVersion = 5; optional int32 peerMinimumVersion = 6; optional string networkId = 7; + optional bool migrating = 8; + optional string migratingFromNodeId = 9; } message FetchAsset { From 2d54f91219a3cc7a0c323c7797b2066431f331fc Mon Sep 17 00:00:00 2001 From: Teccheck Date: Wed, 4 Mar 2026 16:47:09 +0100 Subject: [PATCH 22/29] feat(wear): add runtimeType to ConnectionConfiguration (field 20) --- .../android/gms/wearable/ConnectionConfiguration.java | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/play-services-wearable/src/main/java/com/google/android/gms/wearable/ConnectionConfiguration.java b/play-services-wearable/src/main/java/com/google/android/gms/wearable/ConnectionConfiguration.java index fb4fda1076..faa0757b11 100644 --- a/play-services-wearable/src/main/java/com/google/android/gms/wearable/ConnectionConfiguration.java +++ b/play-services-wearable/src/main/java/com/google/android/gms/wearable/ConnectionConfiguration.java @@ -61,16 +61,18 @@ public class ConnectionConfiguration extends AutoSafeParcelable { public ConnectionDelayFilters connectionDelayFilters; @SafeParceled(19) public int maxSupportedRemoteAndroidSdkVersion; + @SafeParceled(20) + public int runtimeType; private ConnectionConfiguration() { } public ConnectionConfiguration(String name, String address, int type, int role, boolean enabled) { - this(name, address, type, role, enabled, false, null, false, null, null, 0, null, false, false, null, false, null, 0); + this(name, address, type, role, enabled, false, null, false, null, null, 0, null, false, false, null, false, null, 0, 0); } public ConnectionConfiguration(String name, String address, int type, int role, boolean enabled, String nodeId, String packageName) { - this(name, address, type, role, enabled, false, null, false, nodeId, packageName, 0, null, false, false, null, false, null, 0); + this(name, address, type, role, enabled, false, null, false, nodeId, packageName, 0, null, false, false, null, false, null, 0, 0); } public ConnectionConfiguration(String name, String address, int type, int role, boolean enabled, @@ -80,7 +82,7 @@ public ConnectionConfiguration(String name, String address, int type, int role, boolean dataItemSyncEnabled, ConnectionRestrictions connectionRestrictions, boolean removeConnectionWhenBondRemovedByUser, ConnectionDelayFilters connectionDelayFilters, - int maxSupportedRemoteAndroidSdkVersion) { + int maxSupportedRemoteAndroidSdkVersion, int runtimeType) { this.name = name; this.address = address; this.type = type; @@ -99,6 +101,7 @@ public ConnectionConfiguration(String name, String address, int type, int role, this.removeConnectionWhenBondRemovedByUser = removeConnectionWhenBondRemovedByUser; this.connectionDelayFilters = connectionDelayFilters; this.maxSupportedRemoteAndroidSdkVersion = maxSupportedRemoteAndroidSdkVersion; + this.runtimeType = runtimeType; } @Override @@ -121,6 +124,7 @@ public String toString() { sb.append(", connectionRestrictions='").append(connectionRestrictions).append('\''); sb.append(", removeConnectionWhenBondRemovedByUser='").append(removeConnectionWhenBondRemovedByUser).append('\''); sb.append(", maxSupportedRemoteAndroidSdkVersion='").append(maxSupportedRemoteAndroidSdkVersion).append('\''); + sb.append(", runtimeType='").append(runtimeType).append('\''); sb.append('}'); return sb.toString(); } From 3b2adf848ae502c17d938e7ed86e4ae5a7e41406 Mon Sep 17 00:00:00 2001 From: Teccheck Date: Wed, 4 Mar 2026 17:11:44 +0100 Subject: [PATCH 23/29] fix(wear): fix Connect message proto --- .../main/java/org/microg/gms/wearable/MessageHandler.java | 6 ++++-- play-services-wearable/core/src/main/proto/wearable.proto | 8 +++++--- 2 files changed, 9 insertions(+), 5 deletions(-) diff --git a/play-services-wearable/core/src/main/java/org/microg/gms/wearable/MessageHandler.java b/play-services-wearable/core/src/main/java/org/microg/gms/wearable/MessageHandler.java index 6dc10839cc..e60afc45dc 100644 --- a/play-services-wearable/core/src/main/java/org/microg/gms/wearable/MessageHandler.java +++ b/play-services-wearable/core/src/main/java/org/microg/gms/wearable/MessageHandler.java @@ -537,12 +537,14 @@ private static Connect buildConnect(Context ctx, WearableImpl wearable, c -> c.getLong(0)); Connect.Builder b = new Connect.Builder() - .name(Build.MODEL) + .name(Build.MODEL) // TODO: Should be hostname, but seems to be irrelevant .id(wearable.getLocalNodeId()) .networkId(config.nodeId) .peerAndroidId(androidId) .unknown4(3) - .peerVersion(2); + .peerVersion(2) + .peerMinimumVersion(0) + .androidSdkVersion(Build.VERSION.SDK_INT); if (config.migrating && config.role == ROLE_SERVER) { String prevPeerNodeId = wearable.getClockworkNodePreferences().getPeerNodeId(); diff --git a/play-services-wearable/core/src/main/proto/wearable.proto b/play-services-wearable/core/src/main/proto/wearable.proto index a18592e6ac..451bc5e1b5 100644 --- a/play-services-wearable/core/src/main/proto/wearable.proto +++ b/play-services-wearable/core/src/main/proto/wearable.proto @@ -72,12 +72,14 @@ message Connect { optional string id = 1; optional string name = 2; optional int64 peerAndroidId = 3; - optional int32 unknown4 = 4; + optional int32 unknown4 = 4; // Always has value 3 optional int32 peerVersion = 5; optional int32 peerMinimumVersion = 6; optional string networkId = 7; - optional bool migrating = 8; - optional string migratingFromNodeId = 9; + optional string packageName = 8; + optional bool migrating = 9; + optional string migratingFromNodeId = 10; + optional int32 androidSdkVersion = 11; } message FetchAsset { From 7dd589e17c2956b933c7a798629d0250c9946079 Mon Sep 17 00:00:00 2001 From: Teccheck Date: Wed, 4 Mar 2026 17:12:18 +0100 Subject: [PATCH 24/29] feat(wear): add missing proto definitions --- .../core/src/main/proto/wearable.proto | 84 +++++++++++++++++++ 1 file changed, 84 insertions(+) diff --git a/play-services-wearable/core/src/main/proto/wearable.proto b/play-services-wearable/core/src/main/proto/wearable.proto index 451bc5e1b5..c5caadf8d9 100644 --- a/play-services-wearable/core/src/main/proto/wearable.proto +++ b/play-services-wearable/core/src/main/proto/wearable.proto @@ -6,6 +6,28 @@ option java_package = "org.microg.gms.wearable.proto"; option java_outer_classname = "WearableProto"; +message AccountMatchingMessage { + enum Type { + //UNRECOGNIZED = -1; + UNSPECIFIED = 0; + GET_ACCOUNTS = 1; + ACCOUNTS_RESPONSE = 2; + REMOVE_ACCOUNTS = 3; + REMOVE_ACCOUNTS_CONFIRM = 4; + CANCEL = 5; + REMOTE_ERROR = 6; + } + + optional int32 type = 1; + repeated AccountMatchingEntry entries = 2; + optional string exceptionMessage = 3; +} + +message AccountMatchingEntry { + optional string nameMaybe = 1; // Not sure here + optional string id = 2; +} + message AckAsset { optional string digest = 1; } @@ -32,7 +54,21 @@ message AssetEntry { optional int32 unknown3 = 3; } +message CapabilityFilters { + repeated CapabilityFilterEntry entries = 1; +} + +message CapabilityFilterEntry { + optional string nameMaybe = 1; +} + message ChannelControlRequest { + enum Type { + CHANNEL_CONTROL_OPEN = 1; + CHANNEL_CONTROL_CLOSE = 2; + CHANNEL_CONTROL_OPEN_ACK = 3; + } + optional int32 type = 1; optional sfixed64 channelId = 2; optional bool fromChannelOperator = 3; @@ -82,6 +118,47 @@ message Connect { optional int32 androidSdkVersion = 11; } +message ConnectionRestrictions { + repeated ConnectionRestrictionEntry entries = 1; +} + +message ConnectionRestrictionEntry { + optional string packageName = 1; + optional DataItemFilters dataItemFilters = 2; + optional CapabilityFilters capabilityFilters = 3; +} + +message ControlMessage { + enum Type { + UNKNOWN = 1; + TERMINATE_ASSOCIATION = 2; + SUSPEND_SYNC = 3; + RESUME_SYNC = 4; + MIGRATION_FAILED = 5; + ACCOUNT_MATCHING = 6; + MIGRATION_CANCELLED = 7; + } + + optional int32 type = 1; + optional AccountMatchingMessage accountMatching = 2; +} + +message DataItemFilters { + repeated DataItemFilterEntry entries = 1; +} + +message DataItemFilterEntry { + optional int32 filterType = 1; + optional string uri = 2; +} + +message EncryptionHandshake { + optional bytes cryptoNegotiationResponse = 1; // Unsure + optional bytes resumeBytes = 2; // Unsure + optional bool unknown3 = 3; + optional int32 unknown4 = 4; +} + message FetchAsset { optional string packageName = 1; optional string assetName = 2; @@ -121,6 +198,7 @@ message Request { optional int32 generation = 10; optional bool requiresResponse = 13; optional int32 senderRequestId = 14; + optional int32 priority = 15; } message RootMessage { @@ -135,6 +213,11 @@ message RootMessage { optional FilePiece filePiece = 12; optional bool hasAsset = 13; optional Request channelRequest = 16; + optional EncryptionHandshake e2eHandshake = 17; + optional Request rpcServiceRequest = 18; + // This is probably not needed? + // optional IosMultiAppAuth iosMultiAppAuth = 19; + optional ControlMessage controlMessage = 20; } message SetAsset { @@ -160,6 +243,7 @@ message SyncStart { optional int64 receivedSeqId = 1; repeated SyncTableEntry syncTable = 2; optional int32 version = 3; + optional ConnectionRestrictions restrictions = 4; } message SyncTableEntry { From 6d3fc68e4afd492db2132b1916c676a52f01f70e Mon Sep 17 00:00:00 2001 From: deadYokai Date: Wed, 4 Mar 2026 17:52:46 +0100 Subject: [PATCH 25/29] feat(wear): add Wearable Reader and Writer --- .../microg/gms/wearable/WearableReader.java | 110 ++++++++++++++++++ .../microg/gms/wearable/WearableWriter.java | 108 +++++++++++++++++ 2 files changed, 218 insertions(+) create mode 100644 play-services-wearable/core/src/main/java/org/microg/gms/wearable/WearableReader.java create mode 100644 play-services-wearable/core/src/main/java/org/microg/gms/wearable/WearableWriter.java diff --git a/play-services-wearable/core/src/main/java/org/microg/gms/wearable/WearableReader.java b/play-services-wearable/core/src/main/java/org/microg/gms/wearable/WearableReader.java new file mode 100644 index 0000000000..8e512d9a00 --- /dev/null +++ b/play-services-wearable/core/src/main/java/org/microg/gms/wearable/WearableReader.java @@ -0,0 +1,110 @@ +package org.microg.gms.wearable; + +import android.util.Log; + +import org.microg.gms.wearable.proto.RootMessage; + +import java.io.IOException; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.LinkedBlockingDeque; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicBoolean; + +public class WearableReader { + private static final String TAG = "GmsWearReader"; + + private final String nodeId; + + private final WearableConnection source; + private final WearableConnection listenerView; + private final WearableConnection.Listener listener; + + private final CountDownLatch finishedLatch = new CountDownLatch(1); + private final AtomicBoolean closed = new AtomicBoolean(false); + + private volatile Thread thread; + + public WearableReader(String nodeId, WearableConnection source, + WearableConnection listenerView, WearableConnection.Listener listener) { + this.nodeId = nodeId; + this.source = source; + this.listenerView = listenerView; + this.listener = listener; + } + + public void start() { + thread = new Thread(this::loop, "WearReader-" + nodeId); + thread.start(); + Log.d(TAG, "Reader started for node "+ nodeId); + } + + public void close() { + if (closed.compareAndSet(false, true)) { + Log.d(TAG, "Closing reader for node " + nodeId); + Thread t = thread; + if (t != null) t.interrupt(); + try { + source.close(); + } catch (IOException ignore) {} + } + } + + public boolean awaitFinished(long timeout, TimeUnit unit) throws InterruptedException { + return finishedLatch.await(timeout, unit); + } + + public void awaitFinished() { + boolean interrupted = false; + while (true) { + try { + finishedLatch.await(); + break; + } catch (InterruptedException e){ + interrupted = true; + } + } + if (interrupted) Thread.currentThread().interrupt(); + } + + public boolean isClosed() { + return closed.get(); + } + + private void loop() { + try { + listener.onConnected(listenerView); + + while (!Thread.currentThread().isInterrupted()) { + RootMessage message; + try { + message = source.readMessage(); + } catch (IOException e) { + if (!closed.get()) { + Log.w(TAG, "Read error from node " + nodeId + ": " + e.getMessage()); + } + break; + } + + if (message == null) break; + + try { + listener.onMessage(listenerView, message); + } catch (Exception e) { + Log.e(TAG, "Error dispatching message from node " + nodeId, e); + } + } + } catch (Exception e) { + if (!closed.get()) { + Log.e(TAG, "Unexpected reader error for node" + nodeId, e); + } + } finally { + Log.d(TAG, "Reader finished for node " + nodeId); + try { + listener.onDisconnected(); + } catch (Exception e) { + Log.e(TAG, "Error in onDisconnected() for node " + nodeId); + } + finishedLatch.countDown(); + } + } +} diff --git a/play-services-wearable/core/src/main/java/org/microg/gms/wearable/WearableWriter.java b/play-services-wearable/core/src/main/java/org/microg/gms/wearable/WearableWriter.java new file mode 100644 index 0000000000..01bf0c135c --- /dev/null +++ b/play-services-wearable/core/src/main/java/org/microg/gms/wearable/WearableWriter.java @@ -0,0 +1,108 @@ +package org.microg.gms.wearable; + +import android.util.Log; + +import org.microg.gms.wearable.proto.RootMessage; + +import java.io.IOException; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.LinkedBlockingDeque; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicBoolean; + +public class WearableWriter { + private static final String TAG = "GmsWearWriter"; + + private static final RootMessage STOP = new RootMessage.Builder().build(); + + private final String nodeId; + private final WearableConnection connection; + private final LinkedBlockingDeque queue = new LinkedBlockingDeque<>(); + private final CountDownLatch finishedLatch = new CountDownLatch(1); + private final AtomicBoolean closed = new AtomicBoolean(false); + + private volatile Thread thread; + + public WearableWriter(String nodeId, WearableConnection connection) { + this.nodeId = nodeId; + this.connection = connection; + } + + public void start() { + thread = new Thread(this::loop, "WearWriter-" + nodeId); + thread.start(); + Log.d(TAG, "Writer started for node " + nodeId); + } + + public void close() { + if (closed.compareAndSet(false, true)) { + Log.d(TAG, "Closing writer for node " + nodeId); + queue.clear(); + queue.offer(STOP); + Thread t = thread; + if (t != null) t.interrupt(); + } + } + + public void enqueue(RootMessage message) { + if (!closed.get()) { + queue.offer(message); + } + } + + public boolean awaitFinished(long timeout, TimeUnit unit) throws InterruptedException { + return finishedLatch.await(timeout, unit); + } + + public void awaitFinished() { + boolean interrupted = false; + while (true) { + try { + finishedLatch.await(); + break; + } catch (InterruptedException e){ + interrupted = true; + } + } + if (interrupted) Thread.currentThread().interrupt(); + } + + public boolean isClosed() { + return closed.get(); + } + + private void loop() { + try { + while (!Thread.currentThread().isInterrupted()) { + RootMessage message; + try { + message = queue.take(); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + break; + } + + if (message == STOP) break; + + try { + connection.writeMessage(message); + } catch (IOException e) { + if (!closed.get()) { + Log.w(TAG, "Write failed for node " + nodeId + ": " + e.getMessage()); + try { + connection.close(); + } catch (IOException ignore) {} + } + break; + } + } + } catch (Exception e) { + if (!closed.get()) { + Log.e(TAG, "Unexpected writer error for node " + nodeId, e); + } + } finally { + Log.d(TAG, "Writer finished for node "+ nodeId); + finishedLatch.countDown(); + } + } +} From 048aa39b4069b0b3ec81b72c80d7f232cf612bcd Mon Sep 17 00:00:00 2001 From: deadYokai Date: Wed, 4 Mar 2026 15:22:25 +0200 Subject: [PATCH 26/29] feat(wear): add missing messages to MessageHandler and MessageListener --- .../microg/gms/wearable/MessageHandler.java | 20 +++++++++++++++++-- .../microg/gms/wearable/MessageListener.java | 14 +++++++++++++ 2 files changed, 32 insertions(+), 2 deletions(-) diff --git a/play-services-wearable/core/src/main/java/org/microg/gms/wearable/MessageHandler.java b/play-services-wearable/core/src/main/java/org/microg/gms/wearable/MessageHandler.java index e60afc45dc..dbd7ac4a69 100644 --- a/play-services-wearable/core/src/main/java/org/microg/gms/wearable/MessageHandler.java +++ b/play-services-wearable/core/src/main/java/org/microg/gms/wearable/MessageHandler.java @@ -35,6 +35,8 @@ import org.microg.gms.profile.Build; import org.microg.gms.settings.SettingsContract; import org.microg.gms.wearable.proto.AckAsset; +import org.microg.gms.wearable.proto.ControlMessage; +import org.microg.gms.wearable.proto.EncryptionHandshake; import org.microg.gms.wearable.proto.AppKey; import org.microg.gms.wearable.proto.AssetEntry; import org.microg.gms.wearable.proto.Connect; @@ -52,7 +54,6 @@ import java.io.FileInputStream; import java.io.FileOutputStream; import java.io.IOException; -import java.nio.file.Files; import java.util.ArrayList; import java.util.Arrays; import java.util.List; @@ -182,7 +183,7 @@ public void onSetDataItem(SetDataItem setDataItem) { public void onRpcRequest(Request rpcRequest) { Log.d(TAG, "onRpcRequest: " + rpcRequest); if (TextUtils.isEmpty(rpcRequest.targetNodeId) || rpcRequest.targetNodeId.equals(wearable.getLocalNodeId())) { - int requestId = rpcRequest.requestId + 31 * (rpcRequest.generation + 527); + int requestId = rpcRequest.requestId != null ? rpcRequest.requestId : 0; String path = rpcRequest.path; byte[] data = rpcRequest.rawData != null ? rpcRequest.rawData.toByteArray() : null; String sourceNodeId = TextUtils.isEmpty(rpcRequest.sourceNodeId) ? peerNodeId : rpcRequest.sourceNodeId; @@ -197,6 +198,11 @@ public void onRpcRequest(Request rpcRequest) { } } + @Override + public void onRpcWithResponseId(Request rpcWithResponseId) { + + } + @Override public void onHeartbeat(Heartbeat heartbeat) { Log.d(TAG, "onHeartbeat: " + heartbeat); @@ -213,6 +219,16 @@ public void onChannelRequest(Request channelRequest) { Log.d(TAG, "onChannelRequest:" + channelRequest); } + @Override + public void onEncryptionHandshake(EncryptionHandshake encryptionHandshake) { + + } + + @Override + public void onControlMessage(ControlMessage controlMessage) { + + } + public void handleMessage(WearableConnection connection, String sourceNodeId, RootMessage message) { Log.d(TAG, "handleMessage from " + sourceNodeId); diff --git a/play-services-wearable/core/src/main/java/org/microg/gms/wearable/MessageListener.java b/play-services-wearable/core/src/main/java/org/microg/gms/wearable/MessageListener.java index fdfd79f920..8712284bab 100644 --- a/play-services-wearable/core/src/main/java/org/microg/gms/wearable/MessageListener.java +++ b/play-services-wearable/core/src/main/java/org/microg/gms/wearable/MessageListener.java @@ -7,6 +7,8 @@ import org.microg.gms.wearable.proto.AckAsset; import org.microg.gms.wearable.proto.Connect; +import org.microg.gms.wearable.proto.ControlMessage; +import org.microg.gms.wearable.proto.EncryptionHandshake; import org.microg.gms.wearable.proto.FetchAsset; import org.microg.gms.wearable.proto.FilePiece; import org.microg.gms.wearable.proto.Heartbeat; @@ -49,12 +51,18 @@ public void onMessage(WearableConnection connection, RootMessage message) { onSetDataItem(message.setDataItem); } else if (message.rpcRequest != null) { onRpcRequest(message.rpcRequest); + } else if (message.rpcServiceRequest != null) { + onRpcWithResponseId(message.rpcServiceRequest); } else if (message.heartbeat != null) { onHeartbeat(message.heartbeat); } else if (message.filePiece != null) { onFilePiece(message.filePiece); } else if (message.channelRequest != null) { onChannelRequest(message.channelRequest); + } else if (message.e2eHandshake != null) { + onEncryptionHandshake(message.e2eHandshake); + } else if (message.controlMessage != null) { + onControlMessage(message.controlMessage); } else { System.err.println("Unknown message: " + message); } @@ -74,9 +82,15 @@ public void onMessage(WearableConnection connection, RootMessage message) { public abstract void onRpcRequest(Request rpcRequest); + public abstract void onRpcWithResponseId(Request rpcWithResponseId); + public abstract void onHeartbeat(Heartbeat heartbeat); public abstract void onFilePiece(FilePiece filePiece); public abstract void onChannelRequest(Request channelRequest); + + public abstract void onEncryptionHandshake(EncryptionHandshake encryptionHandshake); + + public abstract void onControlMessage(ControlMessage controlMessage); } From 9d96a77ee91fc4c02a89a1aaf76e4909283bc11f Mon Sep 17 00:00:00 2001 From: deadYokai Date: Wed, 4 Mar 2026 18:12:22 +0100 Subject: [PATCH 27/29] feat(wear): add NetworkConnectionManager --- .../core/src/main/AndroidManifest.xml | 1 + .../org/microg/gms/wearable/WearableImpl.java | 14 +- .../bluetooth/NetworkConnectionManager.java | 4 - .../bluetooth/NetworkConnectionThread.java | 4 - .../network/NetworkConnectionManager.java | 202 +++++++++++++ .../network/NetworkConnectionThread.java | 273 ++++++++++++++++++ 6 files changed, 488 insertions(+), 10 deletions(-) delete mode 100644 play-services-wearable/core/src/main/java/org/microg/gms/wearable/bluetooth/NetworkConnectionManager.java delete mode 100644 play-services-wearable/core/src/main/java/org/microg/gms/wearable/bluetooth/NetworkConnectionThread.java create mode 100644 play-services-wearable/core/src/main/java/org/microg/gms/wearable/network/NetworkConnectionManager.java create mode 100644 play-services-wearable/core/src/main/java/org/microg/gms/wearable/network/NetworkConnectionThread.java diff --git a/play-services-wearable/core/src/main/AndroidManifest.xml b/play-services-wearable/core/src/main/AndroidManifest.xml index 2b1e416d85..8da0e0402c 100644 --- a/play-services-wearable/core/src/main/AndroidManifest.xml +++ b/play-services-wearable/core/src/main/AndroidManifest.xml @@ -6,6 +6,7 @@ + diff --git a/play-services-wearable/core/src/main/java/org/microg/gms/wearable/WearableImpl.java b/play-services-wearable/core/src/main/java/org/microg/gms/wearable/WearableImpl.java index 45d3187764..7f1ce2f215 100644 --- a/play-services-wearable/core/src/main/java/org/microg/gms/wearable/WearableImpl.java +++ b/play-services-wearable/core/src/main/java/org/microg/gms/wearable/WearableImpl.java @@ -47,6 +47,7 @@ import org.microg.gms.common.RemoteListenerProxy; import org.microg.gms.common.Utils; import org.microg.gms.wearable.bluetooth.BluetoothClient; +import org.microg.gms.wearable.network.NetworkConnectionManager; import org.microg.gms.wearable.channel.ChannelAssetApiEnum; import org.microg.gms.wearable.channel.ChannelCallbacks; import org.microg.gms.wearable.channel.ChannelManager; @@ -55,7 +56,6 @@ import org.microg.gms.wearable.proto.AppKey; import org.microg.gms.wearable.proto.AppKeys; import org.microg.gms.wearable.proto.Connect; -import org.microg.gms.wearable.proto.FetchAsset; import org.microg.gms.wearable.proto.FilePiece; import org.microg.gms.wearable.proto.Request; import org.microg.gms.wearable.proto.RootMessage; @@ -119,6 +119,8 @@ public class WearableImpl { private AssetFetcher assetFetcher; + private NetworkConnectionManager networkManager; + public WearableImpl(Context context, NodeDatabaseHelper nodeDatabase, ConfigurationDatabaseHelper configDatabase) { this.context = context; this.nodeDatabase = nodeDatabase; @@ -904,7 +906,15 @@ private void handleBle(ConnectionConfiguration config, boolean enabled) { } private void handleNetwork(ConnectionConfiguration config, boolean enabled) { - Log.w(TAG, "Network not implemented"); + if (networkManager == null) { + networkManager = new NetworkConnectionManager(context, this); + } + + if (enabled) { + networkManager.addConfig(config); + } else { + networkManager.removeConfig(config); + } } private void handleLegacy(ConnectionConfiguration config, boolean enabled) { diff --git a/play-services-wearable/core/src/main/java/org/microg/gms/wearable/bluetooth/NetworkConnectionManager.java b/play-services-wearable/core/src/main/java/org/microg/gms/wearable/bluetooth/NetworkConnectionManager.java deleted file mode 100644 index 2b89962950..0000000000 --- a/play-services-wearable/core/src/main/java/org/microg/gms/wearable/bluetooth/NetworkConnectionManager.java +++ /dev/null @@ -1,4 +0,0 @@ -package org.microg.gms.wearable.bluetooth; - -public class NetworkConnectionManager { -} diff --git a/play-services-wearable/core/src/main/java/org/microg/gms/wearable/bluetooth/NetworkConnectionThread.java b/play-services-wearable/core/src/main/java/org/microg/gms/wearable/bluetooth/NetworkConnectionThread.java deleted file mode 100644 index 6f14ca6476..0000000000 --- a/play-services-wearable/core/src/main/java/org/microg/gms/wearable/bluetooth/NetworkConnectionThread.java +++ /dev/null @@ -1,4 +0,0 @@ -package org.microg.gms.wearable.bluetooth; - -public class NetworkConnectionThread { -} diff --git a/play-services-wearable/core/src/main/java/org/microg/gms/wearable/network/NetworkConnectionManager.java b/play-services-wearable/core/src/main/java/org/microg/gms/wearable/network/NetworkConnectionManager.java new file mode 100644 index 0000000000..da4f861ddb --- /dev/null +++ b/play-services-wearable/core/src/main/java/org/microg/gms/wearable/network/NetworkConnectionManager.java @@ -0,0 +1,202 @@ +package org.microg.gms.wearable.network; + +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.Intent; +import android.content.IntentFilter; +import android.net.ConnectivityManager; +import android.net.NetworkInfo; +import android.util.Log; + +import com.google.android.gms.wearable.ConnectionConfiguration; + +import org.microg.gms.wearable.WearableImpl; +import org.microg.gms.wearable.proto.RootMessage; + +import java.util.HashMap; +import java.util.Map; + +public class NetworkConnectionManager implements Cloneable { + private static final String TAG = "GmsWearNetMgr"; + + private static final long SHUTDOWN_JOIN_TIMEOUT = 5000; + + private final Context context; + private final WearableImpl wearable; + + private final Map threads = new HashMap<>(); + private final Map configs = new HashMap<>(); + + private final BroadcastReceiver connectivityReceiver; + private volatile boolean shutdown = false; + + public NetworkConnectionManager(Context context, WearableImpl wearable) { + this.context = context; + this.wearable = wearable; + + connectivityReceiver = new BroadcastReceiver() { + @Override + public void onReceive(Context context, Intent intent) { + if (!ConnectivityManager.EXTRA_NO_CONNECTIVITY.equals(intent.getAction())) { + return; + } + + if (intent.getBooleanExtra(ConnectivityManager.EXTRA_NO_CONNECTIVITY, false)) { + return; + } + + onNetworkAvailable(); + } + }; + + context.registerReceiver( + connectivityReceiver, + new IntentFilter(ConnectivityManager.EXTRA_NO_CONNECTIVITY) + ); + + Log.d(TAG, "initialised"); + } + + public synchronized void addConfig(ConnectionConfiguration config) { + if (shutdown) { + Log.w(TAG, "Manager is shut down, ignoring addConfig for " + config.address); + return; + } + validateConfig(config); + + String addr = config.address; + configs.put(addr, config); + + NetworkConnectionThread existing = threads.get(addr); + if (existing != null) { + if (existing.isAlive() && !existing.isInterrupted()) { + Log.d(TAG, "Thread already active for " + addr + ", triggering retry"); + existing.triggerRetry(); + } else { + Log.d(TAG, "Replacing dead thread for " + addr); + existing.close(); + threads.remove(addr); + startThread(config); + } + return; + } + + if (isNetworkAvailable()) { + startThread(config); + } else { + Log.d(TAG, "Network unavailable, deferring connection for " + addr); + } + } + + public synchronized void removeConfig(ConnectionConfiguration config) { + if (shutdown) return; + validateConfig(config); + + String addr = config.address; + configs.remove(addr); + + NetworkConnectionThread t = threads.remove(addr); + if (t != null) { + Log.d(TAG, "Removing thread for " + addr); + t.close(); + joinThread(t, addr); + } + } + + public synchronized void sendMessage(String address, RootMessage message) { + NetworkConnectionThread t = threads.get(address); + if (t == null || !t.isAlive()) { + throw new IllegalArgumentException("No active connection for " + address); + } + t.sendMessage(message); + } + + public synchronized boolean hasConnection(String address) { + NetworkConnectionThread t = threads.get(address); + return t != null && t.isAlive() && !t.isInterrupted(); + } + + private void onNetworkAvailable() { + synchronized (this) { + if (shutdown) return; + Log.d(TAG, "Network available — checking " + configs.size() + " config(s)"); + + for (Map.Entry entry : configs.entrySet()) { + String addr = entry.getKey(); + NetworkConnectionThread t = threads.get(addr); + + if (t == null || !t.isAlive()) { + Log.d(TAG, "Starting thread for " + addr + " (network regained)"); + if (t != null) threads.remove(addr); + startThread(entry.getValue()); + } else { + t.triggerRetry(); + } + } + } + } + + @SuppressWarnings("deprecation") + private boolean isNetworkAvailable() { + ConnectivityManager cm = + (ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE); + if (cm == null) return false; + NetworkInfo info = cm.getActiveNetworkInfo(); + return info != null && info.isConnected(); + } + + public synchronized void close() { + if (shutdown) return; + shutdown = true; + Log.d(TAG, "Shutting down NetworkConnectionManager (" + threads.size() + " thread(s))"); + + try { + context.unregisterReceiver(connectivityReceiver); + } catch (Exception e) { + Log.w(TAG, "Error unregistering connectivity receiver", e); + } + + for (NetworkConnectionThread t : threads.values()) t.close(); + for (Map.Entry e : threads.entrySet()) { + joinThread(e.getValue(), e.getKey()); + } + + threads.clear(); + configs.clear(); + Log.d(TAG, "NetworkConnectionManager shut down"); + } + + private void startThread(ConnectionConfiguration config) { + NetworkConnectionThread t = + new NetworkConnectionThread(context, config, wearable); + threads.put(config.address, t); + t.start(); + Log.d(TAG, "Started NetworkConnectionThread for " + config.address); + } + + private static void joinThread(NetworkConnectionThread t, String address) { + try { + t.join(SHUTDOWN_JOIN_TIMEOUT); + if (t.isAlive()) { + Log.w(TAG, "Thread for " + address + " did not stop within " + + SHUTDOWN_JOIN_TIMEOUT + "ms"); + } + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + } + } + + private static void validateConfig(ConnectionConfiguration config) { + if (config == null || config.address == null) { + throw new IllegalArgumentException("Config or address is null"); + } + if (config.type != WearableImpl.TYPE_NETWORK) { + throw new IllegalArgumentException("Expected TYPE_NETWORK, got type=" + config.type); + } + if (config.role != WearableImpl.ROLE_CLIENT) { + throw new IllegalArgumentException("Expected ROLE_CLIENT, got role=" + config.role); + } + } + + +} diff --git a/play-services-wearable/core/src/main/java/org/microg/gms/wearable/network/NetworkConnectionThread.java b/play-services-wearable/core/src/main/java/org/microg/gms/wearable/network/NetworkConnectionThread.java new file mode 100644 index 0000000000..a821a6a692 --- /dev/null +++ b/play-services-wearable/core/src/main/java/org/microg/gms/wearable/network/NetworkConnectionThread.java @@ -0,0 +1,273 @@ +package org.microg.gms.wearable.network; + +import android.content.Context; +import android.util.Log; + +import com.google.android.gms.wearable.ConnectionConfiguration; + +import org.microg.gms.wearable.MessageHandler; +import org.microg.gms.wearable.SocketWearableConnection; +import org.microg.gms.wearable.WearableConnection; +import org.microg.gms.wearable.WearableImpl; +import org.microg.gms.wearable.WearableReader; +import org.microg.gms.wearable.WearableWriter; +import org.microg.gms.wearable.bluetooth.RetryStrategy; +import org.microg.gms.wearable.proto.MessagePiece; +import org.microg.gms.wearable.proto.RootMessage; + +import java.io.IOException; +import java.net.InetSocketAddress; +import java.net.Socket; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.locks.Condition; +import java.util.concurrent.locks.Lock; +import java.util.concurrent.locks.ReentrantLock; + +public class NetworkConnectionThread extends Thread implements Cloneable{ + private static final String TAG = "GmsWearNetThread"; + + private static final int CONNECT_TIMEOUT = 30000; + private static final long MIN_ATTEMPT_INTERVAL = 3000; + + private final Context context; + private final ConnectionConfiguration config; + private final WearableImpl wearable; + + private final RetryStrategy retryStrategy; + + private final Lock lock = new ReentrantLock(); + + private final Condition retryCondition = lock.newCondition(); + private final AtomicBoolean running = new AtomicBoolean(true); + private final AtomicBoolean immediateRetry = new AtomicBoolean(false); + + private volatile Socket activeSocket; + private volatile WearableWriter activeWriter; + private long lastAttemptTime = 0; + + public NetworkConnectionThread(Context context, ConnectionConfiguration config, WearableImpl wearable) { + super("NetThread-" + config.address); + this.config = config; + this.context = context; + this.wearable = wearable; + this.retryStrategy = RetryStrategy.fromPolicy(config.connectionRetryStrategy); + } + + @Override + public void run() { + Log.d(TAG, "Started for " + config.address); + + while (running.get() && !isInterrupted()) { + try { + enforceMinInterval(); + if (!running.get()) break; + + connect(); + retryStrategy.reset(); + } catch (IOException e) { + Log.w(TAG, "Connection failed to " + config.address + ": " + e.getMessage()); + } catch (InterruptedException e ) { + if (!running.get()) break; + } catch (Exception e){ + Log.e(TAG, "Unexpected error for " + config.address, e); + } finally { + teardown(); + } + + if (running.get() && !isInterrupted()) { + try { + waitForRetry(); + } catch (InterruptedException e) { + if (!running.get()) break; + } + } + } + + Log.d(TAG, "Stopped for " + config.address); + } + + private void connect() throws IOException { + Log.d(TAG, "Connecting to " + config.address + ":" + WearableImpl.WEAR_TCP_PORT); + + Socket socket = new Socket(); + socket.setTcpNoDelay(true); + socket.connect( + new InetSocketAddress(config.address, WearableImpl.WEAR_TCP_PORT), + CONNECT_TIMEOUT + ); + activeSocket = socket; + + Log.d(TAG, "Connected to " + config.address); + + SocketWearableConnection raw = new SocketWearableConnection(socket, null); + MessageHandler msgHandler = new MessageHandler(context, wearable, config); + WearableWriter writer = new WearableWriter(config.address, raw); + QueueingConnection facade = new QueueingConnection(writer); + WearableReader reader = new WearableReader(config.address, raw, facade, msgHandler); + + activeWriter = writer; + + writer.start(); + reader.start(); + + reader.awaitFinished(); + + writer.close(); + writer.awaitFinished(); + + try { + raw.close(); + } catch (IOException ignore) {} + + activeWriter = null; + } + + private void enforceMinInterval() throws InterruptedException { + long elapsed = System.currentTimeMillis() - lastAttemptTime; + if (lastAttemptTime > 0 && elapsed < MIN_ATTEMPT_INTERVAL) { + Thread.sleep(MIN_ATTEMPT_INTERVAL - elapsed); + } + lastAttemptTime = System.currentTimeMillis(); + } + + private void waitForRetry() throws InterruptedException { + long delayMs = retryStrategy.nextDelayMs(); + + if (immediateRetry.getAndSet(false)) { + Log.d(TAG, "Immediate retry for " + config.address); + return; + } + + if (delayMs < 0) { + Log.d(TAG, "Retry OFF for " + config.address + ", waiting for external trigger"); + waitExternal(); + return; + } + + Log.d(TAG, "Waiting " + delayMs + "ms before retry to " + config.address); + lock.lock(); + try { + long deadline = System.currentTimeMillis() + delayMs; + while (running.get() && !immediateRetry.get()) { + long remaining = deadline - System.currentTimeMillis(); + if (remaining <= 0) break; + retryCondition.await(remaining, TimeUnit.MILLISECONDS); + } + immediateRetry.set(false); + } finally { + lock.unlock(); + } + } + + private void waitExternal() throws InterruptedException { + lock.lock(); + try { + while (running.get() && !immediateRetry.get()) { + retryCondition.await(); + } + immediateRetry.set(false); + } finally { + lock.unlock(); + } + } + + private void signalRetry() { + lock.lock(); + try { + immediateRetry.set(true); + retryCondition.signal(); + } finally { + lock.unlock(); + } + } + + public void sendMessage(RootMessage message) { + WearableWriter w = activeWriter; + if (w != null && !w.isClosed()) { + w.enqueue(message); + } else { + Log.w(TAG, "sendMessage() - no active writer for " + config.address); + } + } + + public void retryNow() { + retryStrategy.reset(); + signalRetry(); + } + + public void triggerRetry() { + signalRetry(); + } + + public boolean isConnected() { + WearableWriter w = activeWriter; + return w != null && !w.isClosed(); + } + + public void close() { + Log.d(TAG, "Closing for " + config.address); + running.set(false); + signalRetry(); + interrupt(); + teardown(); + } + + private void teardown() { + WearableWriter w = activeWriter; + if (w != null) { + w.close(); + activeWriter = null; + } + + Socket s = activeSocket; + if (s != null) { + try { + s.close(); + } catch (IOException e) { + Log.w(TAG, "Error closing socket for " + config.address); + } + activeSocket = null; + } + } + + private static final class QueueingConnection extends WearableConnection { + private static final Listener NO_OP = new Listener() { + @Override + public void onConnected(WearableConnection connection) {} + + @Override + public void onMessage(WearableConnection connection, RootMessage message) {} + + @Override + public void onDisconnected() {} + }; + + private final WearableWriter writer; + + QueueingConnection(WearableWriter writer) { + super(NO_OP); + this.writer = writer; + } + + @Override + public void writeMessage(RootMessage message) throws IOException { + writer.enqueue(message); + } + + @Override + protected void writeMessagePiece(MessagePiece piece) throws IOException { + throw new UnsupportedOperationException("write-only facade"); + } + + @Override + protected MessagePiece readMessagePiece() throws IOException { + throw new UnsupportedOperationException("write-only facade"); + } + + @Override + public void close() throws IOException { + writer.close(); + } + } +} From 05f76cac3494f1e85de513f4fcd75a0170f7a265 Mon Sep 17 00:00:00 2001 From: deadYokai Date: Wed, 4 Mar 2026 22:43:35 +0200 Subject: [PATCH 28/29] rough ble implementation --- .../bluetooth/BleConnectionManager.java | 675 ++++++++++++++++++ .../BleConnectionManagerInterface.java | 9 + .../gms/wearable/bluetooth/BleException.java | 32 + .../gms/wearable/bluetooth/BleScanner.java | 12 + .../bluetooth/BleServicesHandler.java | 6 + .../gms/wearable/bluetooth/BleState.java | 13 + .../wearable/bluetooth/BleStateMachine.java | 93 +++ .../bluetooth/BleTimeoutException.java | 7 + .../bluetooth/BluetoothGattHelper.java | 10 + .../wearable/bluetooth/GattEventListener.java | 10 + 10 files changed, 867 insertions(+) create mode 100644 play-services-wearable/core/src/main/java/org/microg/gms/wearable/bluetooth/BleConnectionManager.java create mode 100644 play-services-wearable/core/src/main/java/org/microg/gms/wearable/bluetooth/BleConnectionManagerInterface.java create mode 100644 play-services-wearable/core/src/main/java/org/microg/gms/wearable/bluetooth/BleException.java create mode 100644 play-services-wearable/core/src/main/java/org/microg/gms/wearable/bluetooth/BleScanner.java create mode 100644 play-services-wearable/core/src/main/java/org/microg/gms/wearable/bluetooth/BleServicesHandler.java create mode 100644 play-services-wearable/core/src/main/java/org/microg/gms/wearable/bluetooth/BleState.java create mode 100644 play-services-wearable/core/src/main/java/org/microg/gms/wearable/bluetooth/BleStateMachine.java create mode 100644 play-services-wearable/core/src/main/java/org/microg/gms/wearable/bluetooth/BleTimeoutException.java create mode 100644 play-services-wearable/core/src/main/java/org/microg/gms/wearable/bluetooth/BluetoothGattHelper.java create mode 100644 play-services-wearable/core/src/main/java/org/microg/gms/wearable/bluetooth/GattEventListener.java diff --git a/play-services-wearable/core/src/main/java/org/microg/gms/wearable/bluetooth/BleConnectionManager.java b/play-services-wearable/core/src/main/java/org/microg/gms/wearable/bluetooth/BleConnectionManager.java new file mode 100644 index 0000000000..0d21549a68 --- /dev/null +++ b/play-services-wearable/core/src/main/java/org/microg/gms/wearable/bluetooth/BleConnectionManager.java @@ -0,0 +1,675 @@ +package org.microg.gms.wearable.bluetooth; + +import android.bluetooth.BluetoothAdapter; +import android.bluetooth.BluetoothGattCharacteristic; +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.Intent; +import android.content.IntentFilter; +import android.os.Build; +import android.os.Looper; +import android.os.Message; +import android.util.Log; + +import com.google.android.gms.wearable.ConnectionConfiguration; + +import java.io.PrintWriter; +import java.io.StringWriter; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.concurrent.atomic.AtomicLong; +import java.util.concurrent.atomic.AtomicReference; + +public class BleConnectionManager extends BleStateMachine implements BleConnectionManagerInterface, GattEventListener { + private static final String TAG = "BleConnectionManager"; + + public static final int MSG_INIT = 1; + public static final int MSG_BT_ADAPTER_STATE_CHANGED = 2; + public static final int MSG_CONNECTION_CONFIG_UPDATE = 3; + public static final int MSG_START_SCAN = 4; + public static final int MSG_START_FORCED_SCAN = 5; + public static final int MSG_SCAN_FAILED = 6; + public static final int MSG_STOP_SCAN = 7; + public static final int MSG_RESCHEDULE_SCAN = 8; + public static final int MSG_RECONNECT_REQUESTED = 9; + public static final int MSG_SERVICE_DISCOVERY_COMPLETE = 10; + public static final int MSG_HANDLE_NOTIFICATION = 11; + public static final int MSG_DECOMMISSION_WATCH = 12; + public static final int MSG_RECONNECT_CHARACTERISTIC_CHANGED = 13; + public static final int MSG_ERROR = 14; + public static final int MSG_CONNECTION_THREAD_DONE = 15; + public static final int MSG_GATT_CONNECTION_CLOSED = 16; + public static final int MSG_READY_TO_SETUP_ANCS = 17; + public static final int MSG_UPDATE_TIME = 18; + public static final int MSG_ON_SERVICE_CHANGED = 19; + public static final int MSG_RESET_CHARACTERICTIC_CHANGED = 20; + public static final int MSG_RESET_CONNECTION = 21; + + public final Context context; + public final BluetoothAdapter btAdapter; + + public final BleServicesHandler gattHelper; + final BluetoothGattHelper bleConnHelper; + + public volatile AtomicReference config; + + final AtomicBoolean isReceiverRegistered; + + final AtomicLong timeServiceNotFoundCounter = new AtomicLong(); + final AtomicLong invalidGattHandleCounter = new AtomicLong(); + final AtomicLong readNotPermittedCounter = new AtomicLong(); + final AtomicLong writeNotPermittedCounter = new AtomicLong(); + final AtomicLong invalidDecommissionCounter = new AtomicLong(); + final AtomicLong serviceNotFoundCounter = new AtomicLong(); + final AtomicLong timeCharInvalidCounter = new AtomicLong(); + final AtomicLong timezoneOffsetInvalidCounter = new AtomicLong(); + final AtomicLong missingClockworkCharCounter = new AtomicLong(); + + public final AtomicInteger scanAttemptCount; + public final AtomicInteger totalExceptionCount; + public final AtomicInteger disconnectExceptionCount; + + final BroadcastReceiver btStateReceiver; + public final BroadcastReceiver screenOnReceiver; + + final BleState idleState; + public final BleState discoveredState; + public final BleState connectedState; + public final BleState disconnectingState; + public final BleState errorDisconnectedState; + private final BleState disconnectedState; + private final BleState scanningState; + private final BleState connectingState; + + private final BleScanner bleScanner; + + private static final int ERROR_SAMPLER_BUF_SIZE = 300; + private static final int SERVICE_CHANGED_SAMPLER_BUF_SIZE = 50; + private final AtomicBoolean isBtlePrioEnabled; + private final AtomicLong errorSampler = new AtomicLong(); + + public BleConnectionManager( + Context context, + BluetoothAdapter bluetoothAdapter, + BleScanner bleScanner, + BluetoothGattHelper bleConnHelper, + BleServicesHandler gattHelper, + Looper looper, + ConnectionConfiguration connectionConfiguration) { + super("BleConnectionManager", looper); + + this.isBtlePrioEnabled = new AtomicBoolean(true); + this.config = new AtomicReference<>(); + this.isReceiverRegistered = new AtomicBoolean(false); + this.totalExceptionCount = new AtomicInteger(); + this.disconnectExceptionCount = new AtomicInteger(); + this.scanAttemptCount = new AtomicInteger(); + + this.btStateReceiver = new BroadcastReceiver() { + @Override + public void onReceive(Context ctx, Intent intent) { + ConnectionConfiguration cfg = BleConnectionManager.this.config.get(); + if (cfg == null || !cfg.enabled) return; + + String action = intent.getAction(); + if (BluetoothAdapter.ACTION_STATE_CHANGED.equals(action)) { + int state = intent.getIntExtra( + BluetoothAdapter.EXTRA_STATE, BluetoothAdapter.STATE_OFF); + if (state == BluetoothAdapter.STATE_OFF + || state == BluetoothAdapter.STATE_TURNING_OFF) { + sendBtAdapterStateMsg(state); + } + } else if ("android.gms.wearable.altReconnect".equals(action)) { + sendMessage(MSG_RECONNECT_REQUESTED); + } + } + }; + + this.screenOnReceiver = new BroadcastReceiver() { + @Override + public void onReceive(Context ctx, Intent intent) { + removeMessages(MSG_START_FORCED_SCAN); + sendMessage(MSG_START_FORCED_SCAN); + BleConnectionManager.this.context + .unregisterReceiver(BleConnectionManager.this.screenOnReceiver); + } + }; + + BleState idleStateLocal = new IdleState(this); + BleState disconnectedStateLocal = new ServiceOnState(this); + BleState discoveredStateLocal = new DisconnectedState(this); + BleState scanningStateLocal = new ScanningState(this); + BleState connectingStateLocal = new ConnectingState(this); + BleState connectedStateLocal = new ConnectedState(this); + BleState disconnectingStateLocal = new DisconnectingState(this); + BleState errorDisconnectedStateLocal = new ServiceOffState(this); + + this.idleState = idleStateLocal; + this.disconnectedState = disconnectedStateLocal; + this.discoveredState = discoveredStateLocal; + this.scanningState = scanningStateLocal; + this.connectingState = connectingStateLocal; + this.connectedState = connectedStateLocal; + this.disconnectingState = disconnectingStateLocal; + this.errorDisconnectedState = errorDisconnectedStateLocal; + + this.context = context; + this.btAdapter = bluetoothAdapter; + this.bleScanner = bleScanner; + this.bleConnHelper = bleConnHelper; + this.gattHelper = gattHelper; + + bleConnHelper.setGattEventListener(this); + + this.config.set(connectionConfiguration); + this.isBtlePrioEnabled.set( + connectionConfiguration == null || connectionConfiguration.btlePriority); + + addState(disconnectedStateLocal); + addState(discoveredStateLocal); + addState(scanningStateLocal); + addState(connectingStateLocal); + addState(connectedStateLocal); + addState(disconnectingStateLocal); + addState(errorDisconnectedStateLocal); + + addTransition(disconnectedStateLocal, discoveredStateLocal); + addTransition(discoveredStateLocal, scanningStateLocal); + addTransition(scanningStateLocal, connectingStateLocal); + addTransition(scanningStateLocal, disconnectingStateLocal); + addTransition(connectingStateLocal, connectedStateLocal); + addTransition(connectingStateLocal, disconnectingStateLocal); + addTransition(connectedStateLocal, disconnectingStateLocal); + addTransition(disconnectingStateLocal, discoveredStateLocal); + addTransition(discoveredStateLocal, disconnectedStateLocal); + addTransition(disconnectedStateLocal, errorDisconnectedStateLocal); + addTransition(errorDisconnectedStateLocal, disconnectedStateLocal); + + setErrorState(errorDisconnectedStateLocal); + start(); + } + + @Override + protected String getMessageName(int what) { + switch (what) { + case MSG_INIT: return "MSG_INIT"; + case MSG_BT_ADAPTER_STATE_CHANGED: return "MSG_BT_ADAPTER_STATE_CHANGED"; + case MSG_CONNECTION_CONFIG_UPDATE: return "MSG_CONNECTION_CONFIG_UPDATE"; + case MSG_START_SCAN: return "MSG_START_SCAN"; + case MSG_START_FORCED_SCAN: return "MSG_START_FORCED_SCAN"; + case MSG_SCAN_FAILED: return "MSG_SCAN_FAILED"; + case MSG_STOP_SCAN: return "MSG_STOP_SCAN"; + case MSG_RESCHEDULE_SCAN: return "MSG_RESCHEDULE_SCAN"; + case MSG_RECONNECT_REQUESTED: return "MSG_RECONNECT_REQUESTED"; + case MSG_SERVICE_DISCOVERY_COMPLETE: return "MSG_SERVICE_DISCOVERY_COMPLETE"; + case MSG_HANDLE_NOTIFICATION: return "MSG_HANDLE_NOTIFICATION"; + case MSG_DECOMMISSION_WATCH: return "MSG_DECOMMISSION_WATCH"; + case MSG_RECONNECT_CHARACTERISTIC_CHANGED: return "MSG_RECONNECT_CHARACTERISTIC_CHANGED"; + case MSG_ERROR: return "MSG_ERROR"; + case MSG_CONNECTION_THREAD_DONE: return "MSG_CONNECTION_THREAD_DONE"; + case MSG_GATT_CONNECTION_CLOSED: return "MSG_GATT_CONNECTION_CLOSED"; + case MSG_READY_TO_SETUP_ANCS: return "MSG_READY_TO_SETUP_ANCS"; + case MSG_UPDATE_TIME: return "MSG_UPDATE_TIME"; + case MSG_ON_SERVICE_CHANGED: return "MSG_ON_SERVICE_CHANGED"; + case MSG_RESET_CHARACTERICTIC_CHANGED: return "MSG_RESET_CHARACTERICTIC_CHANGED"; + case MSG_RESET_CONNECTION: return "MSG_RESET_CONNECTION"; + default: return "UNKNOWN"; + } + } + + @Override + public void updateConfiguration(ConnectionConfiguration connectionConfiguration) { + Log.d(TAG, "updateConfiguration: config is " + + (connectionConfiguration.enabled ? "enabled" : "disabled")); + this.config.set(connectionConfiguration); + sendMessage(MSG_CONNECTION_CONFIG_UPDATE); + } + + @Override + public void quit() { + + } + + @Override + public void quitSafely() { + + } + + @Override + public void onServiceChanged() { + Log.d(TAG, "onServiceChanged"); + sendMessage(MSG_ON_SERVICE_CHANGED); + } + + @Override + public void onCharacteristicWritten(BluetoothGattCharacteristic characteristic) { + characteristic.getUuid(); + } + + @Override + public void onServicesDiscovered() { + Log.d(TAG, "onServicesDiscovered"); + sendMessage(MSG_SERVICE_DISCOVERY_COMPLETE); + } + + @Override + public void onCharacteristicChanged(BluetoothGattCharacteristic characteristic) {} + + @Override + protected boolean shouldLogMessage(Message message) { + return message.what != MSG_HANDLE_NOTIFICATION; + } + + protected void onQuitting() { + Log.d(TAG, "onQuitting"); + stopScan(); + disconnectGatt(); + disconnectGatt(); // twice in original + if (isReceiverRegistered.compareAndSet(true, false)) { + context.unregisterReceiver(btStateReceiver); + } + } + + void syncReceiverRegistration() { + ConnectionConfiguration cfg = config.get(); + if (cfg != null && !cfg.enabled) { + if (isReceiverRegistered.compareAndSet(true, false)) { + context.unregisterReceiver(btStateReceiver); + } + } else if (isReceiverRegistered.compareAndSet(false, true)) { + IntentFilter filter = new IntentFilter(BluetoothAdapter.ACTION_STATE_CHANGED); + filter.addAction("android.gms.wearable.altReconnect"); + context.registerReceiver(btStateReceiver, filter); + } + } + + boolean handleUnhandledMessage(Message message) { + int what = message.what; + if (what == MSG_RECONNECT_REQUESTED || what == MSG_CONNECTION_THREAD_DONE) { + return true; + } + Log.d(TAG, "[" + currentState().getName() + "] Unhandled message: " + message.what); + return false; + } + + void stopScan() { + if (!bleScanner.isScanning()) { + Log.d(TAG, "Not scanning, returning."); + return; + } + bleScanner.stopScan(); + Log.d(TAG, "Stopped scan."); + removeMessages(MSG_START_SCAN); + removeMessages(MSG_STOP_SCAN); + removeMessages(MSG_START_FORCED_SCAN); + } + + private void disconnectGatt() { + try { + try { + if (bleConnHelper.isConnected()) { + Log.d(TAG, "Disconnecting"); + bleConnHelper.disconnect(); + } else { + Log.d(TAG, "Not disconnecting; already disconnected"); + } + } catch (BleException e) { + Log.w(TAG, "Bluetooth exception caught while disconnecting"); + } + } finally { + sendMessageDelayed(MSG_GATT_CONNECTION_CLOSED, 0); + } + } + + void handleBleException(BleException e) { + StringWriter sw = new StringWriter(); + e.printStackTrace(new PrintWriter(sw)); + Log.w(TAG, "Got exception: " + sw, e); + totalExceptionCount.incrementAndGet(); + + int code = e.statusCode; + + if (code == BleException.CODE_MISSING_CLOCKWORK_CHARS) { + if (Build.VERSION.SDK_INT >= 28) { + Log.d(TAG, "Clockwork service characteristics are missing."); + return; + } else { + bleConnHelper.refreshGatt(); + missingClockworkCharCounter.incrementAndGet(); + return; + } + } + + disconnectExceptionCount.incrementAndGet(); + + if (code == BleException.CODE_GATT_INVALID_HANDLE + || code == BleException.CODE_GATT_READ_NOT_PERMITTED + || code == BleException.CODE_GATT_WRITE_NOT_PERMITTED + || code == BleException.CODE_INVALID_DECOMMISSION + || code == BleException.CODE_TIMEZONE_OFFSET_INVALID + || code == BleException.CODE_TIME_CHAR_INVALID) { + bleConnHelper.refreshGatt(); + incrementExceptionCounter(code); + return; + } + + if (code == BleException.CODE_TIME_SERVICE_NOT_FOUND + || code == BleException.CODE_SERVICE_NOT_FOUND) { + if (Build.VERSION.SDK_INT >= 28) { + Log.d(TAG, "Service is missing when OnServiceChanged enabled."); + } else { + bleConnHelper.refreshGatt(); + incrementExceptionCounter(code); + } + return; + } + + if (e instanceof BleTimeoutException) { + errorSampler.incrementAndGet(); + return; + } + + if (code != BleException.CODE_UNKNOWN) { + errorSampler.incrementAndGet(); + } else { + Log.w(TAG, "Unable to log unhandled exception: " + e); + } + } + + + private void incrementExceptionCounter(int code) { + switch (code) { + case BleException.CODE_GATT_INVALID_HANDLE: invalidGattHandleCounter.incrementAndGet(); break; + case BleException.CODE_GATT_READ_NOT_PERMITTED: readNotPermittedCounter.incrementAndGet(); break; + case BleException.CODE_GATT_WRITE_NOT_PERMITTED: writeNotPermittedCounter.incrementAndGet(); break; + case BleException.CODE_TIME_SERVICE_NOT_FOUND: timeServiceNotFoundCounter.incrementAndGet(); break; + case BleException.CODE_MISSING_CLOCKWORK_CHARS: missingClockworkCharCounter.incrementAndGet(); break; + case BleException.CODE_INVALID_DECOMMISSION: invalidDecommissionCounter.incrementAndGet(); break; + case BleException.CODE_SERVICE_NOT_FOUND: serviceNotFoundCounter.incrementAndGet(); break; + case BleException.CODE_TIME_CHAR_INVALID: timeCharInvalidCounter.incrementAndGet(); break; + case BleException.CODE_TIMEZONE_OFFSET_INVALID: timezoneOffsetInvalidCounter.incrementAndGet(); break; + default: + Log.w(TAG, "Failed to log exception with status code: " + code); + break; + } + } + + static final class IdleState extends BleState { + private final BleConnectionManager mgr; + + IdleState(BleConnectionManager mgr) { + this.mgr = mgr; + } + + @Override + public String getName() { return "IdleState"; } + + @Override + public boolean handleMessage(Message msg) { + return true; + } + } + + static final class ServiceOnState extends BleState { + private final BleConnectionManager mgr; + + ServiceOnState(BleConnectionManager mgr) { + this.mgr = mgr; + } + + @Override + public String getName() { return "ServiceOnState"; } + + @Override + public void onEnter() { + mgr.syncReceiverRegistration(); + ConnectionConfiguration cfg = mgr.config.get(); + if (cfg == null) return; + if (!cfg.enabled) { + mgr.sendMessage(MSG_CONNECTION_CONFIG_UPDATE); + } else if (mgr.btAdapter.getState() == BluetoothAdapter.STATE_ON) { + mgr.sendBtAdapterStateMsg(BluetoothAdapter.STATE_ON); + } + } + + @Override + public boolean handleMessage(Message msg) { + ConnectionConfiguration cfg = mgr.config.get(); + boolean enabled = cfg != null && cfg.enabled; + switch (msg.what) { + case MSG_BT_ADAPTER_STATE_CHANGED: + if (msg.arg1 == BluetoothAdapter.STATE_ON && enabled) { + mgr.transitionTo(mgr.discoveredState); + } + return true; + case MSG_CONNECTION_CONFIG_UPDATE: + mgr.syncReceiverRegistration(); + if (!enabled) { + mgr.transitionTo(mgr.errorDisconnectedState); + return true; + } + if (mgr.btAdapter.getState() == BluetoothAdapter.STATE_ON) { + mgr.transitionTo(mgr.discoveredState); + } + return true; + case MSG_DECOMMISSION_WATCH: + mgr.transitionTo(mgr.errorDisconnectedState); + return true; + default: + return mgr.handleUnhandledMessage(msg); + } + } + } + + static final class DisconnectedState extends BleState { + private final BleConnectionManager mgr; + + DisconnectedState(BleConnectionManager mgr) { + this.mgr = mgr; + } + + @Override + public String getName() { + return "DisconnectedState"; + } + + @Override + public void onEnter() { + ConnectionConfiguration cfg = mgr.config.get(); + if (cfg == null || !cfg.enabled) { + mgr.sendMessage(MSG_CONNECTION_CONFIG_UPDATE); + } else if (mgr.btAdapter.getState() == BluetoothAdapter.STATE_ON) { + mgr.sendMessage(MSG_INIT); + } else if (mgr.btAdapter.getState() == BluetoothAdapter.STATE_OFF) { + mgr.sendBtAdapterStateMsg(BluetoothAdapter.STATE_OFF); + } + } + + @Override + public boolean handleMessage(Message msg) { + return true; + } + } + + static final class ScanningState extends BleState { + private final BleConnectionManager mgr; + + ScanningState(BleConnectionManager mgr) { + this.mgr = mgr; + } + + @Override + public String getName() { + return "ScanningState"; + } + + @Override + public void onEnter() { + mgr.sendMessage(MSG_START_SCAN); + } + + @Override + public void onExit() { + mgr.stopScan(); + } + + @Override + public boolean handleMessage(Message msg) { + return true; + } + } + + static final class ConnectingState extends BleState { + private final BleConnectionManager mgr; + + ConnectingState(BleConnectionManager mgr) { + this.mgr = mgr; + } + + @Override + public String getName() { return "ConnectingState"; } + + @Override + public void onEnter() { + mgr.sendMessage(MSG_INIT); + } + + @Override + public boolean handleMessage(Message msg) { + switch (msg.what) { + case MSG_INIT: + try { + if (Build.VERSION.SDK_INT < 28) { + mgr.bleConnHelper.discoverServices(); + } + if (Build.VERSION.SDK_INT >= 28) { + Log.d(TAG, "onServiceChanged() Connection Model enabled," + + " transitioning to Connected State."); + mgr.transitionTo(mgr.connectedState); + } + } catch (BleException e) { + mgr.handleBleException(e); + mgr.transitionTo(mgr.disconnectingState); + } + return true; + + case MSG_BT_ADAPTER_STATE_CHANGED: + if (msg.arg1 == BluetoothAdapter.STATE_OFF) { + mgr.transitionTo(mgr.disconnectingState); + } + return true; + + case MSG_CONNECTION_CONFIG_UPDATE: { + ConnectionConfiguration cfg = mgr.config.get(); + if (cfg == null || !cfg.enabled) { + mgr.transitionTo(mgr.disconnectingState); + } + return true; + } + + case MSG_SERVICE_DISCOVERY_COMPLETE: + if (Build.VERSION.SDK_INT >= 28) { + Log.e(TAG, "Unexpected Services Discovered in Connecting" + + " w/ OnServiceChangedModel. Disconnecting."); + mgr.transitionTo(mgr.disconnectingState); + } else { + try { + mgr.gattHelper.updateCurrentTime(); + } catch (BleException e) { + Log.d(TAG, "Failed to update current time"); + mgr.handleBleException(e); + } + Log.d(TAG, "Companion app reset connection after service changed, returning."); + + Log.w(TAG, "Failed to setup Companion app connection. Disconnecting."); + mgr.transitionTo(mgr.disconnectingState); + } + return true; + + default: + return mgr.handleUnhandledMessage(msg); + } + } + } + + static final class ConnectedState extends BleState { + private final BleConnectionManager mgr; + + ConnectedState(BleConnectionManager mgr) { + this.mgr = mgr; + } + + @Override + public String getName() { + return "ConnectedState"; + } + + @Override + public void onEnter() { + mgr.scanAttemptCount.set(0); + mgr.sendMessage(MSG_INIT); + } + + @Override + public void onExit() {} + + @Override + public boolean handleMessage(Message msg) { + return true; + } + } + + static final class DisconnectingState extends BleState { + private final BleConnectionManager mgr; + + DisconnectingState(BleConnectionManager mgr) { + this.mgr = mgr; + } + + @Override + public String getName() { + return "DisconnectingState"; + } + + @Override + public void onEnter() { + mgr.sendMessage(MSG_INIT); + } + + @Override + public boolean handleMessage(Message msg) { + return true; + } + } + + static final class ServiceOffState extends BleState { + private final BleConnectionManager mgr; + + ServiceOffState(BleConnectionManager mgr) { + this.mgr = mgr; + } + + @Override + public String getName() { + return "ServiceOffState"; + } + + @Override + public void onEnter() { + if (mgr.isReceiverRegistered.compareAndSet(true, false)) { + mgr.context.unregisterReceiver(mgr.btStateReceiver); + } + ConnectionConfiguration cfg = mgr.config.get(); + if (cfg != null && cfg.enabled) { + mgr.sendMessage(MSG_CONNECTION_CONFIG_UPDATE); + } + } + + @Override + public boolean handleMessage(Message msg) { + return true; + } + } + + +} diff --git a/play-services-wearable/core/src/main/java/org/microg/gms/wearable/bluetooth/BleConnectionManagerInterface.java b/play-services-wearable/core/src/main/java/org/microg/gms/wearable/bluetooth/BleConnectionManagerInterface.java new file mode 100644 index 0000000000..9500e6b989 --- /dev/null +++ b/play-services-wearable/core/src/main/java/org/microg/gms/wearable/bluetooth/BleConnectionManagerInterface.java @@ -0,0 +1,9 @@ +package org.microg.gms.wearable.bluetooth; + +import com.google.android.gms.wearable.ConnectionConfiguration; + +public interface BleConnectionManagerInterface { + void updateConfiguration(ConnectionConfiguration config); + void quit(); + void quitSafely(); +} diff --git a/play-services-wearable/core/src/main/java/org/microg/gms/wearable/bluetooth/BleException.java b/play-services-wearable/core/src/main/java/org/microg/gms/wearable/bluetooth/BleException.java new file mode 100644 index 0000000000..9c3d6c734b --- /dev/null +++ b/play-services-wearable/core/src/main/java/org/microg/gms/wearable/bluetooth/BleException.java @@ -0,0 +1,32 @@ +package org.microg.gms.wearable.bluetooth; + +public class BleException extends Exception { + public static final int CODE_UNKNOWN = -1; + public static final int CODE_GATT_INVALID_HANDLE = 1; + public static final int CODE_GATT_READ_NOT_PERMITTED = 2; + public static final int CODE_GATT_WRITE_NOT_PERMITTED = 3; + + public static final int CODE_TIME_SERVICE_NOT_FOUND = 256; + public static final int CODE_MISSING_CLOCKWORK_CHARS = 258; + public static final int CODE_INVALID_DECOMMISSION = 259; + public static final int CODE_SERVICE_NOT_FOUND = 260; + public static final int CODE_TIME_CHAR_INVALID = 261; + public static final int CODE_TIMEZONE_OFFSET_INVALID = 262; + + public final int statusCode; + + public BleException(String message) { + super(message); + this.statusCode = CODE_UNKNOWN; + } + + public BleException(String message, int statusCode) { + super(message); + this.statusCode = statusCode; + } + + public BleException(Throwable cause) { + super(cause); + this.statusCode = CODE_UNKNOWN; + } +} diff --git a/play-services-wearable/core/src/main/java/org/microg/gms/wearable/bluetooth/BleScanner.java b/play-services-wearable/core/src/main/java/org/microg/gms/wearable/bluetooth/BleScanner.java new file mode 100644 index 0000000000..61e33ce0c3 --- /dev/null +++ b/play-services-wearable/core/src/main/java/org/microg/gms/wearable/bluetooth/BleScanner.java @@ -0,0 +1,12 @@ +package org.microg.gms.wearable.bluetooth; + +public interface BleScanner { + boolean isScanning(); + void stopScan(); + void startScan(String address, ScanListener listener); + + interface ScanListener { + void onDeviceFound(String address); + void onScanFailed(int errorCode); + } +} diff --git a/play-services-wearable/core/src/main/java/org/microg/gms/wearable/bluetooth/BleServicesHandler.java b/play-services-wearable/core/src/main/java/org/microg/gms/wearable/bluetooth/BleServicesHandler.java new file mode 100644 index 0000000000..02ca443312 --- /dev/null +++ b/play-services-wearable/core/src/main/java/org/microg/gms/wearable/bluetooth/BleServicesHandler.java @@ -0,0 +1,6 @@ +package org.microg.gms.wearable.bluetooth; + +public interface BleServicesHandler { + void cleanup(); + void updateCurrentTime() throws BleException; +} diff --git a/play-services-wearable/core/src/main/java/org/microg/gms/wearable/bluetooth/BleState.java b/play-services-wearable/core/src/main/java/org/microg/gms/wearable/bluetooth/BleState.java new file mode 100644 index 0000000000..9a18b9946f --- /dev/null +++ b/play-services-wearable/core/src/main/java/org/microg/gms/wearable/bluetooth/BleState.java @@ -0,0 +1,13 @@ +package org.microg.gms.wearable.bluetooth; + +import android.os.Message; + +public abstract class BleState { + public abstract String getName(); + + public void onEnter() {} + + public void onExit() {} + + public abstract boolean handleMessage(Message msg); +} diff --git a/play-services-wearable/core/src/main/java/org/microg/gms/wearable/bluetooth/BleStateMachine.java b/play-services-wearable/core/src/main/java/org/microg/gms/wearable/bluetooth/BleStateMachine.java new file mode 100644 index 0000000000..115d16b3e9 --- /dev/null +++ b/play-services-wearable/core/src/main/java/org/microg/gms/wearable/bluetooth/BleStateMachine.java @@ -0,0 +1,93 @@ +package org.microg.gms.wearable.bluetooth; + +import android.os.Handler; +import android.os.Looper; +import android.os.Message; +import android.util.Log; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +public abstract class BleStateMachine extends Handler { + private final String name; + private final Map> transitions = new HashMap<>(); + private BleState currentState; + private BleState errorState; + + protected BleStateMachine(String name, Looper looper) { + super(looper); + this.name = name; + } + + protected void addState(BleState state) { + transitions.put(state, new ArrayList<>()); + } + + protected void addTransition(BleState from, BleState to) { + List targets = transitions.get(from); + + if (targets == null) { + Log.w(name, "addTransition: unknown source state " + from.getName()); + return; + } + + if (!targets.contains(to)) { + targets.add(to); + } + } + + protected void setErrorState(BleState state){ + this.errorState = state; + } + + protected void start() { + + } + + public void transitionTo(BleState next) { + if (currentState != null) { + currentState.onExit(); + } + currentState = next; + if (currentState != null) { + currentState.onEnter(); + } + } + + public BleState currentState() { + return currentState; + } + + public void sendMessage(int what) { + sendMessage(obtainMessage(what)); + } + + public void sendMessageDelayed(int what, long delay) { + sendMessageDelayed(obtainMessage(what), delay); + } + + public void sendBtAdapterStateMsg(int state) { + Message msg = obtainMessage(BleConnectionManager.MSG_BT_ADAPTER_STATE_CHANGED); + msg.arg1 = state; + sendMessage(msg); + } + + @Override + public void handleMessage(Message msg) { + if (currentState != null && !currentState().handleMessage(msg)) { + Log.w(name, "Unhandled message " + msg.what + " in state " + currentState.getName()); + } + } + + protected String getMessageName(int what) { + return String.valueOf(what); + } + + protected boolean shouldLogMessage(Message msg) { + return true; + } + + protected void onQuiting() {} +} diff --git a/play-services-wearable/core/src/main/java/org/microg/gms/wearable/bluetooth/BleTimeoutException.java b/play-services-wearable/core/src/main/java/org/microg/gms/wearable/bluetooth/BleTimeoutException.java new file mode 100644 index 0000000000..69793f077c --- /dev/null +++ b/play-services-wearable/core/src/main/java/org/microg/gms/wearable/bluetooth/BleTimeoutException.java @@ -0,0 +1,7 @@ +package org.microg.gms.wearable.bluetooth; + +public class BleTimeoutException extends BleException { + public BleTimeoutException(String message) { + super(message); + } +} diff --git a/play-services-wearable/core/src/main/java/org/microg/gms/wearable/bluetooth/BluetoothGattHelper.java b/play-services-wearable/core/src/main/java/org/microg/gms/wearable/bluetooth/BluetoothGattHelper.java new file mode 100644 index 0000000000..fb21e12808 --- /dev/null +++ b/play-services-wearable/core/src/main/java/org/microg/gms/wearable/bluetooth/BluetoothGattHelper.java @@ -0,0 +1,10 @@ +package org.microg.gms.wearable.bluetooth; + +public interface BluetoothGattHelper { + boolean isConnected(); + + void disconnect() throws BleException; + void discoverServices() throws BleException; + void refreshGatt(); + void setGattEventListener(GattEventListener listener); +} diff --git a/play-services-wearable/core/src/main/java/org/microg/gms/wearable/bluetooth/GattEventListener.java b/play-services-wearable/core/src/main/java/org/microg/gms/wearable/bluetooth/GattEventListener.java new file mode 100644 index 0000000000..11a21a53ef --- /dev/null +++ b/play-services-wearable/core/src/main/java/org/microg/gms/wearable/bluetooth/GattEventListener.java @@ -0,0 +1,10 @@ +package org.microg.gms.wearable.bluetooth; + +import android.bluetooth.BluetoothGattCharacteristic; + +public interface GattEventListener { + void onCharacteristicChanged(BluetoothGattCharacteristic characteristic); + void onServiceChanged(); + void onCharacteristicWritten(BluetoothGattCharacteristic characteristic); + void onServicesDiscovered(); +} From c51ede6f01e58b15fe9904675b6b547d0893a2a5 Mon Sep 17 00:00:00 2001 From: deadYokai Date: Wed, 4 Mar 2026 23:35:39 +0200 Subject: [PATCH 29/29] fix Type enum in wearable.proto --- play-services-wearable/core/src/main/proto/wearable.proto | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/play-services-wearable/core/src/main/proto/wearable.proto b/play-services-wearable/core/src/main/proto/wearable.proto index c5caadf8d9..75784a2b83 100644 --- a/play-services-wearable/core/src/main/proto/wearable.proto +++ b/play-services-wearable/core/src/main/proto/wearable.proto @@ -65,8 +65,8 @@ message CapabilityFilterEntry { message ChannelControlRequest { enum Type { CHANNEL_CONTROL_OPEN = 1; - CHANNEL_CONTROL_CLOSE = 2; - CHANNEL_CONTROL_OPEN_ACK = 3; + CHANNEL_CONTROL_OPEN_ACK = 2; + CHANNEL_CONTROL_CLOSE = 3; } optional int32 type = 1;