3333 * Uses the {@link ChannelControlRequest}, {@link ChannelDataRequest}, and
3434 * {@link ChannelDataAckRequest} protocol messages to open, transfer data through,
3535 * and close channels with peer WearOS nodes.
36+ *
37+ * <h3>ParcelFileDescriptor ownership</h3>
38+ * <ul>
39+ * <li>{@link ChannelState} owns the canonical pipe ends created via
40+ * {@link ParcelFileDescriptor#createPipe()}.</li>
41+ * <li>AIDL callers receive {@link ParcelFileDescriptor#dup() duplicated} ends from
42+ * {@link #getInputStream}/{@link #getOutputStream}; closing those dups must not
43+ * tear down the canonical ends still used by the network path.</li>
44+ * <li>{@link #writeInputToFd} / {@link #readOutputFromFd} operate on dups of the
45+ * canonical ends so AutoClose streams cannot invalidate app-facing PFDs.</li>
46+ * <li>The output forwarder owns the read end of the output pipe for the lifetime of
47+ * the channel (or until EOF).</li>
48+ * </ul>
3649 */
3750public class ChannelManager {
3851 private static final String TAG = "GmsWearChannelMgr" ;
@@ -132,14 +145,19 @@ public boolean closeChannel(String token, int errorCode) {
132145 }
133146
134147 try {
135- sendChannelControl (state .nodeId , new ChannelControlRequest .Builder ()
136- .type (CONTROL_TYPE_CLOSE )
137- .channelId (channelId )
138- .fromChannelOperator (true )
139- .closeErrorCode (errorCode )
140- .build ());
148+ if (wearable != null ) {
149+ sendChannelControl (state .nodeId , new ChannelControlRequest .Builder ()
150+ .type (CONTROL_TYPE_CLOSE )
151+ .channelId (channelId )
152+ .fromChannelOperator (true )
153+ .closeErrorCode (errorCode )
154+ .build ());
155+ }
141156 } catch (IOException e ) {
142157 Log .w (TAG , "closeChannel: failed to send CLOSE for channel " + channelId , e );
158+ } catch (RuntimeException e ) {
159+ // Unit tests may construct ChannelManager without a WearableImpl.
160+ Log .w (TAG , "closeChannel: skip peer CLOSE for channel " + channelId , e );
143161 }
144162
145163 state .close ();
@@ -166,8 +184,13 @@ public ParcelFileDescriptor getInputStream(String token) {
166184 synchronized (state ) {
167185 if (state .inputPipe == null ) {
168186 state .inputPipe = ParcelFileDescriptor .createPipe ();
187+ // Keep a long-lived writer over the write-end for incoming network data.
188+ state .inputPipeWriter = new ParcelFileDescriptor .AutoCloseOutputStream (
189+ state .inputPipe [1 ]);
169190 }
170- return state .inputPipe [0 ]; // read end
191+ // Return a dup so the caller's close (or Binder FD handoff) cannot destroy
192+ // the canonical read end still owned by ChannelState.
193+ return state .inputPipe [0 ].dup ();
171194 }
172195 } catch (IOException e ) {
173196 Log .e (TAG , "getInputStream: failed to create pipe for channel " + token , e );
@@ -195,7 +218,8 @@ public ParcelFileDescriptor getOutputStream(String token) {
195218 state .outputPipe = ParcelFileDescriptor .createPipe ();
196219 startOutputForwarder (state );
197220 }
198- return state .outputPipe [1 ]; // write end for caller
221+ // Dup the write end for the AIDL caller; ChannelState keeps the canonical end.
222+ return state .outputPipe [1 ].dup ();
199223 }
200224 } catch (IOException e ) {
201225 Log .e (TAG , "getOutputStream: failed to create pipe for channel " + token , e );
@@ -224,14 +248,18 @@ public boolean writeInputToFd(String token, ParcelFileDescriptor fd) {
224248 return false ;
225249 }
226250 try {
251+ final ParcelFileDescriptor readEndDup ;
227252 synchronized (state ) {
228253 if (state .inputPipe == null ) {
229254 state .inputPipe = ParcelFileDescriptor .createPipe ();
255+ state .inputPipeWriter = new ParcelFileDescriptor .AutoCloseOutputStream (
256+ state .inputPipe [1 ]);
230257 }
258+ // Dup so AutoCloseInputStream cannot close the canonical app/network read end.
259+ readEndDup = state .inputPipe [0 ].dup ();
231260 }
232- final ParcelFileDescriptor readEnd = state .inputPipe [0 ];
233261 new Thread (() -> {
234- try (InputStream in = new ParcelFileDescriptor .AutoCloseInputStream (readEnd );
262+ try (InputStream in = new ParcelFileDescriptor .AutoCloseInputStream (readEndDup );
235263 OutputStream out = new ParcelFileDescriptor .AutoCloseOutputStream (fd )) {
236264 byte [] buf = new byte [CHUNK_SIZE ];
237265 int n ;
@@ -279,16 +307,18 @@ public boolean readOutputFromFd(String token, ParcelFileDescriptor fd,
279307 return false ;
280308 }
281309 try {
310+ final ParcelFileDescriptor writeEndDup ;
282311 synchronized (state ) {
283312 if (state .outputPipe == null ) {
284313 state .outputPipe = ParcelFileDescriptor .createPipe ();
285314 startOutputForwarder (state );
286315 }
316+ // Dup so AutoCloseOutputStream cannot close the canonical app write end.
317+ writeEndDup = state .outputPipe [1 ].dup ();
287318 }
288- final ParcelFileDescriptor writeEnd = state .outputPipe [1 ];
289319 new Thread (() -> {
290320 try (InputStream in = new ParcelFileDescriptor .AutoCloseInputStream (fd );
291- OutputStream out = new ParcelFileDescriptor .AutoCloseOutputStream (writeEnd )) {
321+ OutputStream out = new ParcelFileDescriptor .AutoCloseOutputStream (writeEndDup )) {
292322 if (startOffset > 0 ) {
293323 long skipped = in .skip (startOffset );
294324 if (skipped < startOffset ) {
@@ -461,6 +491,8 @@ private void handleIncomingData(ChannelDataRequest data) {
461491 try {
462492 if (state .inputPipe == null ) {
463493 state .inputPipe = ParcelFileDescriptor .createPipe ();
494+ }
495+ if (state .inputPipeWriter == null && state .inputPipe [1 ] != null ) {
464496 // Open a single OutputStream over the write-end PFD and keep it alive
465497 // across all chunks. Wrapping the PFD rather than its raw FileDescriptor
466498 // ensures the FD is NOT closed when the stream would otherwise be closed.
@@ -526,7 +558,15 @@ private void handleIncomingDataAck(ChannelDataAckRequest ack) {
526558 * Precondition: {@code state.outputPipe} must already be initialised.
527559 */
528560 private void startOutputForwarder (ChannelState state ) {
529- final ParcelFileDescriptor readEnd = state .outputPipe [0 ];
561+ final ParcelFileDescriptor readEnd ;
562+ try {
563+ // Forwarder holds its own dup; ChannelState retains the canonical read end.
564+ readEnd = state .outputPipe [0 ].dup ();
565+ } catch (IOException e ) {
566+ Log .e (TAG , "startOutputForwarder: failed to dup output read end for channel "
567+ + state .channelId , e );
568+ return ;
569+ }
530570 new Thread (() -> {
531571 try (InputStream in = new ParcelFileDescriptor .AutoCloseInputStream (readEnd )) {
532572 byte [] buf = new byte [CHUNK_SIZE ];
@@ -606,6 +646,9 @@ private void sendChannelControl(String targetNodeId, ChannelControlRequest ctrl)
606646
607647 private void dispatchChannelEvent (ChannelState state , int eventType ,
608648 int closeReason , int appSpecificErrorCode ) {
649+ if (wearable == null ) {
650+ return ;
651+ }
609652 ChannelEventParcelable event = new ChannelEventParcelable ();
610653 event .channel = new ChannelParcelable (state .token , state .nodeId , state .path );
611654 event .eventType = eventType ;
0 commit comments