Actually, StreamObserver.onCompleted() is the same as ClientCall.halfClose() on client-side outbound. So to say you're done to the server just call onCompleted() on the client. You should be able to use StreamObserver (but if you like ClientCall better, that's fine, too).
Note however, after you call StreamObserver.onCompleted() you can't technically call StreamObserver.onError() if you want to cancel (say, graceful termination is taking too long). To cancel, cast the StreamObserver to ClientCallStreamObserver (or make your StreamObserver implement ClientResponseObserver) and call cancel().
"If I use onError, then the client needs to re-connect with a new Channel". Note you should be able to reuse the Channel and gRPC can reuse the connection after you call onError().