Apple TV Login + Android

— no, really…

You’ve been watching it rush in, this tide of change in home entertainment. You were the first of your friends to cut the cord. You’ve exhausted the Netflix movie catalog, and binge-watched every Hulu and Amazon Prime Video series you could get your hands on. Always the early adopter, devices have been auditioning for the part of your living room hub for years; the Roku, gaming consoles like the Xbox and Playstation, before that a Boxee Box, TiVo, even that XBMC setup you rigged up in your dorm room. So, as you head home from the Apple store to unbox your 4th generation Apple TV and connect it to the big screen, you’re hopeful that THIS TIME, you will finally be transported to that future of mind-numbing, frictionless entertainment you’ve been promised.

Things start out great. You select Set Up with Device, turn on Bluetooth on your iPhone, and place it down next to the Apple TV. Your settings are whisked over the air, and moments later, knowing that “apps are the future of television,” you’re installing offerings from all of the usual suspects on the app store.

Except, here’s where your enthusiasm begins to wane.

Every app wants you to login, asking you to navigate to their website to get a code, or worse, to type in your username along with that secure 16-character password you’ve been generating for each service. We’ve regressed from 10-finger touch typing, to two fingers pecking away at a 5-inch screen, to now one singular thumb panning across the width of a television, hoping to hit its mark. You wonder what your trusted instructor Mavis Beacon would have to say.

You think back to the magic of the tvOS setup experience, when your settings were effortlessly migrated from your iPhone. If only those same apps you were logged in with on your phone could have informed your TV…

Ok, so maybe it’s not all that bad. Yes, users have to authenticate, but the work is front-loaded and it’s a small price to pay to get setup this one last time. Right? Well, if you are an app developer, you might be wondering how you’re going to get your company’s OAuth login flow to work without a web browser. Maybe it’s not even your company — maybe you’re a third-party client beholden to someone else’s backend.

Of course, we as developers can replicate this experience in our own apps. In fact, we have a whole host of communication technologies available to us that are shared between iOS and tvOS. Benny Wong of Timehop was possibly the first to demonstrate a prototype of this capability. Using NSNetService and CocoaAsyncSocket, one can run a Bonjour server on the tvOS side, and client on the iOS side, communicating all of the pertinent details that it means to be logged-in to your app. Later, Rizwan Sattar expanded on this demo and announced Voucher, a library to further obscure the networking details behind this interaction.

All that is to say, great — problem solved. No more complicated authentication experience on Apple TV right? We can even take this further and use Bonjour for all kinds of interesting, multi-device features (see for example, Photo Remote by Storehouse).

There is another overlooked side to this equation that we have yet to see addressed though.

Enter: Android users.

Sure, they may not be top-of-mind for developers catering to Apple’s ecosystem, but this is a communal device we’re talking about. It sits in your living room, not in your pocket. It’s not inconceivable to imagine a mixed-platform household, or a friend that comes by and wants to participate in the app experience.

So, without further ado, let’s get to the code.

Strictly speaking, Android uses Network Service Discovery to advertise and locate devices, but under the hood the implementation is compatible with what Apple calls Bonjour.

We start by setting up NsdManager callbacks:

private NsdManager mNsdManager;
private NsdManager.DiscoveryListener mDiscoveryListener;
private NsdManager.ResolveListener mResolveListener;
...
services = new ArrayList<>();
mNsdManager = (NsdManager)getSystemService(Context.NSD_SERVICE);
initializeDiscoveryListener();
initializeResolveListener();
...
public void initializeDiscoveryListener() {
mDiscoveryListener = new NsdManager.DiscoveryListener() {
  public void onDiscoveryStarted(String regType) {
Log.d(TAG, "Service discovery started");
}
  public void onServiceFound(NsdServiceInfo svc) {
Log.d(TAG, "Service found: " + svc);
    services.add(svc);
    if (callbacks != null)
callbacks.onTVFound(svc.getServiceName());
}
  public void onServiceLost(NsdServiceInfo svc) {
Log.d(TAG, "service lost: " + svc);
    services.remove(svc);
}
  public void onDiscoveryStopped(String svcType) {
Log.i(TAG, "Discovery stopped: " + svcType);
}
  public void onStartDiscoveryFailed(String svcType, int errCode) {
Log.d(TAG, "Discovery failed: Error code:" + errCode);
mNsdManager.stopServiceDiscovery(this);
AppleTVClientService.this.stopSelf();
}
  public void onStopDiscoveryFailed(String svcType, int errCode) {
Log.d(TAG, "Discovery failed: Error code:" + errCode);
mNsdManager.stopServiceDiscovery(this);
onDestroy();
}
};
}
public void initializeResolveListener() {
mResolveListener = new NsdManager.ResolveListener() {
  public void onResolveFailed(NsdServiceInfo svcInfo, int errCode) {
Log.d(TAG, "Resolve failed " + errCode);
    onDestroy();
}
  public void onServiceResolved(NsdServiceInfo svcInfo) {
Log.d(TAG, "Resolve Succeeded " + svcInfo);
    communicateWithTV(svcInfo);
}
};
}

