/* * Copyright (C) 2009 Google Inc. All rights reserved. * * 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.apps.tvremote; import java.io.IOException; import java.net.InetAddress; import java.net.UnknownHostException; import java.util.ArrayList; import java.util.Comparator; import java.util.HashMap; import java.util.Iterator; import java.util.List; import java.util.Map; import java.util.SortedSet; import java.util.TreeSet; import android.app.Activity; import android.app.AlertDialog; import android.app.ProgressDialog; import android.content.Context; import android.content.DialogInterface; import android.content.Intent; import android.net.DhcpInfo; import android.net.wifi.WifiInfo; import android.net.wifi.WifiManager; import android.os.Bundle; import android.os.Handler; import android.os.Message; import android.provider.Settings; import android.text.InputFilter; import android.text.InputType; import android.text.TextUtils; import android.text.method.NumberKeyListener; import android.util.AttributeSet; import android.util.Log; import android.view.KeyEvent; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; import android.widget.AdapterView; import android.widget.AdapterView.OnItemClickListener; import android.widget.BaseAdapter; import android.widget.Button; import android.widget.EditText; import android.widget.LinearLayout; import android.widget.ListView; import android.widget.TextView; import android.widget.Toast; import com.google.android.apps.tvremote.util.Debug; /** * Device discovery with mDNS. */ public final class DeviceFinder extends Activity { private static final String LOG_TAG = "DeviceFinder"; /** * Request code used by wifi settings activity */ private static final int CODE_WIFI_SETTINGS = 1; private ProgressDialog progressDialog; private AlertDialog confirmationDialog; private RemoteDevice previousRemoteDevice; private List<RemoteDevice> recentlyConnectedDevices; private InetAddress broadcastAddress; private WifiManager wifiManager; private boolean active; /** * Handles used to pass data back to calling activities. */ public static final String EXTRA_REMOTE_DEVICE = "remote_device"; public static final String EXTRA_RECENTLY_CONNECTED = "recently_connected"; public DeviceFinder() { dataAdapter = new DeviceFinderListAdapter(); trackedDevices = new TrackedDevices(); } @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.device_finder_layout); previousRemoteDevice = getIntent().getParcelableExtra(EXTRA_REMOTE_DEVICE); recentlyConnectedDevices = getIntent().getParcelableArrayListExtra(EXTRA_RECENTLY_CONNECTED); broadcastHandler = new BroadcastHandler(); wifiManager = (WifiManager) getSystemService(WIFI_SERVICE); stbList = (ListView) findViewById(R.id.stb_list); stbList.setOnItemClickListener(selectHandler); stbList.setAdapter(dataAdapter); ((Button) findViewById(R.id.button_manual)).setOnClickListener( new View.OnClickListener() { public void onClick(View v) { buildManualIpDialog().show(); } }); } private void showOtherDevices() { broadcastHandler.removeMessages(DELAYED_MESSAGE); if (progressDialog.isShowing()) { progressDialog.dismiss(); } if (confirmationDialog != null && confirmationDialog.isShowing()) { confirmationDialog.dismiss(); } findViewById(R.id.device_finder).setVisibility(View.VISIBLE); } @Override protected void onStart() { super.onStart(); try { broadcastAddress = getBroadcastAddress(); } catch (IOException e) { Log.e(LOG_TAG, "Failed to get broadcast address"); setResult(RESULT_CANCELED, null); finish(); } startBroadcast(); } @Override protected void onPause() { active = false; broadcastHandler.removeMessages(DELAYED_MESSAGE); super.onPause(); } @Override protected void onResume() { super.onResume(); active = true; } @Override protected void onStop() { if (null != broadcastClient) { broadcastClient.stop(); broadcastClient = null; } super.onStop(); } @Override protected void onActivityResult(int requestCode, int resultCode, Intent data) { super.onActivityResult(requestCode, resultCode, data); Log.d(LOG_TAG, "ActivityResult: " + requestCode + ", " + resultCode); if (requestCode == CODE_WIFI_SETTINGS) { if (!isWifiAvailable()) { buildNoWifiDialog().show(); } else { startBroadcast(); } } } private OnItemClickListener selectHandler = new OnItemClickListener() { public void onItemClick(AdapterView<?> parent, View v, int position, long id) { RemoteDevice remoteDevice = (RemoteDevice) parent.getItemAtPosition( position); if (remoteDevice != null) { connectToEntry(remoteDevice); } } }; /** * Connects to the chosen entry in the list. * Finishes the activity and returns the informations on the chosen box. * * @param remoteDevice the listEntry representing the box you want to connect to */ private void connectToEntry(RemoteDevice remoteDevice) { Intent resultIntent = new Intent(); resultIntent.putExtra(EXTRA_REMOTE_DEVICE, remoteDevice); setResult(RESULT_OK, resultIntent); finish(); } private void startBroadcast() { if (!isWifiAvailable()) { buildNoWifiDialog().show(); return; } broadcastClient = new BroadcastDiscoveryClient( broadcastAddress, broadcastHandler); broadcastClientThread = new Thread(broadcastClient); broadcastClientThread.start(); Message message = DelayedMessage.BROADCAST_TIMEOUT .obtainMessage(broadcastHandler); broadcastHandler.sendMessageDelayed(message, getResources().getInteger(R.integer.broadcast_timeout)); showProgressDialog(buildBroadcastProgressDialog()); } /** * Returns an intent that starts this activity. */ public static Intent createConnectIntent(Context ctx, RemoteDevice recentlyConnected, ArrayList<RemoteDevice> recentlyConnectedList) { Intent intent = new Intent(ctx, DeviceFinder.class); intent.putExtra(EXTRA_REMOTE_DEVICE, recentlyConnected); intent.putParcelableArrayListExtra(EXTRA_RECENTLY_CONNECTED, recentlyConnectedList); return intent; } private class DeviceFinderListAdapter extends BaseAdapter { public int getCount() { return getTotalSize(); } @Override public boolean hasStableIds() { return false; } @Override public boolean areAllItemsEnabled() { return false; } @Override public boolean isEnabled(int position) { return position != trackedDevices.size(); } public Object getItem(int position) { return getRemoteDevice(position); } public long getItemId(int position) { return position; } public View getView(int position, View convertView, ViewGroup parent) { ListEntryView liv; if (position == trackedDevices.size()) { return getLayoutInflater().inflate( R.layout.device_list_separator_layout, null); } if (convertView == null || !(convertView instanceof ListEntryView)) { liv = (ListEntryView) getLayoutInflater().inflate( R.layout.device_list_item_layout, null); } else { liv = (ListEntryView) convertView; } liv.setListEntry(getRemoteDevice(position)); return liv; } private int getTotalSize() { return Debug.isDebugDevices() ? trackedDevices.size() + recentlyConnectedDevices.size() + 1 : trackedDevices.size(); } private RemoteDevice getRemoteDevice(int position) { if (position < trackedDevices.size()) { return trackedDevices.get(position); } else if (Debug.isDebugDevices()) { if (position == trackedDevices.size()) { return null; } else if (position < getTotalSize()) { return recentlyConnectedDevices.get( position - trackedDevices.size() - 1); } } return null; } } private InetAddress getBroadcastAddress() throws IOException { WifiManager wifi = (WifiManager) getSystemService(Context.WIFI_SERVICE); DhcpInfo dhcp = wifi.getDhcpInfo(); int broadcast = (dhcp.ipAddress & dhcp.netmask) | ~dhcp.netmask; byte[] quads = new byte[4]; for (int k = 0; k < 4; k++) { quads[k] = (byte) ((broadcast >> k * 8) & 0xFF); } return InetAddress.getByAddress(quads); } /** * Represents an entry in the box list. */ public static class ListEntryView extends LinearLayout { public ListEntryView(Context context, AttributeSet attrs) { super(context, attrs); myContext = context; } public ListEntryView(Context context) { super(context); myContext = context; } @Override protected void onFinishInflate() { super.onFinishInflate(); tvName = (TextView) findViewById(R.id.device_list_item_name); tvTargetAddr = (TextView) findViewById(R.id.device_list_target_addr); } private void updateContents() { if (null != tvName) { String txt = myContext.getString(R.string.unkown_tgt_name); if ((null != listEntry) && (null != listEntry.getName())) { txt = listEntry.getName(); } tvName.setText(txt); } if (null != tvTargetAddr) { String txt = myContext.getString(R.string.unkown_tgt_addr); if ((null != listEntry) && (null != listEntry.getAddress())) { txt = listEntry.getAddress().getHostAddress(); } tvTargetAddr.setText(txt); } } public RemoteDevice getListEntry() { return listEntry; } public void setListEntry(RemoteDevice listEntry) { this.listEntry = listEntry; updateContents(); } private Context myContext = null; private RemoteDevice listEntry = null; private TextView tvName = null; private TextView tvTargetAddr = null; } private final class BroadcastHandler extends Handler { /** {inheritDoc} */ @Override public void handleMessage(Message msg) { if (msg.what == DELAYED_MESSAGE) { if (!active) { return; } switch ((DelayedMessage) msg.obj) { case BROADCAST_TIMEOUT: broadcastClient.stop(); if (progressDialog.isShowing()) { progressDialog.dismiss(); } buildBroadcastTimeoutDialog().show(); break; case GTV_DEVICE_FOUND: // Check if there is previously connected remote and suggest it // for connection: RemoteDevice toConnect = null; if (previousRemoteDevice != null) { Log.d(LOG_TAG, "Previous Remote Device: " + previousRemoteDevice); toConnect = trackedDevices.findRemoteDevice(previousRemoteDevice); } if (toConnect == null) { Log.d(LOG_TAG, "No previous device found."); // No default found - suggest any device toConnect = trackedDevices.get(0); } progressDialog.dismiss(); confirmationDialog = buildConfirmationDialog(toConnect); confirmationDialog.show(); break; } } switch (msg.what) { case BROADCAST_RESPONSE: BroadcastAdvertisement advert = (BroadcastAdvertisement) msg.obj; RemoteDevice remoteDevice = new RemoteDevice(advert.getServiceName(), advert.getServiceAddress(), advert.getServicePort()); handleRemoteDeviceAdd(remoteDevice); break; } } } private void handleRemoteDeviceAdd(final RemoteDevice remoteDevice) { if (trackedDevices.add(remoteDevice)) { Log.v(LOG_TAG, "Adding new device: " + remoteDevice); // Notify data adapter and update title. dataAdapter.notifyDataSetChanged(); // Show confirmation dialog only for the first STB and only if progress // dialog is visible. if ((trackedDevices.size() == 1) && progressDialog.isShowing()) { broadcastHandler.removeMessages(DELAYED_MESSAGE); // delayed automatic adding Message message = DelayedMessage.GTV_DEVICE_FOUND .obtainMessage(broadcastHandler); broadcastHandler.sendMessageDelayed(message, getResources().getInteger(R.integer.gtv_finder_reconnect_delay)); } } } private ProgressDialog buildBroadcastProgressDialog() { String message; String networkName = getNetworkName(); if (!TextUtils.isEmpty(networkName)) { message = getString(R.string.finder_searching_with_ssid, networkName); } else { message = getString(R.string.finder_searching); } return buildProgressDialog(message, new DialogInterface.OnClickListener() { public void onClick(DialogInterface dialogInterface, int which) { broadcastHandler.removeMessages(DELAYED_MESSAGE); showOtherDevices(); } }); } private ProgressDialog buildProgressDialog(String message, DialogInterface.OnClickListener cancelListener) { ProgressDialog dialog = new ProgressDialog(this); dialog.setMessage(message); dialog.setOnKeyListener(new DialogInterface.OnKeyListener() { public boolean onKey( DialogInterface dialogInterface, int which, KeyEvent event) { if (event.getKeyCode() == KeyEvent.KEYCODE_BACK) { finish(); return true; } return false; } }); dialog.setButton(getString(R.string.finder_cancel), cancelListener); return dialog; } private AlertDialog buildConfirmationDialog(final RemoteDevice remoteDevice) { AlertDialog.Builder builder = new AlertDialog.Builder(this); View view = LayoutInflater.from(this).inflate(R.layout.device_info, null); final TextView ipTextView = (TextView) view.findViewById(R.id.device_info_ip_address); if (remoteDevice.getName() != null) { builder.setMessage(remoteDevice.getName()); } ipTextView.setText(remoteDevice.getAddress().getHostAddress()); return builder .setTitle(R.string.finder_label) .setCancelable(false) .setPositiveButton(R.string.finder_connect, new DialogInterface.OnClickListener() { public void onClick(DialogInterface dialogInterface, int id) { connectToEntry(remoteDevice); } }) .setNegativeButton( R.string.finder_add_other, new DialogInterface.OnClickListener() { public void onClick(DialogInterface dialogInterface, int id) { showOtherDevices(); } }) .create(); } private AlertDialog buildManualIpDialog() { AlertDialog.Builder builder = new AlertDialog.Builder(this); View view = LayoutInflater.from(this).inflate(R.layout.manual_ip, null); final EditText ipEditText = (EditText) view.findViewById(R.id.manual_ip_entry); ipEditText.setFilters(new InputFilter[] { new NumberKeyListener() { @Override protected char[] getAcceptedChars() { return "0123456789.:".toCharArray(); } public int getInputType() { return InputType.TYPE_CLASS_NUMBER; } } }); builder .setPositiveButton( R.string.manual_ip_connect, new DialogInterface.OnClickListener() { public void onClick(DialogInterface dialog, int which) { RemoteDevice remoteDevice = remoteDeviceFromString( ipEditText.getText().toString()); if (remoteDevice != null) { connectToEntry(remoteDevice); } else { Toast.makeText(DeviceFinder.this, getString(R.string.manual_ip_error_address), Toast.LENGTH_LONG).show(); } } }) .setNegativeButton( R.string.manual_ip_cancel, new DialogInterface.OnClickListener() { public void onClick(DialogInterface dialog, int which) { // do nothing } }) .setCancelable(true) .setTitle(R.string.manual_ip_label) .setMessage(R.string.manual_ip_entry_label) .setView(view); return builder.create(); } private AlertDialog buildBroadcastTimeoutDialog() { String message; String networkName = getNetworkName(); if (!TextUtils.isEmpty(networkName)) { message = getString(R.string.finder_no_devices_with_ssid, networkName); } else { message = getString(R.string.finder_no_devices); } return buildTimeoutDialog(message, new DialogInterface.OnClickListener() { public void onClick(DialogInterface dialogInterface, int id) { startBroadcast(); } }); } private AlertDialog buildTimeoutDialog(CharSequence message, DialogInterface.OnClickListener retryListener) { AlertDialog.Builder builder = new AlertDialog.Builder(this); return builder .setMessage(message) .setCancelable(false) .setPositiveButton(R.string.finder_wait, retryListener) .setNegativeButton( R.string.finder_cancel, new DialogInterface.OnClickListener() { public void onClick(DialogInterface dialogInterface, int id) { setResult(RESULT_CANCELED, null); finish(); } }) .create(); } private AlertDialog buildNoWifiDialog() { AlertDialog.Builder builder = new AlertDialog.Builder(this); builder.setMessage(R.string.finder_wifi_not_available); builder.setCancelable(false); builder.setPositiveButton(R.string.finder_configure, new DialogInterface.OnClickListener() { public void onClick(DialogInterface dialogInterface, int id) { Intent intent = new Intent(Settings.ACTION_WIRELESS_SETTINGS); startActivityForResult(intent, CODE_WIFI_SETTINGS); } }); builder.setNegativeButton( R.string.finder_cancel, new DialogInterface.OnClickListener() { public void onClick(DialogInterface dialogInterface, int id) { setResult(RESULT_CANCELED, null); finish(); } }); return builder.create(); } private void showProgressDialog(ProgressDialog newDialog) { if ((progressDialog != null) && progressDialog.isShowing()) { progressDialog.dismiss(); } progressDialog = newDialog; newDialog.show(); } private RemoteDevice remoteDeviceFromString(String text) { String[] ipPort = text.split(":"); int port; if (ipPort.length == 1) { port = getResources().getInteger(R.integer.manual_default_port); } else if (ipPort.length == 2) { try { port = Integer.parseInt(ipPort[1]); } catch (NumberFormatException e) { return null; } } else { return null; } try { InetAddress address = InetAddress.getByName(ipPort[0]); return new RemoteDevice( getString(R.string.manual_ip_default_box_name), address, port); } catch (UnknownHostException e) { } return null; } private ListView stbList; private final DeviceFinderListAdapter dataAdapter; private BroadcastHandler broadcastHandler; private BroadcastDiscoveryClient broadcastClient; private Thread broadcastClientThread; private TrackedDevices trackedDevices; /** * Handler message number for a service update from broadcast client. */ public static final int BROADCAST_RESPONSE = 100; /** * Handler message number for all delayed messages */ private static final int DELAYED_MESSAGE = 101; private enum DelayedMessage { BROADCAST_TIMEOUT, GTV_DEVICE_FOUND; Message obtainMessage(Handler handler) { Message message = handler.obtainMessage(DELAYED_MESSAGE); message.obj = this; return message; } } private static class TrackedDevices implements Iterable<RemoteDevice> { private final Map<InetAddress, RemoteDevice> devicesByAddress; private final SortedSet<RemoteDevice> devices; private RemoteDevice[] deviceArray; private static Comparator<RemoteDevice> COMPARATOR = new Comparator<RemoteDevice>() { public int compare(RemoteDevice remote1, RemoteDevice remote2) { int result = remote1.getName().compareToIgnoreCase(remote2.getName()); if (result != 0) { return result; } return remote1.getAddress().getHostAddress().compareTo( remote2.getAddress().getHostAddress()); } }; TrackedDevices() { devicesByAddress = new HashMap<InetAddress, RemoteDevice>(); devices = new TreeSet<RemoteDevice>(COMPARATOR); } public boolean add(RemoteDevice remoteDevice) { InetAddress address = remoteDevice.getAddress(); if (!devicesByAddress.containsKey(address)) { devicesByAddress.put(address, remoteDevice); devices.add(remoteDevice); deviceArray = null; return true; } // address? return false; } public int size() { return devices.size(); } public RemoteDevice get(int index) { return getDeviceArray()[index]; } private RemoteDevice[] getDeviceArray() { if (deviceArray == null) { deviceArray = devices.toArray(new RemoteDevice[0]); } return deviceArray; } public Iterator<RemoteDevice> iterator() { return devices.iterator(); } public RemoteDevice findRemoteDevice(RemoteDevice remoteDevice) { RemoteDevice byIp = devicesByAddress.get(remoteDevice.getAddress()); if (byIp != null && byIp.getName().equals(remoteDevice.getName())) { return byIp; } for (RemoteDevice device : devices) { Log.d(LOG_TAG, "New device: " + device); if (remoteDevice.getName().equals(device.getName())) { return device; } } return byIp; } } private boolean isWifiAvailable() { if (!wifiManager.isWifiEnabled()) { return false; } WifiInfo info = wifiManager.getConnectionInfo(); return info != null && info.getIpAddress() != 0; } private String getNetworkName() { if (!isWifiAvailable()) { return null; } WifiInfo info = wifiManager.getConnectionInfo(); return info != null ? info.getSSID() : null; } }