diff --git a/app-wearos/README.md b/app-wearos/README.md index 71468026d..f43964d71 100644 --- a/app-wearos/README.md +++ b/app-wearos/README.md @@ -2,3 +2,16 @@ A companion Wear OS app for AntennaPod. Can browse episodes from the phone and remote-control the phone to start playback. Communicates with the phone via the WearOS DataLayer API using paths and serialization defined in `:net:sync:wear-interface`. + +## Communication with Phone + +`WearDataListenerService` extends `WearableListenerService` and receives all incoming messages from the phone. +Most requests are watch-initiated requests (polling regularly), rarely the phone pushes events. + +`WearDataListenerService` decodes received payloads via `WearSerializer`. +`WearDataRepository` is a singleton holding `StateFlow`s for each data type. +Composables and ViewModels collect from these flows to stay up to date. +Updated exclusively by `WearDataListenerService`. + +`WearMessageSender.send(context, path)` sends a message to the first connected phone. +Use this to trigger actions (playback control) or to request data (the phone then replies with the data on the same path). diff --git a/app/README.md b/app/README.md index 5815c7298..44a7eb2ea 100644 --- a/app/README.md +++ b/app/README.md @@ -5,3 +5,9 @@ The main application module that integrates all features and hosts app-specific The miniplayer (the collapsed player bar at the bottom of the screen) is implemented in `ExternalPlayerFragment`. It is hosted in `MainActivity` as a bottom sheet. `MainActivity` controls its visibility via `setPlayerVisible()` based on playback state. + +## Wear OS Communication (play flavor only) + +`WearListenerService` is a `WearableListenerService` that handles `DataLayer` messages from connected watches. +It responds to watch-initiated requests. +The service is kept alive by the Android framework while at least one watch is connected. diff --git a/app/src/play/java/de/danoeh/antennapod/WearListenerService.java b/app/src/play/java/de/danoeh/antennapod/WearListenerService.java index 815ad5f9c..2efc0b18b 100644 --- a/app/src/play/java/de/danoeh/antennapod/WearListenerService.java +++ b/app/src/play/java/de/danoeh/antennapod/WearListenerService.java @@ -2,7 +2,9 @@ package de.danoeh.antennapod; import android.util.Log; import com.google.android.gms.wearable.MessageEvent; +import com.google.android.gms.wearable.Node; import com.google.android.gms.wearable.Wearable; +import de.danoeh.antennapod.event.PlayerStatusEvent; import de.danoeh.antennapod.playback.service.PlaybackService; import de.danoeh.antennapod.storage.preferences.PlaybackPreferences; import io.reactivex.rxjava3.core.Completable; @@ -20,6 +22,9 @@ import de.danoeh.antennapod.net.sync.wearinterface.WearDataPaths; import de.danoeh.antennapod.net.sync.wearinterface.WearSerializer; import de.danoeh.antennapod.playback.service.PlaybackServiceStarter; import de.danoeh.antennapod.storage.database.DBReader; +import org.greenrobot.eventbus.EventBus; +import org.greenrobot.eventbus.Subscribe; +import org.greenrobot.eventbus.ThreadMode; import java.util.List; @@ -27,6 +32,34 @@ public class WearListenerService extends WearableListenerService { private static final String TAG = "WearListenerService"; private static final int MAX_ITEMS = 100; + @Override + public void onCreate() { + super.onCreate(); + EventBus.getDefault().register(this); + } + + @Override + public void onDestroy() { + super.onDestroy(); + EventBus.getDefault().unregister(this); + } + + @Subscribe(threadMode = ThreadMode.BACKGROUND) + public void onPlayerStatusEvent(PlayerStatusEvent event) { + FeedMedia media = DBReader.getFeedMedia(PlaybackPreferences.getCurrentlyPlayingFeedMediaId()); + if (media == null || media.getItem() == null) { + return; + } + boolean isPlaying = PlaybackPreferences.getCurrentPlayerStatus() == PlaybackPreferences.PLAYER_STATUS_PLAYING; + byte[] payload = WearSerializer.nowPlayingToBytes(media.getItem(), isPlaying); + Wearable.getNodeClient(this).getConnectedNodes() + .addOnSuccessListener(nodes -> { + for (Node node : nodes) { + Wearable.getMessageClient(this).sendMessage(node.getId(), WearDataPaths.NOW_PLAYING, payload); + } + }); + } + @Override public void onMessageReceived(MessageEvent event) { String path = event.getPath(); diff --git a/net/sync/wear-interface/README.md b/net/sync/wear-interface/README.md index f48b395e3..1f8e64bd9 100644 --- a/net/sync/wear-interface/README.md +++ b/net/sync/wear-interface/README.md @@ -2,3 +2,19 @@ This module contains the shared data paths and serialization logic for communication between the phone app and the Wear OS companion app. `WearDataPaths` defines message path constants; `WearSerializer` handles JSON encoding. Both `:app` and `:app-wearos` depend on this module. + +## Communication + +Most requests are watch-initiated: the watch sends a message to a path (e.g. `WearDataPaths.QUEUE`), the phone responds with data on the same path. +Rarely, the phone proactively sends a message (playback state changes). +Both directions use `Wearable.getMessageClient().sendMessage(nodeId, path, payload)`. +The actual communication is located in `:app` and `:app-wearos`, the `:net:sync:wear-interface` defines the common protocol. + +## Path constants (`WearDataPaths`) + +- `NOW_PLAYING`: current episode + playing state; pushed by phone on player status changes and returned on watch request +- `QUEUE`, `DOWNLOADS`, `EPISODES`, `SUBSCRIPTIONS`: episode/feed lists +- `PLAY_PREFIX + itemId`: watch sends to start playback of an episode on the phone +- `PAUSE`: watch sends to pause playback on the phone +- `FEED_EPISODES_PREFIX + feedId`: watch requests episodes for a specific feed +- `OPEN_ON_PHONE_PREFIX + itemId`: watch requests the phone to open an episode in the main app