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/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/AndroidManifest.xml b/play-services-wearable/core/src/main/AndroidManifest.xml index 9df69b73bd..2b1e416d85 100644 --- a/play-services-wearable/core/src/main/AndroidManifest.xml +++ b/play-services-wearable/core/src/main/AndroidManifest.xml @@ -6,6 +6,14 @@ + + + + + + + + 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..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 @@ -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,16 +28,21 @@ 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 Set capabilities = new HashSet(); + private final Object lock = new Object(); + + private final Set capabilities = new HashSet(); public CapabilityManager(Context context, WearableImpl wearable, String packageName) { this.context = context; @@ -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/ConfigurationDatabaseHelper.java b/play-services-wearable/core/src/main/java/org/microg/gms/wearable/ConfigurationDatabaseHelper.java index 4aa4b58b97..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 @@ -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,145 @@ 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 ConfigurationDatabaseHelper(Context context) { - super(context, "connectionconfig.db", null, 2); + super(context, "connectionconfig.db", null, 11); } @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 +190,54 @@ 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()]); - } else { - return null; } + return new ConnectionConfiguration[0]; } - public void setEnabledState(String name, boolean enabled) { - getWritableDatabase().execSQL("UPDATE connectionConfigurations SET connectionEnabled=? WHERE name=?", new String[]{enabled ? "1" : "0", name}); + public void setEnabledState(String packageName, boolean enabled) { + 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); + } 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..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 @@ -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; @@ -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; @@ -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()); } @@ -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; } @@ -164,7 +168,7 @@ 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; 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 0f12d92edd..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 @@ -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,26 +29,36 @@ 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.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.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; +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.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"; + private static final String TAG = "WearMessageHandler"; private final WearableImpl wearable; private final String oldConfigNodeId; private String peerNodeId; @@ -59,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; @@ -143,23 +158,19 @@ 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; - wearable.sendMessageReceived(rpcRequest.packageName, messageEvent); + MessageEventParcelable messageEvent = new MessageEventParcelable(requestId, path, data, sourceNodeId); + + sendMessageReceived(rpcRequest.packageName, messageEvent); } else if (rpcRequest.targetNodeId.equals(peerNodeId)) { // Drop it } else { // TODO: find next hop } - try { - getConnection().writeMessage(new RootMessage.Builder().heartbeat(new Heartbeat()).build()); - } catch (IOException e) { - onDisconnected(); - } } @Override @@ -170,11 +181,332 @@ 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.heartbeat != null) { + Log.d(TAG, "Received heartbeat from " + sourceNodeId); + return; + } + + 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 && setAsset.appkeys.appKeys != null && + !setAsset.appkeys.appKeys.isEmpty()) { + for (AppKey appKey : setAsset.appkeys.appKeys) { + wearable.getNodeDatabase().allowAssetAccess( + setAsset.digest, + appKey.packageName, + appKey.signatureDigest + ); + } + } + + 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 + ); + } + } + } + + + 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, "Error writing file piece", e); + } + + if (finalPieceDigest == null) { + return; + } + + // 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 { + connection.writeMessage(new RootMessage.Builder() + .ackAsset(new AckAsset(digest)) + .build()); + } catch (IOException e) { + Log.w(TAG, "Failed to send asset ACK", e); + } + + 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(); + } + } + + 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/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/NodeDatabaseHelper.java b/play-services-wearable/core/src/main/java/org/microg/gms/wearable/NodeDatabaseHelper.java index 8b86fee197..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 @@ -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; @@ -35,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; @@ -46,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) { @@ -70,25 +227,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) { @@ -99,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;"); @@ -134,42 +316,110 @@ 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); - } - if (record.assetsAreReady) { - ContentValues update = new ContentValues(); - update.put("assetsPresent", 1); - db.update("dataitems", update, "_id=?", new String[]{key}); + 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()); + + 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 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.insertWithOnConflict("assetrefs", null, assetRef, + SQLiteDatabase.CONFLICT_IGNORE); + } + } + } 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.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()); + + db.insertWithOnConflict("assetrefs", null, assetRef, + SQLiteDatabase.CONFLICT_IGNORE); + } + } + + return String.valueOf(dataItemId); } private static String finishRecord(SQLiteDatabase db, String key, DataItemRecord record) { @@ -181,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; } @@ -192,26 +445,71 @@ 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) { - 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 dataitems_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, " + + "'' AS assetname, " + + "'' AS assets_digest, " + + "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 +566,34 @@ 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 DISTINCT " + + "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 " + + "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); } public boolean hasAsset(Asset asset) { @@ -281,17 +606,70 @@ 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 dataitems_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, " + + "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 ar.assets_digest = ?"; + + return db.rawQuery(query, new String[]{digest}); + } + + public synchronized void updateAssetsReady(String uri, boolean ready) { + ContentValues cv = new ContentValues(); + cv.put("assetsPresent", ready ? 1 : 0); + + 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); - SQLiteDatabase db = getWritableDatabase(); - db.update("assets", cv, "digest=?", new String[]{digest}); - 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")))}); - } - status.close(); + cv.put("timestampMs", System.currentTimeMillis()); + + getWritableDatabase().insertWithOnConflict("assets", null, cv, + SQLiteDatabase.CONFLICT_REPLACE); } + } 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..e55ac4e864 --- /dev/null +++ b/play-services-wearable/core/src/main/java/org/microg/gms/wearable/WearableConnection.java @@ -0,0 +1,160 @@ +/* + * SPDX-FileCopyrightText: 2015, microG Project Team + * SPDX-License-Identifier: Apache-2.0 + */ + +package org.microg.gms.wearable; + +import android.util.Log; + +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 final String TAG = "WearableConnection"; + + 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) { + 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); + } + + synchronized (piecesQueues) { + List queue = piecesQueues.get(piece.queueId); + + if (piece.thisPiece == 1) { + if (queue != null) { + piecesQueues.remove(piece.queueId); + } + queue = new ArrayList<>(piece.totalPieces); + queue.add(piece); + piecesQueues.put(piece.queueId, queue); + 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); + } + + 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) { + try { + listener.onMessage(this, message); + } catch (Exception e) { + Log.e(TAG, "Error processing message", e); + } + } + } catch (IOException e) { + 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); + } + } + } + + 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 1f0ed12669..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 @@ -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,31 +30,34 @@ 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; 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; 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.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.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.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; @@ -77,7 +81,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; @@ -93,6 +97,18 @@ public class WearableImpl { private CountDownLatch networkHandlerLock = new CountDownLatch(1); public Handler networkHandler; + private BluetoothClient bluetoothClient; + + 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; + + private ChannelManager channelManager; + public WearableImpl(Context context, NodeDatabaseHelper nodeDatabase, ConfigurationDatabaseHelper configDatabase) { this.context = context; this.nodeDatabase = nodeDatabase; @@ -105,8 +121,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(); } @@ -125,18 +196,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; } @@ -180,12 +257,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); @@ -194,6 +265,44 @@ private String calculateDigest(byte[] data) { } } + 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 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(); + } + + for (ConnectionConfiguration configuration : configurations) { + if (configuration.address.equals(address)) return configuration; + } + + return null; + } + public synchronized ConnectionConfiguration[] getConfigurations() { if (configurations == null) { configurations = configDatabase.getAllConfigurations(); @@ -203,7 +312,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; @@ -213,6 +323,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; } @@ -254,6 +387,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 { @@ -308,71 +445,122 @@ 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) { 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; } } - 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)); - // Fetch missing assets + + onPeerConnected(new NodeParcelable(connect.id, connect.name, 0, true)); + + 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"); + } + }, 5000); + } 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, 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 + } } + + // 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(); } - cursor.close(); } } + public void onDisconnectReceived(WearableConnection connection, Connect connect) { for (ConnectionConfiguration config : getConfigurations()) { if (config.nodeId.equals(connect.id)) { @@ -396,7 +584,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; @@ -505,23 +693,59 @@ public void removeListener(IWearableListener listener) { } } + @RequiresPermission(Manifest.permission.BLUETOOTH_CONNECT) public void enableConnection(String name) { - configDatabase.setEnabledState(name, true); + Log.d(TAG, "enableConnection: " + name); + + ConnectionConfiguration config = getConfigurationByName(name); + + configDatabase.setEnabledState(config.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(); + + 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) { + 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); + + 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); } } @@ -530,11 +754,75 @@ 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(); + + 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; + + if (configurations != null) { + ConnectionConfiguration[] newConfigs = new ConnectionConfiguration[configurations.length + 1]; + System.arraycopy(configurations, 0, newConfigs, 0, configurations.length); + newConfigs[configurations.length] = config; + configurations = newConfigs; + } + } + + @RequiresPermission(Manifest.permission.BLUETOOTH_CONNECT) + private void handleBle(ConnectionConfiguration config, boolean enabled) { + Log.w(TAG, "BLE not implemented"); + } + + private void handleNetwork(ConnectionConfiguration config, boolean enabled) { + Log.w(TAG, "Network not implemented"); + } + + 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, "Initializing BluetoothClient"); + 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.w(TAG, "Bluetooth role Server not implemented"); + } + } catch (InterruptedException e) { + Log.w(TAG, "Interrupted while handling Bluetooth", e); + } } public int deleteDataItems(Uri uri, String packageName) { @@ -545,14 +833,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; @@ -576,26 +856,35 @@ 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)) { 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"); } - 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,8 +910,15 @@ 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 { + if (channelManager != null) { + channelManager.stop(); + } this.networkHandlerLock.await(); this.networkHandler.getLooper().quit(); } catch (InterruptedException e) { @@ -639,4 +935,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 c9f3194ede..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,19 +16,67 @@ 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; import com.google.android.gms.common.internal.GetServiceRequest; import com.google.android.gms.common.internal.IGmsCallbacks; 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 { 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) + }; + + 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); } @@ -36,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(); @@ -50,6 +131,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..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 @@ -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,14 +26,31 @@ import android.util.Base64; import android.util.Log; +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; import com.google.android.gms.wearable.ConnectionConfiguration; +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.gms.wearable.proto.AppKey; + import java.io.FileNotFoundException; +import java.io.IOException; 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 { @@ -52,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 @@ -74,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, () -> { @@ -103,6 +130,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); @@ -112,6 +140,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); @@ -121,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 */ @@ -189,13 +280,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; } @@ -213,6 +310,54 @@ 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 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); @@ -228,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); } @@ -235,11 +383,17 @@ 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 public void setCloudSyncSetting(IWearableCallbacks callbacks, boolean enable) throws RemoteException { Log.d(TAG, "unimplemented Method: setCloudSyncSetting"); + + postMain(callbacks, () -> { + // dummy + callbacks.onStatus(new Status(0)); + }); } @Override @@ -250,6 +404,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 @@ -257,17 +412,137 @@ 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 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: " + + 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 { + 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)); } }); } + @Override + public void getNodeId(IWearableCallbacks callbacks, String address) throws RemoteException { + postNetwork(callbacks, () -> { + String resultNode; + ConnectionConfiguration configuration = wearable.getConfigurationByAddress(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, () -> { @@ -281,41 +556,148 @@ 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)) { + String dispName = wearable.getConfigurationByNodeId(nodeId).name; + nodes.add(new NodeParcelable(nodeId, dispName)); + } + } + + 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)){ + String dispName = wearable.getConfigurationByNodeId(nodeId).name; + nodes.add(new NodeParcelable(nodeId, dispName)); + } + } + + 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)); + } } }); } @@ -378,49 +760,293 @@ 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 public void syncWifiCredentials(IWearableCallbacks callbacks) throws RemoteException { Log.d(TAG, "unimplemented Method: syncWifiCredentials"); + + postMain(callbacks, () -> { + // dummy stuff + callbacks.onStatus(new Status(0)); + }); } /* @@ -447,6 +1073,7 @@ public void getConnection(IWearableCallbacks callbacks) throws RemoteException { }); } + @RequiresPermission(Manifest.permission.BLUETOOTH_CONNECT) @Override @Deprecated public void enableConnection(IWearableCallbacks callbacks) throws RemoteException { @@ -458,6 +1085,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/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/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 new file mode 100644 index 0000000000..19aaa45c2f --- /dev/null +++ b/play-services-wearable/core/src/main/java/org/microg/gms/wearable/bluetooth/BluetoothClient.java @@ -0,0 +1,281 @@ +package org.microg.gms.wearable.bluetooth; + +import android.Manifest; +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 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; +import java.util.concurrent.Executors; +import java.util.concurrent.ScheduledExecutorService; + +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; + + private final ScheduledExecutorService executor; + private final BleDeviceDiscoverer bleDiscoverer; + + private volatile boolean isShutdown = false; + + 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_STATE_CHANGED.equals(intent.getAction())) { + int state = intent.getIntExtra(BluetoothAdapter.EXTRA_STATE, + BluetoothAdapter.ERROR); + onBluetoothStateChanged(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); + } + } + } + }; + + this.bleDiscoverer = new BleDeviceDiscoverer(context, btAdapter); + this.executor = Executors.newScheduledThreadPool(2); + + 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, "Client is shutdown, ignoring addConfig"); + return; + } + + validateConfig(config); + + String address = config.address; + + synchronized (this) { + if (configurations.containsKey(address)) { + Log.d(TAG, "Configuration already exists for " + address + ", updating"); + + configurations.put(address, config); + + 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 { + startConnection(config); + } + + return; + } + + configurations.put(address, config); + + if (btAdapter != null && btAdapter.isEnabled()) { + startConnection(config); + } else { + Log.w(TAG, "Bluetooth disabled, deferring connection"); + } + } + } + + public void removeConfig(ConnectionConfiguration config) { + if (isShutdown) { + return; + } + + validateConfig(config); + + String address = config.address; + Log.d(TAG, "Removing configuration for " + address); + + synchronized (this) { + BluetoothConnectionThread thread = connections.get(address); + if (thread != null) { + thread.close(); + connections.remove(address); + } + + configurations.remove(address); + } + } + + private void startConnection(ConnectionConfiguration config) { + if (isShutdown) { + return; + } + + String address = config.address; + + 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"); + return; + } + + Log.d(TAG, "Starting connection for " + address); + + BluetoothConnectionThread thread = new BluetoothConnectionThread( + context, config, btAdapter, wearableImpl, executor, bleDiscoverer + ); + + connections.put(address, thread); + thread.start(); + } + } + + private void onAclConnected(BluetoothDevice device) { + String address = device.getAddress(); + + synchronized (this) { + ConnectionConfiguration config = configurations.get(address); + if (config != null) { + Log.d(TAG, "ACL_CONNECTED for configured device " + address + + ", attempting reconnection"); + + BluetoothConnectionThread thread = connections.get(address); + if (thread != null) { + thread.retryConnection(); + } else { + startConnection(config); + } + } + } + } + + 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; + } + + for (BluetoothConnectionThread thread : connections.values()) { + thread.close(); + } + connections.clear(); + } + } + } + + 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); + } + + @RequiresPermission(Manifest.permission.BLUETOOTH_SCAN) + @Override + public void close() { + if (isShutdown) { + return; + } + + Log.d(TAG, "Shutting down BluetoothClient"); + isShutdown = true; + + 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()); + } + } catch (InterruptedException e) { + Log.w(TAG, "Interrupted while waiting for thread", e); + } + } + + connections.clear(); + configurations.clear(); + } + + bleDiscoverer.shutdown(); + + executor.shutdownNow(); + + try { + context.unregisterReceiver(btStateReceiver); + } catch (Exception e) { + Log.w(TAG, "Error unregistering btStateReceiver", e); + } + + try { + context.unregisterReceiver(aclConnReceiver); + } catch (Exception 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 new file mode 100644 index 0000000000..a74fabf03b --- /dev/null +++ b/play-services-wearable/core/src/main/java/org/microg/gms/wearable/bluetooth/BluetoothConnectionThread.java @@ -0,0 +1,521 @@ +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; + +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; +import org.microg.gms.wearable.proto.RootMessage; + +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.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 long MIN_ATTEMPT_INTERVAL_MS = 3000; + 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 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 BroadcastReceiver retryReceiver; + private boolean receiverRegistered = false; + + 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; + + 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; + } + + 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}) + @Override + public void run(){ + Log.d(TAG, "Bluetooth connection thread started for " + config.address); + + while (running.get() && !isInterrupted()) { + try { + enforceMinInterval(); + + if (!running.get()) break; + + wakeLockManager.acquire("connect", SOCKET_CONNECT_TIMEOUT_MS + 5000); + + try { + 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.e(TAG, "Unexpected error", e); + } finally { + closeSocket(); + wakeLockManager.release("connect"); + } + + if (running.get() && !isInterrupted()) { + waitForRetry(); + } + + } catch (InterruptedException e) { + Log.d(TAG, "Thread interrupted"); + if (!running.get()) break; + } catch (Exception e) { + Log.e(TAG, "Unexpected error in main loop", e); + } + } + + Log.d(TAG, "Bluetooth connection thread stopped for " + config.address); + cleanup(); + } + + 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"); + } + + Log.d(TAG, "Connecting to " + config.address); + + socket = btDevice.createRfcommSocketToServiceRecord(WEAR_BT_UUID); + + if (btAdapter.isDiscovering()) { + btAdapter.cancelDiscovery(); + } + + connectSocketWithTimeout(socket); + + Log.d(TAG, "Socket connected to " + config.address); + + isConnected = true; + markActivity(); + + 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 Object connectLock = new Object(); + final IOException[] exception = new IOException[1]; + + Thread connectThread = new Thread(() -> { + try { + synchronized (connectLock) { + if (timedOut.get()) return; + } + + socket.connect(); + + synchronized (connectLock) { + if (!timedOut.get()) { + connected.set(true); + } else { + try { + socket.close(); + } catch (IOException ignored) {} + } + } + } catch (IOException e) { + synchronized (connectLock) { + if (!timedOut.get()) { + exception[0] = e; + } + } + } + }, "BtSocketConnect-" + config.address); + + connectThread.start(); + + long startTime = System.currentTimeMillis(); + long endTime = startTime + SOCKET_CONNECT_TIMEOUT_MS; + + while (System.currentTimeMillis() < endTime && running.get()) { + synchronized (connectLock) { + if (connected.get()) { + return; + } + + if (exception[0] != null) { + throw exception[0]; + } + } + + try { + Thread.sleep(100); + } catch (InterruptedException e) { + connectThread.interrupt(); + throw e; + } + } + + 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 ignored) {} + + connectThread.interrupt(); + 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 requested"); + wakeLockManager.acquire("retry", delayMs + 5000); + return; + } + + 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 { + 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()) { + retryCondition.await(); + } + immediateRetry.set(false); + } catch (InterruptedException e) { + throw new RuntimeException(e); + } finally { + lock.unlock(); + } + + wakeLockManager.acquire("external-retry", 60_000); + } + + private void signalRetry() { + lock.lock(); + try { + immediateRetry.set(true); + retryCondition.signal(); + } finally { + lock.unlock(); + } + } + + public void resetBackoffAndRetryConnection() { + Log.d(TAG, "Reset backoff and retry requested"); + retryStrategy.reset(); + signalRetry(); + } + + public void retryConnection(){ + Log.d(TAG, "Retry requested"); + signalRetry(); + } + + 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(); + } catch (IOException e) { + Log.w(TAG, "Error closing socket", e); + } + socket = null; + } + + wearableConnection = null; + } + + @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 connection thread for " + config.address); + running.set(false); + signalRetry(); + interrupt(); + } + + 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; + + private final BluetoothConnectionThread thread; + + private MessageHandler messageHandler; + + public ConnectionListener(Context context, ConnectionConfiguration config, WearableImpl wearableImpl, BluetoothConnectionThread thread) { + this.context = context; + this.config = config; + this.wearableImpl = wearableImpl; + this.thread = thread; + } + + @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(); + + 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); + } + + @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..c6b9e342cf --- /dev/null +++ b/play-services-wearable/core/src/main/java/org/microg/gms/wearable/bluetooth/BluetoothServer.java @@ -0,0 +1,340 @@ +/* + * 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.gms.wearable.WearableConnection; +import org.microg.gms.wearable.proto.RootMessage; + +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, 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..4321f33099 --- /dev/null +++ b/play-services-wearable/core/src/main/java/org/microg/gms/wearable/bluetooth/BluetoothWearableConnection.java @@ -0,0 +1,362 @@ +/* + * SPDX-FileCopyrightText: 2015, microG Project Team + * SPDX-License-Identifier: Apache-2.0 + */ + +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; +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; + +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 = 64 * 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; + + 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; + + 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; + this.is = new DataInputStream(socket.getInputStream()); + 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"); + } + } + + private boolean handshake() { + 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) + .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); + + if (isClosed.get() || timedOut.get()) { + Log.w(TAG, "Connection closed before receiving handshake response"); + return false; + } + + RootMessage incomingMessage = readMessage(); + + 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); + 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); + handshakeComplete = true; + return true; + + } catch (IOException 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); + } + } + + public String getPeerNodeId() { + return peerNodeId; + } + + public String getLocalNodeId() { + return localNodeId; + } + + public boolean isHandshakeComplete() { + return handshakeComplete; + } + + protected void writeMessagePiece(MessagePiece piece) throws IOException { + if (isClosed.get()) { + throw new IOException("Socket not connected"); + } + + byte[] bytes = MessagePiece.ADAPTER.encode(piece); + + 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(); + + startHeartbeat(); + + 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); + } 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"); + } + + 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); + } + + 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); + } + + 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); + } + } + + @Override + public void close() throws IOException { + if (isClosed.getAndSet(true)) { + Log.d(TAG, "Connection already closed"); + return; + } + + Log.d(TAG, "Closing Bluetooth wearable connection"); + + stopHeartbeat(); + + 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 (os != null) { + os.close(); + } + } catch (IOException e) { + Log.w(TAG, "Error closing output stream", e); + if (exception == null) exception = e; + } + + try { + socket.close(); + } catch (IOException e) { + Log.w(TAG, "Error closing socket", e); + if (exception == null) exception = e; + } + + if (exception != null) { + throw exception; + } + } + + 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/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 { +} 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 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..10ba47a6ff --- /dev/null +++ b/play-services-wearable/core/src/main/java/org/microg/gms/wearable/channel/ChannelManager.java @@ -0,0 +1,668 @@ +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.WearableConnection; +import org.microg.gms.wearable.WearableImpl; +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; +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; + + private volatile long cooldownUntil = 0; + + public ChannelManager(Handler handler, WearableImpl wearable, String localNodeId) { + this.handler = handler; + this.wearable = wearable; + this.localNodeId = 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); + } + + public void stop() { + isRunning.set(false); + synchronized (lock) { + for (ChannelStateMachine channel : channels.values()) { + try { + channel.clearOpenCallback(); + 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; + } + + 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) { + 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(); + Log.d(TAG, "ChannelRequest: " + channelRequest); + 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(); + Log.d(TAG, "RootMessage: " + message); + + 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..8ab96e2421 --- /dev/null +++ b/play-services-wearable/core/src/main/java/org/microg/gms/wearable/channel/ChannelStateMachine.java @@ -0,0 +1,344 @@ +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 clearOpenCallback() { + this.openCallback = null; + } + + 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..98d24f0c02 --- /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.gms.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/core/src/main/proto/wearable.proto b/play-services-wearable/core/src/main/proto/wearable.proto new file mode 100644 index 0000000000..af7f9f6f5f --- /dev/null +++ b/play-services-wearable/core/src/main/proto/wearable.proto @@ -0,0 +1,163 @@ +/* + * 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 { + // cannot find what other fields is + // maybe deprecated and + // not used anymore in new google gms + 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/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/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/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/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/IWearableCallbacks.aidl b/play-services-wearable/src/main/aidl/com/google/android/gms/wearable/internal/IWearableCallbacks.aidl index ffa91cb9e3..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,6 +26,22 @@ 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.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 void onGetConfigResponse(in GetConfigResponse response) = 1; @@ -49,6 +65,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 +79,27 @@ 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/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..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 @@ -10,14 +10,25 @@ 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; + +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; + 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; @@ -28,9 +39,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 @@ -74,6 +90,22 @@ interface IWearableService { void sendRemoteCommand(IWearableCallbacks callbacks, byte b) = 52; + 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; + 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/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/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/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/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/AcceptTermsRequest.java b/play-services-wearable/src/main/java/com/google/android/gms/wearable/internal/AcceptTermsRequest.java new file mode 100644 index 0000000000..e04bd60834 --- /dev/null +++ b/play-services-wearable/src/main/java/com/google/android/gms/wearable/internal/AcceptTermsRequest.java @@ -0,0 +1,54 @@ +/* + * 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; + +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/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/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..b6374c7598 --- /dev/null +++ b/play-services-wearable/src/main/java/com/google/android/gms/wearable/internal/BooleanResponse.java @@ -0,0 +1,55 @@ +/* + * 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; + +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/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/ConsentResponse.java b/play-services-wearable/src/main/java/com/google/android/gms/wearable/internal/ConsentResponse.java new file mode 100644 index 0000000000..a32c1d709f --- /dev/null +++ b/play-services-wearable/src/main/java/com/google/android/gms/wearable/internal/ConsentResponse.java @@ -0,0 +1,87 @@ +/* + * 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; + +import java.util.Arrays; +import java.util.List; + +public class ConsentResponse extends AutoSafeParcelable { + + @SafeParceled(1) + public int statusCode; + @SafeParceled(2) + public boolean hasTosConsent; + @SafeParceled(3) + public boolean hasLoggingConsent; + @SafeParceled(4) + public boolean hasCloudSyncConsent; + @SafeParceled(5) + public boolean hasLocationConsent; + @SafeParceled(6) + public List accountConsentRecords; + @SafeParceled(7) + public String nodeId; + @SafeParceled(8) + 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; + 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("\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(); + + } + + 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..02e7eaa028 --- /dev/null +++ b/play-services-wearable/src/main/java/com/google/android/gms/wearable/internal/ConsentStatusRequest.java @@ -0,0 +1,31 @@ +/* + * 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 ConsentStatusRequest extends AutoSafeParcelable { + @SafeParceled(1) + public String status; + + 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/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/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); } 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/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/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); } 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..2e269025c7 --- /dev/null +++ b/play-services-wearable/src/main/java/com/google/android/gms/wearable/internal/GetCompanionPackageForNodeResponse.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 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/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..e2bcb93fe3 --- /dev/null +++ b/play-services-wearable/src/main/java/com/google/android/gms/wearable/internal/GetNodeIdResponse.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 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/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 new file mode 100644 index 0000000000..1d1249f17e --- /dev/null +++ b/play-services-wearable/src/main/java/com/google/android/gms/wearable/internal/GetTermsResponse.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; + +import java.util.List; + +public class GetTermsResponse extends AutoSafeParcelable { + @SafeParceled(1) + public int statusCode; + @SafeParceled(2) + 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; + 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/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); +} 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 1c8af18b66..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 @@ -92,7 +92,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); 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); } 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/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(); } 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..111b94d5c6 --- /dev/null +++ b/play-services-wearable/src/main/java/com/google/android/gms/wearable/internal/RecordTermConsentRequest.java @@ -0,0 +1,47 @@ +/* + * 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 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); + +} 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..c4f01fe1d4 --- /dev/null +++ b/play-services-wearable/src/main/java/com/google/android/gms/wearable/internal/RpcResponse.java @@ -0,0 +1,39 @@ +/* + * 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 { + @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); +} 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");