Next, begin discovery to locate the service we’re advertising from Apple TV:

private static final String SERVICE_NAME = “_name._tcp.”;
mNsdManager.discoverServices(SERVICE_NAME, NsdManager.PROTOCOL_DNS_SD, mDiscoveryListener);

When the user indicates that they wish to login, resolve the discovered service:

for (NsdServiceInfo service : services) {
mNsdManager.resolveService(service, mResolveListener);
}

Your onServiceResolved callback can then kickoff the communication process:

private void communicateWithTV(NsdServiceInfo svcInfo) {
try {
Socket skt = new Socket(svcInfo.getHost(), svcInfo.getPort());
int headerLength = getHeaderLength(skt);
    // TODO: determine your string to send to Apple TV
String stringToSend = ...
byte[] bytesToSend = getBytesToSend(stringToSend);
sendBytes(skt, bytesToSend);
    boolean succeeded = getSucceeded(skt);
    if (callbacks != null)
callbacks.onCommunicationComplete(succeeded);

this.stopSelf();
}
catch (IOException e) {
e.printStackTrace();
Log.e(TAG, e.getMessage());
    if (callbacks != null)
callbacks.onCommunicationFailed(e.getLocalizedMessage());
    onDestroy();
}
}

The communication details will largely depend on what you’ve setup your Apple TV service to expect, but when dealing in sockets, we’ll want to send a header first to indicate the length of data to come, followed by the content. Be sure to agree on the byte order and size of the length data type on both sides — we’ve used a long, encoded in little endian in our example:

private static final ByteOrder BYTE_ORDER = ByteOrder.LITTLE_ENDIAN;
...
private void sendBytes(Socket skt, byte[] bytesToSend) throws IOException {
  OutputStream os = socket.getOutputStream();
DataOutputStream dos = new DataOutputStream(os);

int bytesToSendLength = bytesToSend.length;
  byte[] bytesToSendLengthBytes =
ByteBuffer.allocate(LONG_BYTE_COUNT)
.order(BYTE_ORDER)
.putLong(bytesToSendLength)
.array();

dos.write(bytesToSendLengthBytes);
dos.write(bytesToSend);
}

If communication succeeded, you might have your Apple TV service send back a boolean as acknowledgement:

private boolean getSucceeded(Socket socket) throws IOException {
byte[] headerBytes = new byte[LONG_BYTE_COUNT];
InputStream inputStream = socket.getInputStream();
  while (true) {
int i = inputStream.read(headerBytes);
if (LONG_BYTE_COUNT == i || i == -1)
break;
}
  ByteBuffer headerByteBuffer = ByteBuffer.wrap(headerBytes);
headerByteBuffer.order(BYTE_ORDER);
long responseSize = headerByteBuffer.getLong();
  byte[] responseBytes = new byte[(int) responseSize];
  while (true) {
int i = inputStream.read(responseBytes);
if (responseSize == i || i == -1)
break;
}
  ByteBuffer responseByteBuffer = ByteBuffer.wrap(responseBytes);
responseByteBuffer.order(BYTE_ORDER);
int response = responseByteBuffer.getShort();
  return (response == 1);
}

That’s it! You can find the full client code listing on Github.

Behance for Apple TV 1.2, now supporting sign in from both iPhone and Android devices

Check out our implementation in the latest Behance for Android & iOS, and Search for “Behance in the App Store on your Apple TV.