├── .classpath
├── .project
├── .settings
└── org.eclipse.core.resources.prefs
├── AndroidManifest.xml
├── README.md
├── libs
├── android-support-v4.jar
└── libGoogleAnalyticsV2.jar
├── project.properties
├── res
├── drawable-hdpi
│ └── ic_launcher.png
├── drawable-ldpi
│ └── ic_launcher.png
├── drawable-mdpi
│ └── ic_launcher.png
├── drawable-xhdpi
│ └── ic_launcher.png
├── layout
│ ├── activity_main.xml
│ ├── device_finder_layout.xml
│ ├── device_info.xml
│ ├── device_list_item_layout.xml
│ ├── device_list_separator_layout.xml
│ └── manual_ip.xml
├── menu
│ └── activity_main.xml
├── values-v11
│ └── styles.xml
├── values-v14
│ └── styles.xml
└── values
│ ├── analytics.xml
│ ├── config.xml
│ ├── strings.xml
│ └── styles.xml
└── src
└── com
├── codebutler
└── android_websockets
│ ├── HybiParser.java
│ └── WebSocketClient.java
└── entertailion
└── android
└── dial
├── Analytics.java
├── BroadcastAdvertisement.java
├── BroadcastDiscoveryClient.java
├── DialServer.java
├── HttpRequestHelper.java
├── MainActivity.java
├── ServerFinder.java
├── TrackedDialServers.java
└── Utils.java
/.classpath:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
--------------------------------------------------------------------------------
/.project:
--------------------------------------------------------------------------------
1 |
2 |
3 | Dial
4 |
5 |
6 |
7 |
8 |
9 | com.android.ide.eclipse.adt.ResourceManagerBuilder
10 |
11 |
12 |
13 |
14 | com.android.ide.eclipse.adt.PreCompilerBuilder
15 |
16 |
17 |
18 |
19 | org.eclipse.jdt.core.javabuilder
20 |
21 |
22 |
23 |
24 | com.android.ide.eclipse.adt.ApkBuilder
25 |
26 |
27 |
28 |
29 |
30 | com.android.ide.eclipse.adt.AndroidNature
31 | org.eclipse.jdt.core.javanature
32 |
33 |
34 |
--------------------------------------------------------------------------------
/.settings/org.eclipse.core.resources.prefs:
--------------------------------------------------------------------------------
1 | #Mon Jul 29 09:53:17 CDT 2013
2 | eclipse.preferences.version=1
3 | encoding/README.md=UTF-8
4 |
--------------------------------------------------------------------------------
/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
2 |
6 |
7 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
21 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
34 |
35 |
36 |
37 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | DIAL
2 | ====
3 |
4 |
5 | This is DIAL client written in Android. The DIAL protocol allows TV devices to be discovered and controlled.
6 |
7 | The current version will discover both Google TV and ChromeCast devices. The client is a proof of concept for controlling ChromeCast devices using open API's. The current code does not rely on the Google Cast SDK and the OS on the ChromeCast device does not need to be hacked.
8 | The ChromeCast device also does not need to have developer options enabled.
9 |
10 |
11 | After the DIAL servers are discovered and the user selects a particular device in the UI, an attempt is made to connect to the ChromeCast device and play a YouTube video.
12 | Most of the ChromeCast-specific logic is contained in MainActivity.onActivityResult.
13 | Operations are done via HTTP and Web Sockets. ChromeCast apps use a protocol called RAMP for media playback which is not currently supported by the client.
14 |
15 |
16 | This holds promise for being to control other aspects of the ChromeCast device using open API's. How the CromeCast device works is now better understood (especially since the low level protocol details aren't documented by Google).
17 | It is possible to remotely control the device from a third-party app. There might be other aspects of the device that might be controlled in ways the Google apps don't support.
18 | This also shows that it might be possible to develop apps that don't use the cloud based solution of the official Google Cast SDK.
19 |
20 |
21 |
22 | Other apps developed by Entertailion:
23 |
31 |
--------------------------------------------------------------------------------
/libs/android-support-v4.jar:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/NoRulesJustFeels/DIAL/cdfdd16737d45f2643928b8d4b740bb7cb9a28a2/libs/android-support-v4.jar
--------------------------------------------------------------------------------
/libs/libGoogleAnalyticsV2.jar:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/NoRulesJustFeels/DIAL/cdfdd16737d45f2643928b8d4b740bb7cb9a28a2/libs/libGoogleAnalyticsV2.jar
--------------------------------------------------------------------------------
/project.properties:
--------------------------------------------------------------------------------
1 | # This file is automatically generated by Android Tools.
2 | # Do not modify this file -- YOUR CHANGES WILL BE ERASED!
3 | #
4 | # This file must be checked in Version Control Systems.
5 | #
6 | # To customize properties used by the Ant build system edit
7 | # "ant.properties", and override values to adapt the script to your
8 | # project structure.
9 | #
10 | # To enable ProGuard to shrink and obfuscate your code, uncomment this (available properties: sdk.dir, user.home):
11 | #proguard.config=${sdk.dir}/tools/proguard/proguard-android.txt:proguard-project.txt
12 |
13 | # Project target.
14 | target=android-17
15 |
--------------------------------------------------------------------------------
/res/drawable-hdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/NoRulesJustFeels/DIAL/cdfdd16737d45f2643928b8d4b740bb7cb9a28a2/res/drawable-hdpi/ic_launcher.png
--------------------------------------------------------------------------------
/res/drawable-ldpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/NoRulesJustFeels/DIAL/cdfdd16737d45f2643928b8d4b740bb7cb9a28a2/res/drawable-ldpi/ic_launcher.png
--------------------------------------------------------------------------------
/res/drawable-mdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/NoRulesJustFeels/DIAL/cdfdd16737d45f2643928b8d4b740bb7cb9a28a2/res/drawable-mdpi/ic_launcher.png
--------------------------------------------------------------------------------
/res/drawable-xhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/NoRulesJustFeels/DIAL/cdfdd16737d45f2643928b8d4b740bb7cb9a28a2/res/drawable-xhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/res/layout/activity_main.xml:
--------------------------------------------------------------------------------
1 |
6 |
7 |
12 |
13 |
--------------------------------------------------------------------------------
/res/layout/device_finder_layout.xml:
--------------------------------------------------------------------------------
1 |
2 |
10 |
11 |
16 |
17 |
21 |
22 |
23 |
--------------------------------------------------------------------------------
/res/layout/device_info.xml:
--------------------------------------------------------------------------------
1 |
2 |
10 |
11 |
14 |
15 |
16 |
--------------------------------------------------------------------------------
/res/layout/device_list_item_layout.xml:
--------------------------------------------------------------------------------
1 |
2 |
8 |
15 |
21 |
22 |
--------------------------------------------------------------------------------
/res/layout/device_list_separator_layout.xml:
--------------------------------------------------------------------------------
1 |
2 |
7 |
15 |
16 |
--------------------------------------------------------------------------------
/res/layout/manual_ip.xml:
--------------------------------------------------------------------------------
1 |
2 |
10 |
11 |
15 |
16 |
20 |
21 |
22 |
--------------------------------------------------------------------------------
/res/menu/activity_main.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
8 |
9 |
--------------------------------------------------------------------------------
/res/values-v11/styles.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
7 |
10 |
11 |
--------------------------------------------------------------------------------
/res/values-v14/styles.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
8 |
11 |
12 |
--------------------------------------------------------------------------------
/res/values/analytics.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | UA-20992556-9
4 | true
5 | true
6 | true
7 |
--------------------------------------------------------------------------------
/res/values/config.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | 1500
4 | 9551
5 | 1500
6 | 3000
7 | 1000
8 | 10000
9 | 2000
10 | 100
11 | 10000
12 | 250
13 | 10
14 | 40
15 | 8
16 | 20
17 | 80
18 | 20
19 | 30
20 | 200
21 | 125
22 | 1
23 | 0
24 | 12/30/2012
25 | 7
26 | 0
27 | 1
28 | 5000
29 | 0
30 | 5000
31 | 100
32 | 300
33 | 20
34 | 1
35 | 24
36 | 24
37 | 15000
38 | false
39 | true
40 | 2
41 | 500
42 | Fahrenheit
43 | 24
44 | 4
45 |
46 |
--------------------------------------------------------------------------------
/res/values/strings.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | Dial Client
5 | Unknown
6 | unknown address
7 | OK
8 |
9 |
10 | Connect to DIAL Server
11 |
12 |
13 | Switch DIAL Server
14 | About
15 |
16 |
17 | Unknown
18 |
19 |
20 | Searching for DIAL Server devices on your network.
21 | Searching for DIAL Server devices on %s network.
22 | Add other DIAL Server
23 | Connect
24 | Cancel
25 | Add DIAL Server manually
26 | Configure
27 | No DIAL Server devices found
28 | No DIAL Server devices found on %s
29 | WiFi network not available
30 | Recently connected devices
31 | Connected to \"%s\"
32 |
33 |
34 | Add DIAL Server
35 | IP Address
36 | xxx.xxx.xxx.xxx
37 | You can find IP address of your DIAL Server device by going to:\nSettings > Network > Network Information
38 | Invalid address.
39 | Unknown DIAL Server
40 | Connect
41 | Cancel
42 |
43 |
44 |
--------------------------------------------------------------------------------
/res/values/styles.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
7 |
14 |
15 |
16 |
19 |
20 |
--------------------------------------------------------------------------------
/src/com/codebutler/android_websockets/HybiParser.java:
--------------------------------------------------------------------------------
1 | //
2 | // HybiParser.java: draft-ietf-hybi-thewebsocketprotocol-13 parser
3 | //
4 | // Based on code from the faye project.
5 | // https://github.com/faye/faye-websocket-node
6 | // Copyright (c) 2009-2012 James Coglan
7 | //
8 | // Ported from Javascript to Java by Eric Butler
9 | //
10 | // (The MIT License)
11 | //
12 | // Permission is hereby granted, free of charge, to any person obtaining
13 | // a copy of this software and associated documentation files (the
14 | // "Software"), to deal in the Software without restriction, including
15 | // without limitation the rights to use, copy, modify, merge, publish,
16 | // distribute, sublicense, and/or sell copies of the Software, and to
17 | // permit persons to whom the Software is furnished to do so, subject to
18 | // the following conditions:
19 | //
20 | // The above copyright notice and this permission notice shall be
21 | // included in all copies or substantial portions of the Software.
22 | //
23 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
24 | // EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
25 | // MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
26 | // NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
27 | // LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
28 | // OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
29 | // WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
30 |
31 | // https://github.com/koush/android-websockets
32 |
33 | package com.codebutler.android_websockets;
34 |
35 | import android.util.Log;
36 |
37 | import java.io.*;
38 | import java.util.Arrays;
39 | import java.util.List;
40 |
41 | public class HybiParser {
42 | private static final String TAG = "HybiParser";
43 |
44 | private WebSocketClient mClient;
45 |
46 | private boolean mMasking = true;
47 |
48 | private int mStage;
49 |
50 | private boolean mFinal;
51 | private boolean mMasked;
52 | private int mOpcode;
53 | private int mLengthSize;
54 | private int mLength;
55 | private int mMode;
56 |
57 | private byte[] mMask = new byte[0];
58 | private byte[] mPayload = new byte[0];
59 |
60 | private boolean mClosed = false;
61 |
62 | private ByteArrayOutputStream mBuffer = new ByteArrayOutputStream();
63 |
64 | private static final int BYTE = 255;
65 | private static final int FIN = 128;
66 | private static final int MASK = 128;
67 | private static final int RSV1 = 64;
68 | private static final int RSV2 = 32;
69 | private static final int RSV3 = 16;
70 | private static final int OPCODE = 15;
71 | private static final int LENGTH = 127;
72 |
73 | private static final int MODE_TEXT = 1;
74 | private static final int MODE_BINARY = 2;
75 |
76 | private static final int OP_CONTINUATION = 0;
77 | private static final int OP_TEXT = 1;
78 | private static final int OP_BINARY = 2;
79 | private static final int OP_CLOSE = 8;
80 | private static final int OP_PING = 9;
81 | private static final int OP_PONG = 10;
82 |
83 | private static final List OPCODES = Arrays.asList(
84 | OP_CONTINUATION,
85 | OP_TEXT,
86 | OP_BINARY,
87 | OP_CLOSE,
88 | OP_PING,
89 | OP_PONG
90 | );
91 |
92 | private static final List FRAGMENTED_OPCODES = Arrays.asList(
93 | OP_CONTINUATION, OP_TEXT, OP_BINARY
94 | );
95 |
96 | public HybiParser(WebSocketClient client) {
97 | mClient = client;
98 | }
99 |
100 | private static byte[] mask(byte[] payload, byte[] mask, int offset) {
101 | if (mask.length == 0) return payload;
102 |
103 | for (int i = 0; i < payload.length - offset; i++) {
104 | payload[offset + i] = (byte) (payload[offset + i] ^ mask[i % 4]);
105 | }
106 | return payload;
107 | }
108 |
109 | public void start(HappyDataInputStream stream) throws IOException {
110 | while (true) {
111 | if (stream.available() == -1) break;
112 | switch (mStage) {
113 | case 0:
114 | parseOpcode(stream.readByte());
115 | break;
116 | case 1:
117 | parseLength(stream.readByte());
118 | break;
119 | case 2:
120 | parseExtendedLength(stream.readBytes(mLengthSize));
121 | break;
122 | case 3:
123 | mMask = stream.readBytes(4);
124 | mStage = 4;
125 | break;
126 | case 4:
127 | mPayload = stream.readBytes(mLength);
128 | emitFrame();
129 | mStage = 0;
130 | break;
131 | }
132 | }
133 | mClient.getListener().onDisconnect(0, "EOF");
134 | }
135 |
136 | private void parseOpcode(byte data) throws ProtocolError {
137 | boolean rsv1 = (data & RSV1) == RSV1;
138 | boolean rsv2 = (data & RSV2) == RSV2;
139 | boolean rsv3 = (data & RSV3) == RSV3;
140 |
141 | if (rsv1 || rsv2 || rsv3) {
142 | throw new ProtocolError("RSV not zero");
143 | }
144 |
145 | mFinal = (data & FIN) == FIN;
146 | mOpcode = (data & OPCODE);
147 | mMask = new byte[0];
148 | mPayload = new byte[0];
149 |
150 | if (!OPCODES.contains(mOpcode)) {
151 | throw new ProtocolError("Bad opcode");
152 | }
153 |
154 | if (!FRAGMENTED_OPCODES.contains(mOpcode) && !mFinal) {
155 | throw new ProtocolError("Expected non-final packet");
156 | }
157 |
158 | mStage = 1;
159 | }
160 |
161 | private void parseLength(byte data) {
162 | mMasked = (data & MASK) == MASK;
163 | mLength = (data & LENGTH);
164 |
165 | if (mLength >= 0 && mLength <= 125) {
166 | mStage = mMasked ? 3 : 4;
167 | } else {
168 | mLengthSize = (mLength == 126) ? 2 : 8;
169 | mStage = 2;
170 | }
171 | }
172 |
173 | private void parseExtendedLength(byte[] buffer) throws ProtocolError {
174 | mLength = getInteger(buffer);
175 | mStage = mMasked ? 3 : 4;
176 | }
177 |
178 | public byte[] frame(String data) {
179 | return frame(data, OP_TEXT, -1);
180 | }
181 |
182 | public byte[] frame(byte[] data) {
183 | return frame(data, OP_BINARY, -1);
184 | }
185 |
186 | private byte[] frame(byte[] data, int opcode, int errorCode) {
187 | return frame((Object)data, opcode, errorCode);
188 | }
189 |
190 | private byte[] frame(String data, int opcode, int errorCode) {
191 | return frame((Object)data, opcode, errorCode);
192 | }
193 |
194 | private byte[] frame(Object data, int opcode, int errorCode) {
195 | if (mClosed) return null;
196 |
197 | Log.d(TAG, "Creating frame for: " + data + " op: " + opcode + " err: " + errorCode);
198 |
199 | byte[] buffer = (data instanceof String) ? decode((String) data) : (byte[]) data;
200 | int insert = (errorCode > 0) ? 2 : 0;
201 | int length = buffer.length + insert;
202 | int header = (length <= 125) ? 2 : (length <= 65535 ? 4 : 10);
203 | int offset = header + (mMasking ? 4 : 0);
204 | int masked = mMasking ? MASK : 0;
205 | byte[] frame = new byte[length + offset];
206 |
207 | frame[0] = (byte) ((byte)FIN | (byte)opcode);
208 |
209 | if (length <= 125) {
210 | frame[1] = (byte) (masked | length);
211 | } else if (length <= 65535) {
212 | frame[1] = (byte) (masked | 126);
213 | frame[2] = (byte) Math.floor(length / 256);
214 | frame[3] = (byte) (length & BYTE);
215 | } else {
216 | frame[1] = (byte) (masked | 127);
217 | frame[2] = (byte) (((int) Math.floor(length / Math.pow(2, 56))) & BYTE);
218 | frame[3] = (byte) (((int) Math.floor(length / Math.pow(2, 48))) & BYTE);
219 | frame[4] = (byte) (((int) Math.floor(length / Math.pow(2, 40))) & BYTE);
220 | frame[5] = (byte) (((int) Math.floor(length / Math.pow(2, 32))) & BYTE);
221 | frame[6] = (byte) (((int) Math.floor(length / Math.pow(2, 24))) & BYTE);
222 | frame[7] = (byte) (((int) Math.floor(length / Math.pow(2, 16))) & BYTE);
223 | frame[8] = (byte) (((int) Math.floor(length / Math.pow(2, 8))) & BYTE);
224 | frame[9] = (byte) (length & BYTE);
225 | }
226 |
227 | if (errorCode > 0) {
228 | frame[offset] = (byte) (((int) Math.floor(errorCode / 256)) & BYTE);
229 | frame[offset+1] = (byte) (errorCode & BYTE);
230 | }
231 | System.arraycopy(buffer, 0, frame, offset + insert, buffer.length);
232 |
233 | if (mMasking) {
234 | byte[] mask = {
235 | (byte) Math.floor(Math.random() * 256), (byte) Math.floor(Math.random() * 256),
236 | (byte) Math.floor(Math.random() * 256), (byte) Math.floor(Math.random() * 256)
237 | };
238 | System.arraycopy(mask, 0, frame, header, mask.length);
239 | mask(frame, mask, offset);
240 | }
241 |
242 | return frame;
243 | }
244 |
245 | public void ping(String message) {
246 | mClient.send(frame(message, OP_PING, -1));
247 | }
248 |
249 | public void close(int code, String reason) {
250 | if (mClosed) return;
251 | mClient.send(frame(reason, OP_CLOSE, code));
252 | mClosed = true;
253 | }
254 |
255 | private void emitFrame() throws IOException {
256 | byte[] payload = mask(mPayload, mMask, 0);
257 | int opcode = mOpcode;
258 |
259 | if (opcode == OP_CONTINUATION) {
260 | if (mMode == 0) {
261 | throw new ProtocolError("Mode was not set.");
262 | }
263 | mBuffer.write(payload);
264 | if (mFinal) {
265 | byte[] message = mBuffer.toByteArray();
266 | if (mMode == MODE_TEXT) {
267 | mClient.getListener().onMessage(encode(message));
268 | } else {
269 | mClient.getListener().onMessage(message);
270 | }
271 | reset();
272 | }
273 |
274 | } else if (opcode == OP_TEXT) {
275 | if (mFinal) {
276 | String messageText = encode(payload);
277 | mClient.getListener().onMessage(messageText);
278 | } else {
279 | mMode = MODE_TEXT;
280 | mBuffer.write(payload);
281 | }
282 |
283 | } else if (opcode == OP_BINARY) {
284 | if (mFinal) {
285 | mClient.getListener().onMessage(payload);
286 | } else {
287 | mMode = MODE_BINARY;
288 | mBuffer.write(payload);
289 | }
290 |
291 | } else if (opcode == OP_CLOSE) {
292 | int code = (payload.length >= 2) ? 256 * payload[0] + payload[1] : 0;
293 | String reason = (payload.length > 2) ? encode(slice(payload, 2)) : null;
294 | Log.d(TAG, "Got close op! " + code + " " + reason);
295 | mClient.getListener().onDisconnect(code, reason);
296 |
297 | } else if (opcode == OP_PING) {
298 | if (payload.length > 125) { throw new ProtocolError("Ping payload too large"); }
299 | Log.d(TAG, "Sending pong!!");
300 | mClient.sendFrame(frame(payload, OP_PONG, -1));
301 |
302 | } else if (opcode == OP_PONG) {
303 | String message = encode(payload);
304 | // FIXME: Fire callback...
305 | Log.d(TAG, "Got pong! " + message);
306 | }
307 | }
308 |
309 | private void reset() {
310 | mMode = 0;
311 | mBuffer.reset();
312 | }
313 |
314 | private String encode(byte[] buffer) {
315 | try {
316 | return new String(buffer, "UTF-8");
317 | } catch (UnsupportedEncodingException e) {
318 | throw new RuntimeException(e);
319 | }
320 | }
321 |
322 | private byte[] decode(String string) {
323 | try {
324 | return (string).getBytes("UTF-8");
325 | } catch (UnsupportedEncodingException e) {
326 | throw new RuntimeException(e);
327 | }
328 | }
329 |
330 | private int getInteger(byte[] bytes) throws ProtocolError {
331 | long i = byteArrayToLong(bytes, 0, bytes.length);
332 | if (i < 0 || i > Integer.MAX_VALUE) {
333 | throw new ProtocolError("Bad integer: " + i);
334 | }
335 | return (int) i;
336 | }
337 |
338 | /**
339 | * Copied from AOSP Arrays.java.
340 | */
341 | /**
342 | * Copies elements from {@code original} into a new array, from indexes start (inclusive) to
343 | * end (exclusive). The original order of elements is preserved.
344 | * If {@code end} is greater than {@code original.length}, the result is padded
345 | * with the value {@code (byte) 0}.
346 | *
347 | * @param original the original array
348 | * @param start the start index, inclusive
349 | * @param end the end index, exclusive
350 | * @return the new array
351 | * @throws ArrayIndexOutOfBoundsException if {@code start < 0 || start > original.length}
352 | * @throws IllegalArgumentException if {@code start > end}
353 | * @throws NullPointerException if {@code original == null}
354 | * @since 1.6
355 | */
356 | private static byte[] copyOfRange(byte[] original, int start, int end) {
357 | if (start > end) {
358 | throw new IllegalArgumentException();
359 | }
360 | int originalLength = original.length;
361 | if (start < 0 || start > originalLength) {
362 | throw new ArrayIndexOutOfBoundsException();
363 | }
364 | int resultLength = end - start;
365 | int copyLength = Math.min(resultLength, originalLength - start);
366 | byte[] result = new byte[resultLength];
367 | System.arraycopy(original, start, result, 0, copyLength);
368 | return result;
369 | }
370 |
371 | private byte[] slice(byte[] array, int start) {
372 | return copyOfRange(array, start, array.length);
373 | }
374 |
375 | public static class ProtocolError extends IOException {
376 | public ProtocolError(String detailMessage) {
377 | super(detailMessage);
378 | }
379 | }
380 |
381 | private static long byteArrayToLong(byte[] b, int offset, int length) {
382 | if (b.length < length)
383 | throw new IllegalArgumentException("length must be less than or equal to b.length");
384 |
385 | long value = 0;
386 | for (int i = 0; i < length; i++) {
387 | int shift = (length - 1 - i) * 8;
388 | value += (b[i + offset] & 0x000000FF) << shift;
389 | }
390 | return value;
391 | }
392 |
393 | public static class HappyDataInputStream extends DataInputStream {
394 | public HappyDataInputStream(InputStream in) {
395 | super(in);
396 | }
397 |
398 | public byte[] readBytes(int length) throws IOException {
399 | byte[] buffer = new byte[length];
400 |
401 | int total = 0;
402 |
403 | while (total < length) {
404 | int count = read(buffer, total, length - total);
405 | if (count == -1) {
406 | break;
407 | }
408 | total += count;
409 | }
410 |
411 | if (total != length) {
412 | throw new IOException(String.format("Read wrong number of bytes. Got: %s, Expected: %s.", total, length));
413 | }
414 |
415 | return buffer;
416 | }
417 | }
418 | }
--------------------------------------------------------------------------------
/src/com/codebutler/android_websockets/WebSocketClient.java:
--------------------------------------------------------------------------------
1 | /*
2 | Copyright (c) 2009-2012 James Coglan
3 | Copyright (c) 2012 Eric Butler
4 | Copyright (c) 2012 Koushik Dutta
5 |
6 | Permission is hereby granted, free of charge, to any person obtaining a copy of
7 | this software and associated documentation files (the 'Software'), to deal in
8 | the Software without restriction, including without limitation the rights to use,
9 | copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
10 | Software, and to permit persons to whom the Software is furnished to do so,
11 | subject to the following conditions:
12 |
13 | The above copyright notice and this permission notice shall be included in all
14 | copies or substantial portions of the Software.
15 |
16 | THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
18 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
19 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
20 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
21 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
22 | */
23 |
24 | // https://github.com/koush/android-websockets
25 |
26 | package com.codebutler.android_websockets;
27 |
28 | import android.os.Handler;
29 | import android.os.HandlerThread;
30 | import android.text.TextUtils;
31 | import android.util.Base64;
32 | import android.util.Log;
33 | import org.apache.http.*;
34 | import org.apache.http.client.HttpResponseException;
35 | import org.apache.http.message.BasicLineParser;
36 | import org.apache.http.message.BasicNameValuePair;
37 |
38 | import javax.net.SocketFactory;
39 | import javax.net.ssl.SSLContext;
40 | import javax.net.ssl.SSLException;
41 | import javax.net.ssl.SSLSocketFactory;
42 | import javax.net.ssl.TrustManager;
43 | import java.io.EOFException;
44 | import java.io.IOException;
45 | import java.io.OutputStream;
46 | import java.io.PrintWriter;
47 | import java.net.Socket;
48 | import java.net.URI;
49 | import java.security.KeyManagementException;
50 | import java.security.NoSuchAlgorithmException;
51 | import java.util.List;
52 |
53 | public class WebSocketClient {
54 | private static final String TAG = "WebSocketClient";
55 |
56 | private URI mURI;
57 | private Listener mListener;
58 | private Socket mSocket;
59 | private Thread mThread;
60 | private HandlerThread mHandlerThread;
61 | private Handler mHandler;
62 | private List mExtraHeaders;
63 | private HybiParser mParser;
64 | private boolean mConnected;
65 |
66 | private final Object mSendLock = new Object();
67 |
68 | private static TrustManager[] sTrustManagers;
69 |
70 | public static void setTrustManagers(TrustManager[] tm) {
71 | sTrustManagers = tm;
72 | }
73 |
74 | public WebSocketClient(URI uri, Listener listener, List extraHeaders) {
75 | mURI = uri;
76 | mListener = listener;
77 | mExtraHeaders = extraHeaders;
78 | mConnected = false;
79 | mParser = new HybiParser(this);
80 |
81 | mHandlerThread = new HandlerThread("websocket-thread");
82 | mHandlerThread.start();
83 | mHandler = new Handler(mHandlerThread.getLooper());
84 | }
85 |
86 | public Listener getListener() {
87 | return mListener;
88 | }
89 |
90 | public void connect() {
91 | if (mThread != null && mThread.isAlive()) {
92 | return;
93 | }
94 |
95 | mThread = new Thread(new Runnable() {
96 | @Override
97 | public void run() {
98 | try {
99 | int port = (mURI.getPort() != -1) ? mURI.getPort() : ((mURI.getScheme().equals("wss") || mURI.getScheme().equals("https")) ? 443 : 80);
100 |
101 | String path = TextUtils.isEmpty(mURI.getPath()) ? "/" : mURI.getPath();
102 | if (!TextUtils.isEmpty(mURI.getQuery())) {
103 | path += "?" + mURI.getQuery();
104 | }
105 |
106 | String originScheme = mURI.getScheme().equals("wss") ? "https" : "http";
107 | URI origin = new URI(originScheme, "//" + mURI.getHost(), null);
108 |
109 | SocketFactory factory = (mURI.getScheme().equals("wss") || mURI.getScheme().equals("https")) ? getSSLSocketFactory() : SocketFactory.getDefault();
110 | mSocket = factory.createSocket(mURI.getHost(), port);
111 |
112 | PrintWriter out = new PrintWriter(mSocket.getOutputStream());
113 | out.print("GET " + path + " HTTP/1.1\r\n");
114 | out.print("Upgrade: websocket\r\n");
115 | out.print("Connection: Upgrade\r\n");
116 | out.print("Host: " + mURI.getHost() + "\r\n");
117 | out.print("Origin: " + origin.toString() + "\r\n");
118 | out.print("Sec-WebSocket-Key: " + createSecret() + "\r\n");
119 | out.print("Sec-WebSocket-Version: 13\r\n");
120 | if (mExtraHeaders != null) {
121 | for (NameValuePair pair : mExtraHeaders) {
122 | out.print(String.format("%s: %s\r\n", pair.getName(), pair.getValue()));
123 | }
124 | }
125 | out.print("\r\n");
126 | out.flush();
127 |
128 | HybiParser.HappyDataInputStream stream = new HybiParser.HappyDataInputStream(mSocket.getInputStream());
129 |
130 | // Read HTTP response status line.
131 | StatusLine statusLine = parseStatusLine(readLine(stream));
132 | if (statusLine == null) {
133 | throw new HttpException("Received no reply from server.");
134 | } else if (statusLine.getStatusCode() != HttpStatus.SC_SWITCHING_PROTOCOLS) {
135 | throw new HttpResponseException(statusLine.getStatusCode(), statusLine.getReasonPhrase());
136 | }
137 |
138 | // Read HTTP response headers.
139 | String line;
140 | while (!TextUtils.isEmpty(line = readLine(stream))) {
141 | Header header = parseHeader(line);
142 | if (header.getName().equals("Sec-WebSocket-Accept")) {
143 | // FIXME: Verify the response...
144 | }
145 | }
146 |
147 | mListener.onConnect();
148 |
149 | mConnected = true;
150 |
151 | // Now decode websocket frames.
152 | mParser.start(stream);
153 |
154 | } catch (EOFException ex) {
155 | Log.d(TAG, "WebSocket EOF!", ex);
156 | mListener.onDisconnect(0, "EOF");
157 | mConnected = false;
158 |
159 | } catch (SSLException ex) {
160 | // Connection reset by peer
161 | Log.d(TAG, "Websocket SSL error!", ex);
162 | mListener.onDisconnect(0, "SSL");
163 | mConnected = false;
164 |
165 | } catch (Exception ex) {
166 | mListener.onError(ex);
167 | }
168 | }
169 | });
170 | mThread.start();
171 | }
172 |
173 | public void disconnect() {
174 | if (mSocket != null) {
175 | mHandler.post(new Runnable() {
176 | @Override
177 | public void run() {
178 | if (mSocket != null) {
179 | try {
180 | mSocket.close();
181 | } catch (IOException ex) {
182 | Log.d(TAG, "Error while disconnecting", ex);
183 | mListener.onError(ex);
184 | }
185 | mSocket = null;
186 | }
187 | mConnected = false;
188 | }
189 | });
190 | }
191 | }
192 |
193 | public void send(String data) {
194 | sendFrame(mParser.frame(data));
195 | }
196 |
197 | public void send(byte[] data) {
198 | sendFrame(mParser.frame(data));
199 | }
200 |
201 | public boolean isConnected() {
202 | return mConnected;
203 | }
204 |
205 | private StatusLine parseStatusLine(String line) {
206 | if (TextUtils.isEmpty(line)) {
207 | return null;
208 | }
209 | return BasicLineParser.parseStatusLine(line, new BasicLineParser());
210 | }
211 |
212 | private Header parseHeader(String line) {
213 | return BasicLineParser.parseHeader(line, new BasicLineParser());
214 | }
215 |
216 | // Can't use BufferedReader because it buffers past the HTTP data.
217 | private String readLine(HybiParser.HappyDataInputStream reader) throws IOException {
218 | int readChar = reader.read();
219 | if (readChar == -1) {
220 | return null;
221 | }
222 | StringBuilder string = new StringBuilder("");
223 | while (readChar != '\n') {
224 | if (readChar != '\r') {
225 | string.append((char) readChar);
226 | }
227 |
228 | readChar = reader.read();
229 | if (readChar == -1) {
230 | return null;
231 | }
232 | }
233 | return string.toString();
234 | }
235 |
236 | private String createSecret() {
237 | byte[] nonce = new byte[16];
238 | for (int i = 0; i < 16; i++) {
239 | nonce[i] = (byte) (Math.random() * 256);
240 | }
241 | return Base64.encodeToString(nonce, Base64.DEFAULT).trim();
242 | }
243 |
244 | void sendFrame(final byte[] frame) {
245 | mHandler.post(new Runnable() {
246 | @Override
247 | public void run() {
248 | try {
249 | synchronized (mSendLock) {
250 | OutputStream outputStream = mSocket.getOutputStream();
251 | outputStream.write(frame);
252 | outputStream.flush();
253 | }
254 | } catch (IOException e) {
255 | mListener.onError(e);
256 | }
257 | }
258 | });
259 | }
260 |
261 | public interface Listener {
262 | public void onConnect();
263 | public void onMessage(String message);
264 | public void onMessage(byte[] data);
265 | public void onDisconnect(int code, String reason);
266 | public void onError(Exception error);
267 | }
268 |
269 | private SSLSocketFactory getSSLSocketFactory() throws NoSuchAlgorithmException, KeyManagementException {
270 | SSLContext context = SSLContext.getInstance("TLS");
271 | context.init(null, sTrustManagers, null);
272 | return context.getSocketFactory();
273 | }
274 | }
--------------------------------------------------------------------------------
/src/com/entertailion/android/dial/Analytics.java:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright (C) 2013 ENTERTAILION, LLC.
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
5 | * in compliance with the License. You may obtain a copy of the License at
6 | *
7 | * http://www.apache.org/licenses/LICENSE-2.0
8 | *
9 | * Unless required by applicable law or agreed to in writing, software distributed under the License
10 | * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
11 | * or implied. See the License for the specific language governing permissions and limitations under
12 | * the License.
13 | */
14 | package com.entertailion.android.dial;
15 |
16 | import java.util.Map;
17 |
18 | import android.app.Activity;
19 | import android.content.Context;
20 | import android.util.Log;
21 |
22 | import com.google.analytics.tracking.android.EasyTracker;
23 |
24 | public class Analytics {
25 | private static final String LOG_CAT = "Analytics";
26 |
27 | public static final String ANALYTICS = "Analytics";
28 |
29 | private static Context context;
30 |
31 | public static void createAnalytics(Context context) {
32 | try {
33 | Analytics.context = context;
34 | EasyTracker.getInstance().setContext(context);
35 | } catch (Exception e) {
36 | Log.e(LOG_CAT, "createAnalytics", e);
37 | }
38 | }
39 |
40 | public static void startAnalytics(final Activity activity) {
41 | try {
42 | if (activity != null && activity.getResources().getInteger(R.integer.development) == 0) {
43 | EasyTracker.getInstance().activityStart(activity);
44 | }
45 | } catch (Exception e) {
46 | Log.e(LOG_CAT, "startAnalytics", e);
47 | }
48 | }
49 |
50 | public static void stopAnalytics(Activity activity) {
51 | try {
52 | if (activity != null && activity.getResources().getInteger(R.integer.development) == 0) {
53 | EasyTracker.getInstance().activityStop(activity);
54 | }
55 | } catch (Exception e) {
56 | Log.e(LOG_CAT, "stopAnalytics", e);
57 | }
58 | }
59 |
60 | public static void logEvent(String event) {
61 | try {
62 | if (context != null && context.getResources().getInteger(R.integer.development) == 0) {
63 | EasyTracker.getTracker().trackEvent(ANALYTICS, event, event, 1L);
64 | }
65 | } catch (Exception e) {
66 | Log.e(LOG_CAT, "logEvent", e);
67 | }
68 | }
69 |
70 | public static void logEvent(String event, Map parameters) {
71 | try {
72 | if (context != null && context.getResources().getInteger(R.integer.development) == 0) {
73 | EasyTracker.getTracker().trackEvent(ANALYTICS, event, event, 1L);
74 | }
75 | } catch (Exception e) {
76 | Log.e(LOG_CAT, "logEvent", e);
77 | }
78 | }
79 | }
80 |
--------------------------------------------------------------------------------
/src/com/entertailion/android/dial/BroadcastAdvertisement.java:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright (C) 2013 ENTERTAILION, LLC.
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
5 | * in compliance with the License. You may obtain a copy of the License at
6 | *
7 | * http://www.apache.org/licenses/LICENSE-2.0
8 | *
9 | * Unless required by applicable law or agreed to in writing, software distributed under the License
10 | * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
11 | * or implied. See the License for the specific language governing permissions and limitations under
12 | * the License.
13 | */
14 |
15 | package com.entertailion.android.dial;
16 |
17 | import java.net.InetAddress;
18 |
19 | /*
20 | HTTP/1.1 200 OK
21 | USN: uuid:d17c2986-4624-3f2c-93a5-fe3ef0a0ec9c::urn:dial-multiscreen-org:service:dial:1
22 | LOCATION: http://192.168.0.51:47944/dd.xml
23 | BOOTID.UPNP.ORG: 1287126024
24 | ST: urn:dial-multiscreen-org:service:dial:1
25 | CACHE-CONTROL: max-age=1800
26 | EXT
27 | */
28 |
29 | public final class BroadcastAdvertisement {
30 |
31 | private final String location;
32 | private final InetAddress ipAddress;
33 | private final int port;
34 |
35 | BroadcastAdvertisement(String location, InetAddress ipAddress, int port) {
36 | this.location = location;
37 | this.ipAddress = ipAddress;
38 | this.port = port;
39 | }
40 |
41 | public String getLocation() {
42 | return location;
43 | }
44 |
45 | public InetAddress getIpAddress() {
46 | return ipAddress;
47 | }
48 |
49 | public int getPort() {
50 | return port;
51 | }
52 | }
53 |
--------------------------------------------------------------------------------
/src/com/entertailion/android/dial/BroadcastDiscoveryClient.java:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright (C) 2009 Google Inc. All rights reserved.
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 |
17 | package com.entertailion.android.dial;
18 |
19 | import java.io.IOException;
20 | import java.io.InterruptedIOException;
21 | import java.net.DatagramPacket;
22 | import java.net.DatagramSocket;
23 | import java.net.InetAddress;
24 | import java.net.SocketException;
25 |
26 | import android.net.Uri;
27 | import android.os.Handler;
28 | import android.os.Message;
29 | import android.util.Log;
30 |
31 | /**
32 | * DIAL protocol client:
33 | * http://www.dial-multiscreen.org/dial-protocol-specification
34 | */
35 | public class BroadcastDiscoveryClient implements Runnable {
36 |
37 | private static final String LOG_TAG = "BroadcastDiscoveryClient";
38 |
39 | /**
40 | * UDP port to send probe messages to.
41 | */
42 | private static final int BROADCAST_SERVER_PORT = 1900;
43 |
44 | /**
45 | * Frequency of probe messages.
46 | */
47 | private static final int PROBE_INTERVAL_MS = 6000;
48 | private static final int PROBE_INTERVAL_MS_MAX = 60000;
49 |
50 | private static final String SEARCH_TARGET = "urn:dial-multiscreen-org:service:dial:1";
51 |
52 | private static final String M_SEARCH = "M-SEARCH * HTTP/1.1\r\n" + "HOST: 239.255.255.250:1900\r\n" + "MAN: \"ssdp:discover\"\r\n" + "MX: 10\r\n" + "ST: "
53 | + SEARCH_TARGET + "\r\n\r\n";
54 |
55 | private static final String HEADER_LOCATION = "LOCATION";
56 | private static final String HEADER_ST = "ST";
57 |
58 | private final InetAddress mBroadcastAddress;
59 |
60 | private final Thread mBroadcastThread;
61 | private int mBroadcastInterval;
62 | private boolean mBroadcasting = true;
63 | private boolean mReceiving = true;
64 |
65 | /**
66 | * Handle to main thread.
67 | */
68 | private final Handler mHandler;
69 |
70 | /**
71 | * Send/receive socket.
72 | */
73 | private final DatagramSocket mSocket;
74 |
75 | /**
76 | * Constructor
77 | *
78 | * @param broadcastAddress
79 | * destination address for probes
80 | * @param handler
81 | * update Handler in main thread
82 | */
83 | public BroadcastDiscoveryClient(InetAddress broadcastAddress, Handler handler) {
84 | mReceiving = true;
85 | mBroadcastAddress = broadcastAddress;
86 | mHandler = handler;
87 |
88 | try {
89 | mSocket = new DatagramSocket(); // binds to random port
90 | mSocket.setBroadcast(true);
91 | } catch (SocketException e) {
92 | Log.e(LOG_TAG, "Could not create broadcast client socket.", e);
93 | throw new RuntimeException();
94 | }
95 | mBroadcastInterval = PROBE_INTERVAL_MS;
96 | mBroadcastThread = new Thread(new Runnable() {
97 |
98 | @Override
99 | public void run() {
100 | while (mBroadcasting) {
101 | try {
102 | BroadcastDiscoveryClient.this.sendProbe();
103 | try {
104 | Thread.sleep(mBroadcastInterval);
105 | } catch (InterruptedException e) {
106 | }
107 | mBroadcastInterval = mBroadcastInterval * 2;
108 | if (mBroadcastInterval > PROBE_INTERVAL_MS_MAX) {
109 | mBroadcastInterval = PROBE_INTERVAL_MS_MAX;
110 | mBroadcasting = false;
111 | mReceiving = false;
112 | }
113 | } catch (Throwable e) {
114 | Log.e(LOG_TAG, "run", e);
115 | }
116 | }
117 | }
118 | });
119 | Log.i(LOG_TAG, "Starting client on address " + mBroadcastAddress);
120 | }
121 |
122 | /** {@inheritDoc} */
123 | public void run() {
124 | Log.i(LOG_TAG, "Broadcast client thread starting.");
125 | byte[] buffer = new byte[4096];
126 |
127 | mBroadcastThread.start();
128 |
129 | while (mReceiving) {
130 | try {
131 | DatagramPacket packet = new DatagramPacket(buffer, buffer.length);
132 | mSocket.receive(packet);
133 | handleResponsePacket(packet);
134 | } catch (InterruptedIOException e) {
135 | // timeout
136 | } catch (IOException e) {
137 | // SocketException - stop() was called
138 | break;
139 | } catch (IllegalArgumentException e) {
140 | break;
141 | }
142 | }
143 | Log.i(LOG_TAG, "Exiting client loop.");
144 | mBroadcasting = false;
145 | mBroadcastThread.interrupt();
146 | }
147 |
148 | /**
149 | * Sends a single broadcast discovery request.
150 | */
151 | private void sendProbe() {
152 | try {
153 | DatagramPacket packet = makeRequestPacket(mSocket.getLocalPort());
154 | mSocket.send(packet);
155 | } catch (Throwable e) {
156 | Log.e(LOG_TAG, "Exception sending broadcast probe", e);
157 | return;
158 | }
159 | }
160 |
161 | /**
162 | * Immediately stops the receiver thread, and cancels the probe timer.
163 | */
164 | public void stop() {
165 | if (mSocket != null) {
166 | mSocket.close();
167 | }
168 | }
169 |
170 | /**
171 | * Constructs a new probe packet.
172 | *
173 | * @param serviceName
174 | * the service name to discover
175 | * @param responsePort
176 | * the udp port number for replies
177 | * @return a new DatagramPacket
178 | */
179 | private DatagramPacket makeRequestPacket(int responsePort) {
180 | String message = M_SEARCH;
181 | byte[] buf = message.getBytes();
182 | DatagramPacket packet = new DatagramPacket(buf, buf.length, mBroadcastAddress, BROADCAST_SERVER_PORT);
183 | return packet;
184 | }
185 |
186 | /**
187 | * Parse a received packet, and notify the main thread if valid.
188 | *
189 | * @param packet
190 | * The locally-received DatagramPacket
191 | */
192 | private void handleResponsePacket(DatagramPacket packet) {
193 | try {
194 | String strPacket = new String(packet.getData(), 0, packet.getLength());
195 | Log.d(LOG_TAG, "response=" + strPacket);
196 | String tokens[] = strPacket.trim().split("\\n");
197 |
198 | String location = null;
199 | boolean foundSt = false;
200 | for (int i = 0; i < tokens.length; i++) {
201 | String token = tokens[i].trim();
202 | if (token.startsWith(HEADER_LOCATION)) {
203 | // LOCATION: http://192.168.0.51:47944/dd.xml
204 | location = token.substring(10).trim();
205 | } else if (token.startsWith(HEADER_ST)) {
206 | // ST: urn:dial-multiscreen-org:service:dial:1
207 | String st = token.substring(4).trim();
208 | if (st.equals(SEARCH_TARGET)) {
209 | foundSt = true;
210 | }
211 | }
212 | }
213 |
214 | if (!foundSt || location == null) {
215 | Log.w(LOG_TAG, "Malformed response: " + strPacket);
216 | return;
217 | }
218 |
219 | BroadcastAdvertisement advert;
220 | try {
221 | Uri uri = Uri.parse(location);
222 | InetAddress address = InetAddress.getByName(uri.getHost());
223 | advert = new BroadcastAdvertisement(location, address, uri.getPort());
224 | } catch (Exception e) {
225 | return;
226 | }
227 |
228 | Message message = mHandler.obtainMessage(ServerFinder.BROADCAST_RESPONSE, advert);
229 | mHandler.sendMessage(message);
230 | } catch (Exception e) {
231 | Log.e(LOG_TAG, "handleResponsePacket", e);
232 | }
233 | }
234 |
235 | }
236 |
--------------------------------------------------------------------------------
/src/com/entertailion/android/dial/DialServer.java:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright (C) 2013 ENTERTAILION, LLC.
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
5 | * in compliance with the License. You may obtain a copy of the License at
6 | *
7 | * http://www.apache.org/licenses/LICENSE-2.0
8 | *
9 | * Unless required by applicable law or agreed to in writing, software distributed under the License
10 | * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
11 | * or implied. See the License for the specific language governing permissions and limitations under
12 | * the License.
13 | */
14 | package com.entertailion.android.dial;
15 |
16 | import java.net.InetAddress;
17 |
18 | import android.os.Parcel;
19 | import android.os.Parcelable;
20 |
21 | public class DialServer implements Parcelable {
22 | private String location;
23 | private InetAddress ipAddress;
24 | private int port;
25 | private String appsUrl;
26 | private String friendlyName;
27 | private String uuid;
28 | private String manufacturer;
29 | private String modelName;
30 |
31 | public DialServer() {
32 |
33 | }
34 |
35 | public DialServer(String location, InetAddress ipAddress, int port, String appsUrl, String friendlyName, String uuid, String manufacturer, String modelName) {
36 | this.location = location;
37 | this.ipAddress = ipAddress;
38 | this.port = port;
39 | this.appsUrl = appsUrl;
40 | this.friendlyName = friendlyName;
41 | this.uuid = uuid;
42 | this.manufacturer = manufacturer;
43 | this.modelName = modelName;
44 | }
45 |
46 | public String getLocation() {
47 | return location;
48 | }
49 |
50 | public void setLocation(String location) {
51 | this.location = location;
52 | }
53 |
54 | public InetAddress getIpAddress() {
55 | return ipAddress;
56 | }
57 |
58 | public void setIpAddress(InetAddress ipAddress) {
59 | this.ipAddress = ipAddress;
60 | }
61 |
62 | public int getPort() {
63 | return port;
64 | }
65 |
66 | public void setPort(int port) {
67 | this.port = port;
68 | }
69 |
70 | public String getAppsUrl() {
71 | return appsUrl;
72 | }
73 |
74 | public void setAppsUrl(String appsUrl) {
75 | this.appsUrl = appsUrl;
76 | }
77 |
78 | public String getFriendlyName() {
79 | return friendlyName;
80 | }
81 |
82 | public void setFriendlyName(String friendlyName) {
83 | this.friendlyName = friendlyName;
84 | }
85 |
86 | public String getUuid() {
87 | return uuid;
88 | }
89 |
90 | public void setUuid(String uuid) {
91 | this.uuid = uuid;
92 | }
93 |
94 | public String getManufacturer() {
95 | return manufacturer;
96 | }
97 |
98 | public void setManufacturer(String manufacturer) {
99 | this.manufacturer = manufacturer;
100 | }
101 |
102 | public String getModelName() {
103 | return modelName;
104 | }
105 |
106 | public void setModelName(String modelName) {
107 | this.modelName = modelName;
108 | }
109 |
110 | public DialServer clone() {
111 | return new DialServer(location, ipAddress, port, appsUrl, friendlyName, uuid, manufacturer, modelName);
112 | }
113 |
114 | @Override
115 | public boolean equals(Object obj) {
116 | if (this == obj) {
117 | return true;
118 | }
119 | if (!(obj instanceof DialServer)) {
120 | return false;
121 | }
122 | DialServer that = (DialServer) obj;
123 | return equal(this.ipAddress, that.ipAddress) && (this.port == that.port);
124 | }
125 |
126 | private static boolean equal(T obj1, T obj2) {
127 | if (obj1 == null) {
128 | return obj2 == null;
129 | }
130 | return obj1.equals(obj2);
131 | }
132 |
133 | @Override
134 | public String toString() {
135 | return String.format("%s [%s:%d]", friendlyName, ipAddress.getHostAddress(), port);
136 | }
137 |
138 | @Override
139 | public int describeContents() {
140 | return 0;
141 | }
142 |
143 | @Override
144 | public void writeToParcel(Parcel parcel, int flags) {
145 | parcel.writeString(location);
146 | parcel.writeSerializable(ipAddress);
147 | parcel.writeInt(port);
148 | parcel.writeString(appsUrl);
149 | parcel.writeString(friendlyName);
150 | parcel.writeString(uuid);
151 | }
152 |
153 | public static final Parcelable.Creator CREATOR = new Parcelable.Creator() {
154 |
155 | public DialServer createFromParcel(Parcel parcel) {
156 | return new DialServer(parcel);
157 | }
158 |
159 | public DialServer[] newArray(int size) {
160 | return new DialServer[size];
161 | }
162 | };
163 |
164 | private DialServer(Parcel parcel) {
165 | this(parcel.readString(), (InetAddress) parcel.readSerializable(), parcel.readInt(), parcel.readString(), parcel.readString(), parcel.readString(), parcel.readString(), parcel.readString());
166 | }
167 | }
168 |
--------------------------------------------------------------------------------
/src/com/entertailion/android/dial/HttpRequestHelper.java:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright (C) 2013 ENTERTAILION, LLC.
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
5 | * in compliance with the License. You may obtain a copy of the License at
6 | *
7 | * http://www.apache.org/licenses/LICENSE-2.0
8 | *
9 | * Unless required by applicable law or agreed to in writing, software distributed under the License
10 | * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
11 | * or implied. See the License for the specific language governing permissions and limitations under
12 | * the License.
13 | */
14 | package com.entertailion.android.dial;
15 |
16 | import java.io.IOException;
17 | import java.io.InputStream;
18 | import java.io.OutputStream;
19 | import java.io.UnsupportedEncodingException;
20 | import java.net.HttpURLConnection;
21 | import java.net.URI;
22 | import java.net.URISyntaxException;
23 | import java.net.URL;
24 | import java.net.URLConnection;
25 | import java.util.ArrayList;
26 | import java.util.List;
27 | import java.util.Map;
28 |
29 | import org.apache.http.HttpEntity;
30 | import org.apache.http.HttpResponse;
31 | import org.apache.http.HttpVersion;
32 | import org.apache.http.NameValuePair;
33 | import org.apache.http.client.entity.UrlEncodedFormEntity;
34 | import org.apache.http.client.methods.HttpGet;
35 | import org.apache.http.client.methods.HttpPost;
36 | import org.apache.http.client.params.ClientPNames;
37 | import org.apache.http.client.params.CookiePolicy;
38 | import org.apache.http.client.utils.URIUtils;
39 | import org.apache.http.client.utils.URLEncodedUtils;
40 | import org.apache.http.conn.ClientConnectionManager;
41 | import org.apache.http.conn.scheme.PlainSocketFactory;
42 | import org.apache.http.conn.scheme.Scheme;
43 | import org.apache.http.conn.scheme.SchemeRegistry;
44 | import org.apache.http.conn.ssl.SSLSocketFactory;
45 | import org.apache.http.impl.client.DefaultHttpClient;
46 | import org.apache.http.impl.conn.tsccm.ThreadSafeClientConnManager;
47 | import org.apache.http.message.BasicNameValuePair;
48 | import org.apache.http.params.BasicHttpParams;
49 | import org.apache.http.params.HttpConnectionParams;
50 | import org.apache.http.params.HttpParams;
51 | import org.apache.http.params.HttpProtocolParams;
52 | import org.apache.http.protocol.BasicHttpContext;
53 | import org.apache.http.protocol.HTTP;
54 | import org.apache.http.protocol.HttpContext;
55 | import org.apache.http.util.EntityUtils;
56 |
57 | import android.util.Log;
58 |
59 | public class HttpRequestHelper {
60 | DefaultHttpClient httpClient;
61 | HttpContext localContext;
62 | private String ret;
63 | private String TAG = "HttpRequestHelper";
64 |
65 | HttpResponse response = null;
66 | HttpPost httpPost = null;
67 | HttpGet httpGet = null;
68 |
69 | public HttpRequestHelper() {
70 | httpClient = createHttpClient();
71 | localContext = new BasicHttpContext();
72 | }
73 |
74 | public static DefaultHttpClient createHttpClient() {
75 | HttpParams params = new BasicHttpParams();
76 | HttpConnectionParams.setConnectionTimeout(params, 20000);
77 | HttpConnectionParams.setSoTimeout(params, 20000);
78 | HttpProtocolParams.setVersion(params, HttpVersion.HTTP_1_1);
79 | HttpProtocolParams.setContentCharset(params, HTTP.DEFAULT_CONTENT_CHARSET);
80 |
81 | SchemeRegistry schReg = new SchemeRegistry();
82 | schReg.register(new Scheme("http", PlainSocketFactory.getSocketFactory(), 80));
83 | schReg.register(new Scheme("https", SSLSocketFactory.getSocketFactory(), 443));
84 | ClientConnectionManager conMgr = new ThreadSafeClientConnManager(params, schReg);
85 |
86 | return new DefaultHttpClient(conMgr, params);
87 | }
88 |
89 | public void clearCookies() {
90 | httpClient.getCookieStore().clear();
91 | }
92 |
93 | public void abort() {
94 | try {
95 | if (httpClient != null) {
96 | Log.i(TAG, "Abort.");
97 | httpPost.abort();
98 | }
99 | } catch (Throwable e) {
100 | Log.e(TAG, "Failed to abort", e);
101 | }
102 | }
103 |
104 | public String sendPost(String url, Map params) {
105 | return sendPost(url, params, null);
106 | }
107 |
108 | public String sendPost(String url, Map params, String contentType) {
109 | ret = null;
110 |
111 | try {
112 | httpClient.getParams().setParameter(ClientPNames.COOKIE_POLICY, CookiePolicy.RFC_2109);
113 | httpPost = new HttpPost(url);
114 | response = null;
115 | Log.d(TAG, "Setting httpPost headers");
116 | httpPost.setHeader("Accept", "text/html,application/xml,application/xhtml+xml,text/html;q=0.9,text/plain;q=0.8,image/png,*/*;q=0.5");
117 |
118 | if (contentType != null) {
119 | httpPost.setHeader("Content-Type", contentType);
120 | } else {
121 | httpPost.setHeader("Content-Type", "application/x-www-form-urlencoded");
122 | }
123 |
124 | httpPost.setEntity(buildData(params));
125 | response = httpClient.execute(httpPost, localContext);
126 | if (response != null) {
127 | ret = EntityUtils.toString(response.getEntity());
128 | }
129 | } catch (Throwable e) {
130 | Log.e(TAG, "Failed to post to url: " + url, e);
131 | }
132 |
133 | Log.d(TAG, "Returning value:" + ret);
134 | return ret;
135 | }
136 |
137 | public boolean sendPostStream(String url, Map params, String contentType, OutputStream outstream) {
138 | boolean success = false;
139 |
140 | try {
141 | httpClient.getParams().setParameter(ClientPNames.COOKIE_POLICY, CookiePolicy.RFC_2109);
142 | httpPost = new HttpPost(url);
143 | response = null;
144 | Log.d(TAG, "Setting httpPost headers");
145 | httpPost.setHeader("Accept", "text/html,application/xml,application/xhtml+xml,text/html;q=0.9,text/plain;q=0.8,image/png,*/*;q=0.5");
146 |
147 | if (contentType != null) {
148 | httpPost.setHeader("Content-Type", contentType);
149 | } else {
150 | httpPost.setHeader("Content-Type", "application/x-www-form-urlencoded");
151 | }
152 |
153 | httpPost.setEntity(buildData(params));
154 | response = httpClient.execute(httpPost, localContext);
155 | if (response != null) {
156 | response.getEntity().writeTo(outstream);
157 | success = true;
158 | }
159 | } catch (Throwable e) {
160 | Log.e(TAG, "Failed to post to url: " + url, e);
161 | }
162 |
163 | Log.d(TAG, "Returning value:" + success);
164 | return success;
165 | }
166 |
167 | public String sendGet(String url) {
168 | httpGet = new HttpGet(url);
169 |
170 | try {
171 | response = httpClient.execute(httpGet);
172 | } catch (Throwable e) {
173 | Log.e(TAG, "sendGet exception: ", e);
174 | }
175 |
176 | // we assume that the response body contains the error message
177 | try {
178 | if (null == response)
179 | return null;
180 | ret = EntityUtils.toString(response.getEntity());
181 | } catch (Throwable e) {
182 | Log.e(TAG, "sendGet exception: ", e);
183 | }
184 |
185 | return ret;
186 | }
187 |
188 | public boolean sendGetStream(String url, OutputStream outstream) {
189 | boolean success = false;
190 | httpGet = new HttpGet(url);
191 |
192 | try {
193 | response = httpClient.execute(httpGet);
194 | } catch (Throwable e) {
195 | Log.e(TAG, "sendGet exception: ", e);
196 | }
197 |
198 | // we assume that the response body contains the error message
199 | try {
200 | if (null == response)
201 | return false;
202 | // ret = EntityUtils.toString(response.getEntity());
203 | response.getEntity().writeTo(outstream);
204 | success = true;
205 | } catch (Throwable e) {
206 | Log.e(TAG, "sendGet exception: ", e);
207 | }
208 |
209 | return success;
210 | }
211 |
212 | public HttpResponse sendHttpGet(String url) {
213 | httpGet = new HttpGet(url);
214 |
215 | try {
216 | return httpClient.execute(httpGet);
217 | } catch (Throwable e) {
218 | Log.e(TAG, "sendHttpGet exception: ", e);
219 | }
220 | return null;
221 |
222 | }
223 |
224 | public HttpResponse sendHttpGet(String url, String... query) {
225 | String queryParams = buildgetData(query);
226 | return sendHttpGet(url + "?" + queryParams);
227 | }
228 |
229 | public String sendGet(String scheme, String host, int port, String path, String... query) throws URISyntaxException {
230 | String queryParams = buildgetData(query);
231 | URI uri = URIUtils.createURI(scheme, host, port, path, queryParams, null);
232 | httpGet = new HttpGet(uri);
233 |
234 | try {
235 | response = httpClient.execute(httpGet);
236 | } catch (Throwable e) {
237 | Log.e(TAG, "sendGet exception: ", e);
238 | }
239 |
240 | // we assume that the response body contains the error message
241 | try {
242 | ret = EntityUtils.toString(response.getEntity());
243 | } catch (Throwable e) {
244 | Log.e(TAG, "sendGet exception: ", e);
245 | }
246 |
247 | return ret;
248 | }
249 |
250 | public String sendGet(String url, String... query) {
251 | String queryParams = buildgetData(query);
252 | return sendGet(url + "?" + queryParams);
253 | }
254 |
255 | public boolean sendGetStream(String url, OutputStream outstream, String... query) {
256 | String queryParams = buildgetData(query);
257 | return sendGetStream(url + "?" + queryParams, outstream);
258 | }
259 |
260 | public InputStream getHttpStream(String urlString) throws IOException {
261 | InputStream in = null;
262 | int response = -1;
263 |
264 | URL url = new URL(urlString);
265 | URLConnection conn = url.openConnection();
266 |
267 | if (!(conn instanceof HttpURLConnection))
268 | throw new IOException("Not an HTTP connection");
269 |
270 | try {
271 | HttpURLConnection httpConn = (HttpURLConnection) conn;
272 | httpConn.setAllowUserInteraction(false);
273 | httpConn.setInstanceFollowRedirects(true);
274 | httpConn.setRequestMethod("GET");
275 | httpConn.connect();
276 |
277 | response = httpConn.getResponseCode();
278 |
279 | if (response == HttpURLConnection.HTTP_OK) {
280 | in = httpConn.getInputStream();
281 | }
282 | } catch (Exception e) {
283 | throw new IOException("Error connecting");
284 | } // end try-catch
285 |
286 | return in;
287 | }
288 |
289 | public static String buildgetData(String... list) {
290 | if (null == list || list.length == 0)
291 | return null;
292 | List qparams = new ArrayList();
293 | for (int i = 0; i < list.length - 1; i = i + 2) {
294 | qparams.add(new BasicNameValuePair(list[i], list[i + 1]));
295 | }
296 | return URLEncodedUtils.format(qparams, "UTF-8");
297 |
298 | }
299 |
300 | public HttpEntity buildData(Map map) throws UnsupportedEncodingException {
301 | if (null != map && !map.isEmpty()) {
302 | List parameters = new ArrayList();
303 | for (String name : map.keySet()) {
304 | parameters.add(new BasicNameValuePair(name, map.get(name)));
305 | }
306 | return new UrlEncodedFormEntity(parameters, "UTF-8");
307 | }
308 | return null;
309 | }
310 |
311 | }
312 |
--------------------------------------------------------------------------------
/src/com/entertailion/android/dial/MainActivity.java:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright (C) 2013 ENTERTAILION, LLC.
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
5 | * in compliance with the License. You may obtain a copy of the License at
6 | *
7 | * http://www.apache.org/licenses/LICENSE-2.0
8 | *
9 | * Unless required by applicable law or agreed to in writing, software distributed under the License
10 | * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
11 | * or implied. See the License for the specific language governing permissions and limitations under
12 | * the License.
13 | */
14 | package com.entertailion.android.dial;
15 |
16 | import java.io.Reader;
17 | import java.io.StringReader;
18 | import java.net.InetAddress;
19 | import java.net.URI;
20 | import java.util.ArrayList;
21 | import java.util.Arrays;
22 | import java.util.Collections;
23 | import java.util.LinkedHashMap;
24 | import java.util.List;
25 |
26 | import org.apache.http.Header;
27 | import org.apache.http.HttpResponse;
28 | import org.apache.http.ProtocolException;
29 | import org.apache.http.client.methods.HttpDelete;
30 | import org.apache.http.client.methods.HttpGet;
31 | import org.apache.http.client.methods.HttpPost;
32 | import org.apache.http.entity.StringEntity;
33 | import org.apache.http.impl.client.DefaultHttpClient;
34 | import org.apache.http.impl.client.DefaultRedirectHandler;
35 | import org.apache.http.message.BasicNameValuePair;
36 | import org.apache.http.protocol.BasicHttpContext;
37 | import org.apache.http.protocol.HttpContext;
38 | import org.apache.http.util.EntityUtils;
39 | import org.json.JSONException;
40 | import org.json.JSONObject;
41 | import org.xmlpull.v1.XmlPullParser;
42 | import org.xmlpull.v1.XmlPullParserFactory;
43 |
44 | import android.app.Activity;
45 | import android.content.Context;
46 | import android.content.Intent;
47 | import android.os.Bundle;
48 | import android.util.Log;
49 | import android.view.Menu;
50 | import android.view.MenuItem;
51 | import android.widget.Toast;
52 |
53 | import com.codebutler.android_websockets.WebSocketClient;
54 |
55 | /**
56 | * DIAL protocol client:
57 | * http://www.dial-multiscreen.org/dial-protocol-specification
58 | *
59 | * @author leon_nicholls
60 | *
61 | */
62 | public class MainActivity extends Activity {
63 | private static final String LOG_TAG = "MainActivity";
64 |
65 | public static final String PREFS_NAME = "preferences";
66 |
67 | private static final String YOU_TUBE = "YouTube";
68 | private static final String FLING = "Fling";
69 | private static final String NETFLIX = "Netflix";
70 | private static final String CHROME_CAST = "ChromeCast";
71 | private static final String PLAY_MOVIES = "PlayMovies";
72 | private static final String TIC_TAC_TOE = "TicTacToe";
73 |
74 | private static final String STATE_RUNNING = "running";
75 | private static final String STATE_STOPPED = "stopped";
76 |
77 | private static final String HEADER_CONNECTION = "Connection";
78 | private static final String HEADER_CONNECTION_VALUE = "keep-alive";
79 | private static final String HEADER_ORIGN = "Origin";
80 | private static final String HEADER_ORIGIN_VALUE = "chrome-extension://boadgeojelhgndaghljhdicfkmllpafd";
81 | private static final String HEADER_USER_AGENT = "User-Agent";
82 | private static final String HEADER_USER_AGENT_VALUE = "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_8_4) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/28.0.1500.71 Safari/537.36";
83 | private static final String HEADER_DNT = "DNT";
84 | private static final String HEADER_DNT_VALUE = "1";
85 | private static final String HEADER_ACCEPT_ENCODING = "Accept-Encoding";
86 | private static final String HEADER_ACCEPT_ENCODING_VALUE = "gzip,deflate,sdch";
87 | private static final String HEADER_ACCEPT = "Accept";
88 | private static final String HEADER_ACCEPT_VALUE = "*/*";
89 | private static final String HEADER_ACCEPT_LANGUAGE = "Accept-Language";
90 | private static final String HEADER_ACCEPT_LANGUAGE_VALUE = "en-US,en;q=0.8";
91 | private static final String HEADER_CONTENT_TYPE = "Content-Type";
92 | private static final String HEADER_CONTENT_TYPE_JSON_VALUE = "application/json";
93 | private static final String HEADER_CONTENT_TYPE_TEXT_VALUE = "text/plain";
94 |
95 | /**
96 | * Request code used by this activity.
97 | */
98 | protected static final int CODE_SWITCH_SERVER = 1;
99 |
100 | private DialServer target;
101 |
102 | private LinkedHashMap recentlyConnected = new LinkedHashMap();
103 |
104 | private WebSocketClient client;
105 | private String connectionServiceUrl;
106 | private String state;
107 | private String protocol;
108 | private String response;
109 |
110 | /*
111 | * @see android.app.Activity#onCreate(android.os.Bundle)
112 | */
113 | @Override
114 | protected void onCreate(Bundle savedInstanceState) {
115 | super.onCreate(savedInstanceState);
116 | setContentView(R.layout.activity_main);
117 |
118 | startActivityForResult(ServerFinder.createConnectIntent(this, target, getRecentlyConnected()), CODE_SWITCH_SERVER);
119 | }
120 |
121 | /*
122 | * @see android.app.Activity#onCreateOptionsMenu(android.view.Menu)
123 | */
124 | @Override
125 | public boolean onCreateOptionsMenu(Menu menu) {
126 | // Inflate the menu; this adds items to the action bar if it is present.
127 | getMenuInflater().inflate(R.menu.activity_main, menu);
128 | return true;
129 | }
130 |
131 | /*
132 | * The user has selected a DIAL server to connect to
133 | *
134 | * @see android.app.Activity#onActivityResult(int, int,
135 | * android.content.Intent)
136 | */
137 | @Override
138 | protected void onActivityResult(final int requestCode, final int resultCode, final Intent data) {
139 | if (requestCode == CODE_SWITCH_SERVER) {
140 | if (resultCode == RESULT_OK && data != null) {
141 | final DialServer dialServer = data.getParcelableExtra(ServerFinder.EXTRA_DIAL_SERVER);
142 | if (dialServer != null) {
143 | Toast.makeText(MainActivity.this, getString(R.string.finder_connected, dialServer.toString()), Toast.LENGTH_LONG).show();
144 | new Thread(new Runnable() {
145 | public void run() {
146 | try {
147 | String device = "http://" + dialServer.getIpAddress().getHostAddress() + ":" + dialServer.getPort();
148 | Log.d(LOG_TAG, "device=" + device);
149 | Log.d(LOG_TAG, "apps url=" + dialServer.getAppsUrl());
150 |
151 | // application instance url
152 | String location = null;
153 | String app = YOU_TUBE;
154 |
155 | DefaultHttpClient defaultHttpClient = HttpRequestHelper.createHttpClient();
156 | CustomRedirectHandler handler = new CustomRedirectHandler();
157 | defaultHttpClient.setRedirectHandler(handler);
158 | BasicHttpContext localContext = new BasicHttpContext();
159 |
160 | // check if any app is running
161 | HttpGet httpGet = new HttpGet(dialServer.getAppsUrl());
162 | httpGet.setHeader(HEADER_CONNECTION, HEADER_CONNECTION_VALUE);
163 | httpGet.setHeader(HEADER_USER_AGENT, HEADER_USER_AGENT_VALUE);
164 | httpGet.setHeader(HEADER_ACCEPT, HEADER_ACCEPT_VALUE);
165 | httpGet.setHeader(HEADER_DNT, HEADER_DNT_VALUE);
166 | httpGet.setHeader(HEADER_ACCEPT_ENCODING, HEADER_ACCEPT_ENCODING_VALUE);
167 | httpGet.setHeader(HEADER_ACCEPT_LANGUAGE, HEADER_ACCEPT_LANGUAGE_VALUE);
168 | HttpResponse httpResponse = defaultHttpClient.execute(httpGet);
169 | if (httpResponse != null) {
170 | int responseCode = httpResponse.getStatusLine().getStatusCode();
171 | Log.d(LOG_TAG, "get response code=" + httpResponse.getStatusLine().getStatusCode());
172 | if (responseCode == 204) {
173 | // nothing is running
174 | } else if (responseCode == 200) {
175 | // app is running
176 |
177 | // Need to get real URL after a redirect
178 | // http://stackoverflow.com/a/10286025/594751
179 | String lastUrl = dialServer.getAppsUrl();
180 | if (handler.lastRedirectedUri != null) {
181 | lastUrl = handler.lastRedirectedUri.toString();
182 | Log.d(LOG_TAG, "lastUrl=" + lastUrl);
183 | }
184 |
185 | String response = EntityUtils.toString(httpResponse.getEntity());
186 | Log.d(LOG_TAG, "get response=" + response);
187 | parseXml(MainActivity.this, new StringReader(response));
188 |
189 | Header[] headers = httpResponse.getAllHeaders();
190 | for (int i = 0; i < headers.length; i++) {
191 | Log.d(LOG_TAG, headers[i].getName() + "=" + headers[i].getValue());
192 | }
193 |
194 | // stop the app instance
195 | HttpDelete httpDelete = new HttpDelete(lastUrl);
196 | httpResponse = defaultHttpClient.execute(httpDelete);
197 | if (httpResponse != null) {
198 | Log.d(LOG_TAG, "delete response code=" + httpResponse.getStatusLine().getStatusCode());
199 | response = EntityUtils.toString(httpResponse.getEntity());
200 | Log.d(LOG_TAG, "delete response=" + response);
201 | } else {
202 | Log.d(LOG_TAG, "no delete response");
203 | }
204 | }
205 |
206 | } else {
207 | Log.i(LOG_TAG, "no get response");
208 | return;
209 | }
210 |
211 | // Check if app is installed on device
212 | int responseCode = getAppStatus(defaultHttpClient, dialServer.getAppsUrl() + app);
213 | if (responseCode != 200) {
214 | return;
215 | }
216 | parseXml(MainActivity.this, new StringReader(response));
217 | Log.d(LOG_TAG, "state=" + state);
218 |
219 | // start the app with POST
220 | HttpPost httpPost = new HttpPost(dialServer.getAppsUrl() + app);
221 | httpPost.setHeader(HEADER_CONNECTION, HEADER_CONNECTION_VALUE);
222 | httpPost.setHeader(HEADER_ORIGN, HEADER_ORIGIN_VALUE);
223 | httpPost.setHeader(HEADER_USER_AGENT, HEADER_USER_AGENT_VALUE);
224 | httpPost.setHeader(HEADER_DNT, HEADER_DNT_VALUE);
225 | httpPost.setHeader(HEADER_ACCEPT_ENCODING, HEADER_ACCEPT_ENCODING_VALUE);
226 | httpPost.setHeader(HEADER_ACCEPT, HEADER_ACCEPT_VALUE);
227 | httpPost.setHeader(HEADER_ACCEPT_LANGUAGE, HEADER_ACCEPT_LANGUAGE_VALUE);
228 | httpPost.setHeader(HEADER_CONTENT_TYPE, HEADER_CONTENT_TYPE_TEXT_VALUE);
229 | // Set variable values as the body of the POST;
230 | // v is the YouTube video id.
231 | httpPost.setEntity(new StringEntity("pairingCode=eac4ae42-8b54-4441-9be3-d8a9abb5c481&v=cKG5HDyTW8o&t=0")); // http://www.youtube.com/watch?v=cKG5HDyTW8o
232 |
233 | httpResponse = defaultHttpClient.execute(httpPost, localContext);
234 | if (httpResponse != null) {
235 | Log.d(LOG_TAG, "post response code=" + httpResponse.getStatusLine().getStatusCode());
236 | response = EntityUtils.toString(httpResponse.getEntity());
237 | Log.d(LOG_TAG, "post response=" + response);
238 | Header[] headers = httpResponse.getHeaders("LOCATION");
239 | if (headers.length > 0) {
240 | location = headers[0].getValue();
241 | Log.d(LOG_TAG, "post response location=" + location);
242 | }
243 |
244 | headers = httpResponse.getAllHeaders();
245 | for (int i = 0; i < headers.length; i++) {
246 | Log.d(LOG_TAG, headers[i].getName() + "=" + headers[i].getValue());
247 | }
248 | } else {
249 | Log.i(LOG_TAG, "no post response");
250 | return;
251 | }
252 |
253 | // Keep trying to get the app status until the
254 | // connection service URL is available
255 | state = STATE_STOPPED;
256 | do {
257 | responseCode = getAppStatus(defaultHttpClient, dialServer.getAppsUrl() + app);
258 | if (responseCode != 200) {
259 | break;
260 | }
261 | parseXml(MainActivity.this, new StringReader(response));
262 | Log.d(LOG_TAG, "state=" + state);
263 | Log.d(LOG_TAG, "connectionServiceUrl=" + connectionServiceUrl);
264 | Log.d(LOG_TAG, "protocol=" + protocol);
265 | try {
266 | Thread.sleep(1000);
267 | } catch (Exception e) {
268 | }
269 | } while (state.equals(STATE_RUNNING) && connectionServiceUrl == null);
270 |
271 | if (connectionServiceUrl == null) {
272 | Log.i(LOG_TAG, "connectionServiceUrl is null");
273 | return; // oops, something went wrong
274 | }
275 |
276 | // get the websocket URL
277 | String webSocketAddress = null;
278 | httpPost = new HttpPost(connectionServiceUrl); // "http://192.168.0.17:8008/connection/YouTube"
279 | httpPost.setHeader(HEADER_CONNECTION, HEADER_CONNECTION_VALUE);
280 | httpPost.setHeader(HEADER_ORIGN, HEADER_ORIGIN_VALUE);
281 | httpPost.setHeader(HEADER_USER_AGENT, HEADER_USER_AGENT_VALUE);
282 | httpPost.setHeader(HEADER_DNT, HEADER_DNT_VALUE);
283 | httpPost.setHeader(HEADER_ACCEPT_ENCODING, HEADER_ACCEPT_ENCODING_VALUE);
284 | httpPost.setHeader(HEADER_ACCEPT, HEADER_ACCEPT_VALUE);
285 | httpPost.setHeader(HEADER_ACCEPT_LANGUAGE, HEADER_ACCEPT_LANGUAGE_VALUE);
286 | httpPost.setHeader(HEADER_CONTENT_TYPE, HEADER_CONTENT_TYPE_JSON_VALUE);
287 | httpPost.setEntity(new StringEntity("{\"channel\":0,\"senderId\":{\"appName\":\"ChromeCast\", \"senderId\":\"7v3zqrpliq3i\"}}"));
288 |
289 | httpResponse = defaultHttpClient.execute(httpPost, localContext);
290 | if (httpResponse != null) {
291 | responseCode = httpResponse.getStatusLine().getStatusCode();
292 | Log.d(LOG_TAG, "post response code=" + responseCode);
293 | if (responseCode == 200) {
294 | // should return JSON payload
295 | response = EntityUtils.toString(httpResponse.getEntity());
296 | Log.d(LOG_TAG, "post response=" + response);
297 | Header[] headers = httpResponse.getAllHeaders();
298 | for (int i = 0; i < headers.length; i++) {
299 | Log.d(LOG_TAG, headers[i].getName() + "=" + headers[i].getValue());
300 | }
301 |
302 | JSONObject jObject;
303 | try {
304 | jObject = new JSONObject(response); // {"URL":"ws://192.168.0.17:8008/session?33","pingInterval":0}
305 | webSocketAddress = jObject.getString("URL");
306 | Log.d(LOG_TAG, "webSocketAddress: " + webSocketAddress);
307 | int pingInterval = jObject.optInt("pingInterval"); // TODO
308 | } catch (JSONException e) {
309 | Log.e(LOG_TAG, "JSON", e);
310 | }
311 | }
312 | } else {
313 | Log.i(LOG_TAG, "no post response");
314 | return;
315 | }
316 |
317 | // Make a web socket connection for doing RAMP
318 | // to control media playback
319 | if (webSocketAddress != null) {
320 | // https://github.com/koush/android-websockets
321 | List extraHeaders = Arrays.asList(new BasicNameValuePair(HEADER_ORIGN, HEADER_ORIGIN_VALUE),
322 | new BasicNameValuePair("Pragma", "no-cache"), new BasicNameValuePair("Cache-Control", "no-cache"),
323 | new BasicNameValuePair(HEADER_USER_AGENT, HEADER_USER_AGENT_VALUE));
324 | client = new WebSocketClient(URI.create(webSocketAddress), new WebSocketClient.Listener() { // ws://192.168.0.17:8008/session?26
325 | @Override
326 | public void onConnect() {
327 | Log.d(LOG_TAG, "Websocket Connected!");
328 |
329 | // TODO RAMP commands
330 | }
331 |
332 | @Override
333 | public void onMessage(String message) {
334 | Log.d(LOG_TAG, String.format("Websocket Got string message! %s", message));
335 | }
336 |
337 | @Override
338 | public void onMessage(byte[] data) {
339 | Log.d(LOG_TAG, String.format("Websocket Got binary message! %s", data));
340 | }
341 |
342 | @Override
343 | public void onDisconnect(int code, String reason) {
344 | Log.d(LOG_TAG, String.format("Websocket Disconnected! Code: %d Reason: %s", code, reason));
345 | }
346 |
347 | @Override
348 | public void onError(Exception error) {
349 | Log.e(LOG_TAG, "Websocket Error!", error);
350 | }
351 |
352 | }, extraHeaders);
353 | client.connect();
354 | } else {
355 | Log.i(LOG_TAG, "webSocketAddress is null");
356 | }
357 |
358 | } catch (Exception e) {
359 | Log.e(LOG_TAG, "run", e);
360 | }
361 | }
362 | }).start();
363 | }
364 | }
365 | }
366 | }
367 |
368 | /**
369 | * Do HTTP GET for app status to determine response code and response body
370 | *
371 | * @param defaultHttpClient
372 | * @param url
373 | * @return
374 | */
375 | private int getAppStatus(DefaultHttpClient defaultHttpClient, String url) {
376 | int responseCode = 200;
377 | try {
378 | HttpGet httpGet = new HttpGet(url);
379 | HttpResponse httpResponse = defaultHttpClient.execute(httpGet);
380 | if (httpResponse != null) {
381 | responseCode = httpResponse.getStatusLine().getStatusCode();
382 | Log.d(LOG_TAG, "get response code=" + responseCode);
383 | response = EntityUtils.toString(httpResponse.getEntity());
384 | Log.d(LOG_TAG, "get response=" + response);
385 | } else {
386 | Log.i(LOG_TAG, "no get response");
387 | }
388 | } catch (Exception e) {
389 | Log.e(LOG_TAG, "getAppStatus", e);
390 | }
391 | return responseCode;
392 | }
393 |
394 | /**
395 | * Parse the App status description XML
396 | *
397 | * @param context
398 | * @param reader
399 | */
400 | private void parseXml(Context context, Reader reader) {
401 | try {
402 | XmlPullParserFactory factory = XmlPullParserFactory.newInstance();
403 | factory.setNamespaceAware(true);
404 | XmlPullParser parser = factory.newPullParser();
405 | parser.setInput(reader);
406 | int eventType = parser.getEventType();
407 | String lastTagName = null;
408 | while (eventType != XmlPullParser.END_DOCUMENT) {
409 | switch (eventType) {
410 | case XmlPullParser.START_DOCUMENT:
411 | break;
412 | case XmlPullParser.START_TAG:
413 | String tagName = parser.getName();
414 | lastTagName = tagName;
415 | break;
416 | case XmlPullParser.TEXT:
417 | if (lastTagName != null) {
418 | if ("connectionSvcURL".equals(lastTagName)) {
419 | connectionServiceUrl = parser.getText();
420 | } else if ("state".equals(lastTagName)) {
421 | state = parser.getText();
422 | } else if ("protocol".equals(lastTagName)) {
423 | protocol = parser.getText();
424 | }
425 | }
426 | break;
427 | case XmlPullParser.END_TAG:
428 | tagName = parser.getName();
429 | lastTagName = null;
430 | break;
431 | }
432 | eventType = parser.next();
433 | }
434 | } catch (Exception e) {
435 | Log.e(LOG_TAG, "parseXml", e);
436 | }
437 | }
438 |
439 | /**
440 | * @return list of recently connected devices
441 | */
442 | public ArrayList getRecentlyConnected() {
443 | ArrayList devices = new ArrayList(recentlyConnected.values());
444 | Collections.reverse(devices);
445 | return devices;
446 | }
447 |
448 | /*
449 | * Menu selection
450 | *
451 | * @see android.app.Activity#onOptionsItemSelected(android.view.MenuItem)
452 | */
453 | @Override
454 | public boolean onOptionsItemSelected(MenuItem item) {
455 | switch (item.getItemId()) {
456 |
457 | case R.id.menu_switch:
458 | startActivityForResult(ServerFinder.createConnectIntent(this, target, getRecentlyConnected()), CODE_SWITCH_SERVER);
459 | return true;
460 | default:
461 | return super.onOptionsItemSelected(item);
462 | }
463 | }
464 |
465 | /**
466 | * Custom HTTP redirection handler to keep track of the redirected URL
467 | * ChromeCast web server will redirect "/apps" to "/apps/YouTube" if that is the active/last app
468 | *
469 | */
470 | public class CustomRedirectHandler extends DefaultRedirectHandler {
471 |
472 | public URI lastRedirectedUri;
473 |
474 | @Override
475 | public boolean isRedirectRequested(HttpResponse response, HttpContext context) {
476 |
477 | return super.isRedirectRequested(response, context);
478 | }
479 |
480 | @Override
481 | public URI getLocationURI(HttpResponse response, HttpContext context) throws ProtocolException {
482 |
483 | lastRedirectedUri = super.getLocationURI(response, context);
484 |
485 | return lastRedirectedUri;
486 | }
487 |
488 | }
489 | }
490 |
--------------------------------------------------------------------------------
/src/com/entertailion/android/dial/ServerFinder.java:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright (C) 2009 Google Inc. All rights reserved.
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 |
17 | package com.entertailion.android.dial;
18 |
19 | import java.io.BufferedReader;
20 | import java.io.InputStream;
21 | import java.io.InputStreamReader;
22 | import java.net.InetAddress;
23 | import java.net.UnknownHostException;
24 | import java.util.ArrayList;
25 | import java.util.List;
26 |
27 | import org.apache.http.Header;
28 | import org.apache.http.HttpResponse;
29 | import org.apache.http.util.EntityUtils;
30 | import org.xmlpull.v1.XmlPullParser;
31 | import org.xmlpull.v1.XmlPullParserFactory;
32 |
33 | import android.app.Activity;
34 | import android.app.AlertDialog;
35 | import android.app.ProgressDialog;
36 | import android.content.Context;
37 | import android.content.DialogInterface;
38 | import android.content.Intent;
39 | import android.content.SharedPreferences;
40 | import android.net.wifi.WifiInfo;
41 | import android.net.wifi.WifiManager;
42 | import android.os.Build;
43 | import android.os.Bundle;
44 | import android.os.Handler;
45 | import android.os.Message;
46 | import android.provider.Settings;
47 | import android.text.InputFilter;
48 | import android.text.InputType;
49 | import android.text.TextUtils;
50 | import android.text.method.NumberKeyListener;
51 | import android.util.AttributeSet;
52 | import android.util.Log;
53 | import android.view.KeyEvent;
54 | import android.view.LayoutInflater;
55 | import android.view.View;
56 | import android.view.ViewGroup;
57 | import android.widget.AdapterView;
58 | import android.widget.AdapterView.OnItemClickListener;
59 | import android.widget.BaseAdapter;
60 | import android.widget.Button;
61 | import android.widget.EditText;
62 | import android.widget.LinearLayout;
63 | import android.widget.ListView;
64 | import android.widget.TextView;
65 | import android.widget.Toast;
66 |
67 | /**
68 | * Device discovery with UPnP.
69 | */
70 | public final class ServerFinder extends Activity {
71 | private static final String LOG_TAG = "ServerFinder";
72 |
73 | /**
74 | * Request code used by wifi settings activity
75 | */
76 | private static final int CODE_WIFI_SETTINGS = 1;
77 |
78 | public static final String MANUAL_IP_ADDRESS = "manual_ip_address";
79 |
80 | private static final String HEADER_APPLICATION_URL = "Application-URL";
81 |
82 | private ProgressDialog progressDialog;
83 | private AlertDialog confirmationDialog;
84 | private DialServer previousDialServer;
85 | private List recentlyConnectedServers;
86 |
87 | private InetAddress broadcastAddress;
88 | private WifiManager wifiManager;
89 | private boolean active;
90 |
91 | /**
92 | * Handles used to pass data back to calling activities.
93 | */
94 | public static final String EXTRA_DIAL_SERVER = "dial_server";
95 | public static final String EXTRA_RECENTLY_CONNECTED = "recently_connected";
96 |
97 | private ListView stbList;
98 | private final DeviceFinderListAdapter dataAdapter;
99 |
100 | private BroadcastHandler broadcastHandler;
101 | private BroadcastDiscoveryClient broadcastClient;
102 | private Thread broadcastClientThread;
103 |
104 | private TrackedDialServers trackedServers;
105 |
106 | private Handler handler = new Handler();
107 |
108 | /**
109 | * Handler message number for a service update from broadcast client.
110 | */
111 | public static final int BROADCAST_RESPONSE = 100;
112 |
113 | /**
114 | * Handler message number for all delayed messages
115 | */
116 | private static final int DELAYED_MESSAGE = 101;
117 |
118 | private enum DelayedMessage {
119 | BROADCAST_TIMEOUT, DIAL_SERVER_FOUND;
120 |
121 | Message obtainMessage(Handler handler) {
122 | Message message = handler.obtainMessage(DELAYED_MESSAGE);
123 | message.obj = this;
124 | return message;
125 | }
126 | }
127 |
128 | public ServerFinder() {
129 | dataAdapter = new DeviceFinderListAdapter();
130 | trackedServers = new TrackedDialServers();
131 | }
132 |
133 | @Override
134 | protected void onCreate(Bundle savedInstanceState) {
135 | super.onCreate(savedInstanceState);
136 |
137 | setContentView(R.layout.device_finder_layout);
138 |
139 | previousDialServer = getIntent().getParcelableExtra(EXTRA_DIAL_SERVER);
140 |
141 | recentlyConnectedServers = getIntent().getParcelableArrayListExtra(EXTRA_RECENTLY_CONNECTED);
142 |
143 | broadcastHandler = new BroadcastHandler();
144 | wifiManager = (WifiManager) getSystemService(WIFI_SERVICE);
145 |
146 | stbList = (ListView) findViewById(R.id.stb_list);
147 | stbList.setOnItemClickListener(selectHandler);
148 | stbList.setAdapter(dataAdapter);
149 |
150 | ((Button) findViewById(R.id.button_manual)).setOnClickListener(new View.OnClickListener() {
151 | public void onClick(View v) {
152 | buildManualIpDialog().show();
153 | }
154 | });
155 |
156 | Analytics.createAnalytics(this);
157 | }
158 |
159 | private void showOtherDevices() {
160 | broadcastHandler.removeMessages(DELAYED_MESSAGE);
161 | if (progressDialog.isShowing()) {
162 | try {
163 | progressDialog.dismiss();
164 | } catch (Throwable e) {
165 | Log.e(LOG_TAG, "showOtherDevices", e);
166 | }
167 | }
168 | if (confirmationDialog != null && confirmationDialog.isShowing()) {
169 | try {
170 | confirmationDialog.dismiss();
171 | } catch (Throwable e) {
172 | Log.e(LOG_TAG, "showOtherDevices", e);
173 | }
174 | }
175 | findViewById(R.id.device_finder).setVisibility(View.VISIBLE);
176 | }
177 |
178 | @Override
179 | protected void onStart() {
180 | super.onStart();
181 |
182 | Analytics.startAnalytics(this);
183 |
184 | try {
185 | broadcastAddress = InetAddress.getByName("239.255.255.250");
186 | } catch (UnknownHostException e) {
187 | Log.e(LOG_TAG, "broadcastAddress", e);
188 | }
189 |
190 | startBroadcast();
191 | }
192 |
193 | @Override
194 | protected void onPause() {
195 | active = false;
196 | broadcastHandler.removeMessages(DELAYED_MESSAGE);
197 | super.onPause();
198 | }
199 |
200 | @Override
201 | protected void onResume() {
202 | super.onResume();
203 | active = true;
204 | }
205 |
206 | @Override
207 | protected void onStop() {
208 | if (null != broadcastClient) {
209 | broadcastClient.stop();
210 | broadcastClient = null;
211 | }
212 |
213 | super.onStop();
214 | Analytics.stopAnalytics(this);
215 | }
216 |
217 | @Override
218 | protected void onActivityResult(int requestCode, int resultCode, Intent data) {
219 | super.onActivityResult(requestCode, resultCode, data);
220 | Log.d(LOG_TAG, "ActivityResult: " + requestCode + ", " + resultCode);
221 | if (requestCode == CODE_WIFI_SETTINGS) {
222 | if (!isWifiAvailable()) {
223 | buildNoWifiDialog().show();
224 | } else {
225 | startBroadcast();
226 | }
227 | }
228 | }
229 |
230 | private OnItemClickListener selectHandler = new OnItemClickListener() {
231 | public void onItemClick(AdapterView> parent, View v, int position, long id) {
232 | DialServer DialServer = (DialServer) parent.getItemAtPosition(position);
233 | if (DialServer != null) {
234 | connectToEntry(DialServer);
235 | }
236 | }
237 | };
238 |
239 | /**
240 | * Connects to the chosen entry in the list. Finishes the activity and
241 | * returns the informations on the chosen box.
242 | *
243 | * @param DialServer
244 | * the listEntry representing the box you want to connect to
245 | */
246 | private void connectToEntry(DialServer DialServer) {
247 | Intent resultIntent = new Intent();
248 | resultIntent.putExtra(EXTRA_DIAL_SERVER, DialServer);
249 | setResult(RESULT_OK, resultIntent);
250 | finish();
251 | }
252 |
253 | private void startBroadcast() {
254 | if (!isWifiAvailable()) {
255 | try {
256 | buildNoWifiDialog().show();
257 | } catch (Exception e) {
258 | Log.e(LOG_TAG, "startBroadcast", e);
259 | }
260 | return;
261 | }
262 | try {
263 | broadcastClient = new BroadcastDiscoveryClient(broadcastAddress, broadcastHandler);
264 | broadcastClientThread = new Thread(broadcastClient);
265 | broadcastClientThread.start();
266 | Message message = DelayedMessage.BROADCAST_TIMEOUT.obtainMessage(broadcastHandler);
267 | broadcastHandler.sendMessageDelayed(message, getResources().getInteger(R.integer.broadcast_timeout));
268 | showProgressDialog(buildBroadcastProgressDialog());
269 | } catch (RuntimeException e) {
270 | Log.e(LOG_TAG, "startBroadcast", e);
271 | }
272 | }
273 |
274 | private void enableWifi() {
275 | if (null != wifiManager) {
276 | wifiManager.setWifiEnabled(true);
277 | }
278 | }
279 |
280 | /**
281 | * Returns an intent that starts this activity.
282 | */
283 | public static Intent createConnectIntent(Context ctx, DialServer recentlyConnected, ArrayList recentlyConnectedList) {
284 | Intent intent = new Intent(ctx, ServerFinder.class);
285 | intent.putExtra(EXTRA_DIAL_SERVER, recentlyConnected);
286 | intent.putParcelableArrayListExtra(EXTRA_RECENTLY_CONNECTED, recentlyConnectedList);
287 | return intent;
288 | }
289 |
290 | private class DeviceFinderListAdapter extends BaseAdapter {
291 | public int getCount() {
292 | return getTotalSize();
293 | }
294 |
295 | @Override
296 | public boolean hasStableIds() {
297 | return false;
298 | }
299 |
300 | @Override
301 | public boolean areAllItemsEnabled() {
302 | return false;
303 | }
304 |
305 | @Override
306 | public boolean isEnabled(int position) {
307 | return position != trackedServers.size();
308 | }
309 |
310 | public Object getItem(int position) {
311 | return getDialServer(position);
312 | }
313 |
314 | public long getItemId(int position) {
315 | return position;
316 | }
317 |
318 | public View getView(int position, View convertView, ViewGroup parent) {
319 | ListEntryView liv;
320 |
321 | if (position == trackedServers.size()) {
322 | return getLayoutInflater().inflate(R.layout.device_list_separator_layout, null);
323 | }
324 |
325 | if (convertView == null || !(convertView instanceof ListEntryView)) {
326 | liv = (ListEntryView) getLayoutInflater().inflate(R.layout.device_list_item_layout, null);
327 | } else {
328 | liv = (ListEntryView) convertView;
329 | }
330 |
331 | liv.setListEntry(getDialServer(position));
332 | return liv;
333 | }
334 |
335 | private int getTotalSize() {
336 | return trackedServers.size();
337 | }
338 |
339 | private DialServer getDialServer(int position) {
340 | if (position < trackedServers.size()) {
341 | return trackedServers.get(position);
342 | }
343 | return null;
344 | }
345 | }
346 |
347 | /**
348 | * Represents an entry in the box list.
349 | */
350 | public static class ListEntryView extends LinearLayout {
351 |
352 | public ListEntryView(Context context, AttributeSet attrs) {
353 | super(context, attrs);
354 | myContext = context;
355 | }
356 |
357 | public ListEntryView(Context context) {
358 | super(context);
359 | myContext = context;
360 | }
361 |
362 | @Override
363 | protected void onFinishInflate() {
364 | super.onFinishInflate();
365 | tvName = (TextView) findViewById(R.id.device_list_item_name);
366 | tvTargetAddr = (TextView) findViewById(R.id.device_list_target_addr);
367 | }
368 |
369 | private void updateContents() {
370 | if (null != tvName) {
371 | String txt = myContext.getString(R.string.unkown_tgt_name);
372 | if (null != listEntry) {
373 | txt = formatName(listEntry);
374 | }
375 | tvName.setText(txt);
376 | }
377 |
378 | if (null != tvTargetAddr) {
379 | String txt = myContext.getString(R.string.unkown_tgt_addr);
380 | if ((null != listEntry) && (null != listEntry.getIpAddress())) {
381 | txt = listEntry.getIpAddress().getHostAddress();
382 | }
383 | tvTargetAddr.setText(txt);
384 | }
385 | }
386 |
387 | public DialServer getListEntry() {
388 | return listEntry;
389 | }
390 |
391 | public void setListEntry(DialServer listEntry) {
392 | this.listEntry = listEntry;
393 | updateContents();
394 | }
395 |
396 | private Context myContext = null;
397 | private DialServer listEntry = null;
398 | private TextView tvName = null;
399 | private TextView tvTargetAddr = null;
400 | }
401 |
402 | private final class BroadcastHandler extends Handler {
403 | /** {inheritDoc} */
404 | @Override
405 | public void handleMessage(Message msg) {
406 | if (msg.what == DELAYED_MESSAGE) {
407 | if (!active) {
408 | return;
409 | }
410 | switch ((DelayedMessage) msg.obj) {
411 | case BROADCAST_TIMEOUT:
412 | try {
413 | broadcastClient.stop();
414 | try {
415 | if (progressDialog.isShowing()) {
416 | progressDialog.dismiss();
417 | }
418 | } catch (Throwable e) {
419 | Log.e(LOG_TAG, "handleMessage", e);
420 | }
421 | buildBroadcastTimeoutDialog().show();
422 | } catch (Exception e1) {
423 | Log.e(LOG_TAG, "handleMessage", e1);
424 | }
425 | break;
426 |
427 | case DIAL_SERVER_FOUND:
428 | try {
429 | // Check if there is previously connected remote and
430 | // suggest it
431 | // for connection:
432 | DialServer toConnect = null;
433 | if (previousDialServer != null) {
434 | Log.d(LOG_TAG, "Previous Remote Device: " + previousDialServer);
435 | toConnect = trackedServers.findDialServer(previousDialServer);
436 | }
437 | if (toConnect == null) {
438 | Log.d(LOG_TAG, "No previous device found.");
439 | // No default found - suggest any device
440 | toConnect = trackedServers.get(0);
441 | }
442 |
443 | try {
444 | progressDialog.dismiss();
445 | } catch (Throwable e) {
446 | Log.e(LOG_TAG, "handleMessage", e);
447 | }
448 | confirmationDialog = buildConfirmationDialog(toConnect);
449 | confirmationDialog.show();
450 | } catch (Exception e) {
451 | Log.e(LOG_TAG, "handleMessage", e);
452 | }
453 | break;
454 | }
455 | }
456 |
457 | switch (msg.what) {
458 | case BROADCAST_RESPONSE:
459 | final BroadcastAdvertisement advert = (BroadcastAdvertisement) msg.obj;
460 | if (advert.getLocation() != null) {
461 | new Thread(new Runnable() {
462 | public void run() {
463 | HttpResponse response = new HttpRequestHelper().sendHttpGet(advert.getLocation());
464 | if (response != null) {
465 | String appsUrl = null;
466 | Header header = response.getLastHeader(HEADER_APPLICATION_URL);
467 | if (header != null) {
468 | appsUrl = header.getValue();
469 | Log.d(LOG_TAG, "appsUrl="+appsUrl);
470 | }
471 | String friendlyName = null;
472 | String manufacturer = null;
473 | String modelName = null;
474 | String uuid = null;
475 | try {
476 | InputStream inputStream = response.getEntity().getContent();
477 | BufferedReader reader = new BufferedReader(new InputStreamReader(inputStream));
478 | XmlPullParserFactory factory = XmlPullParserFactory.newInstance();
479 | factory.setNamespaceAware(true);
480 | XmlPullParser parser = factory.newPullParser();
481 | parser.setInput(reader);
482 | int eventType = parser.getEventType();
483 | String lastTagName = null;
484 | while (eventType != XmlPullParser.END_DOCUMENT) {
485 | switch (eventType) {
486 | case XmlPullParser.START_DOCUMENT:
487 | break;
488 | case XmlPullParser.START_TAG:
489 | String tagName = parser.getName();
490 | lastTagName = tagName;
491 | break;
492 | case XmlPullParser.TEXT:
493 | if (lastTagName != null) {
494 | if ("friendlyName".equals(lastTagName)) {
495 | friendlyName = parser.getText();
496 | } else if ("UDN".equals(lastTagName)) {
497 | uuid = parser.getText();
498 | } else if ("manufacturer".equals(lastTagName)) {
499 | manufacturer = parser.getText();
500 | } else if ("modelName".equals(lastTagName)) {
501 | modelName = parser.getText();
502 | }
503 | }
504 | break;
505 | case XmlPullParser.END_TAG:
506 | tagName = parser.getName();
507 | lastTagName = null;
508 | break;
509 | }
510 | eventType = parser.next();
511 | }
512 | inputStream.close();
513 | } catch (Exception e) {
514 | Log.e(LOG_TAG, "parse device description", e);
515 | }
516 | Log.d(LOG_TAG, "friendlyName="+friendlyName);
517 | final DialServer dialServer = new DialServer(advert.getLocation(), advert.getIpAddress(), advert.getPort(), appsUrl, friendlyName, uuid, manufacturer, modelName);
518 | handler.post(new Runnable() {
519 | public void run() {
520 | handleDialServerAdd(dialServer);
521 | }
522 | });
523 | }
524 | }
525 | }).start();
526 | }
527 | break;
528 | }
529 | }
530 | }
531 |
532 | private void handleDialServerAdd(final DialServer dialServer) {
533 | if (trackedServers.add(dialServer)) {
534 | Log.v(LOG_TAG, "Adding new device: " + dialServer);
535 |
536 | // Notify data adapter and update title.
537 | dataAdapter.notifyDataSetChanged();
538 |
539 | // Show confirmation dialog only for the first STB and only if
540 | // progress
541 | // dialog is visible.
542 | if ((trackedServers.size() == 1) && progressDialog.isShowing()) {
543 | broadcastHandler.removeMessages(DELAYED_MESSAGE);
544 | // delayed automatic adding
545 | Message message = DelayedMessage.DIAL_SERVER_FOUND.obtainMessage(broadcastHandler);
546 | broadcastHandler.sendMessageDelayed(message, getResources().getInteger(R.integer.gtv_finder_reconnect_delay));
547 | }
548 | }
549 | }
550 |
551 | private ProgressDialog buildBroadcastProgressDialog() {
552 | String message;
553 | String networkName = getNetworkName();
554 | if (!TextUtils.isEmpty(networkName)) {
555 | message = getString(R.string.finder_searching_with_ssid, networkName);
556 | } else {
557 | message = getString(R.string.finder_searching);
558 | }
559 |
560 | return buildProgressDialog(message, new DialogInterface.OnClickListener() {
561 | public void onClick(DialogInterface dialogInterface, int which) {
562 | broadcastHandler.removeMessages(DELAYED_MESSAGE);
563 | showOtherDevices();
564 | }
565 | });
566 | }
567 |
568 | private ProgressDialog buildProgressDialog(String message, DialogInterface.OnClickListener cancelListener) {
569 | ProgressDialog dialog = new ProgressDialog(this);
570 | dialog.setMessage(message);
571 | dialog.setOnKeyListener(new DialogInterface.OnKeyListener() {
572 | public boolean onKey(DialogInterface dialogInterface, int which, KeyEvent event) {
573 | if (event.getKeyCode() == KeyEvent.KEYCODE_BACK) {
574 | finish();
575 | return true;
576 | }
577 | return false;
578 | }
579 | });
580 | dialog.setButton(getString(R.string.finder_cancel), cancelListener);
581 | return dialog;
582 | }
583 |
584 | private AlertDialog buildConfirmationDialog(final DialServer dialServer) {
585 | AlertDialog.Builder builder = new AlertDialog.Builder(this);
586 | View view = LayoutInflater.from(this).inflate(R.layout.device_info, null);
587 | final TextView ipTextView = (TextView) view.findViewById(R.id.device_info_ip_address);
588 |
589 | builder.setMessage(formatName(dialServer));
590 | ipTextView.setText(dialServer.getIpAddress().getHostAddress());
591 |
592 | return builder.setTitle(R.string.finder_label).setCancelable(false).setPositiveButton(R.string.finder_connect, new DialogInterface.OnClickListener() {
593 | public void onClick(DialogInterface dialogInterface, int id) {
594 | connectToEntry(dialServer);
595 | }
596 | }).setNegativeButton(R.string.finder_add_other, new DialogInterface.OnClickListener() {
597 | public void onClick(DialogInterface dialogInterface, int id) {
598 | showOtherDevices();
599 | }
600 | }).create();
601 | }
602 |
603 | private static final String formatName(DialServer dialServer) {
604 | StringBuffer buffer = new StringBuffer();
605 | if (dialServer.getFriendlyName() != null) {
606 | buffer.append(dialServer.getFriendlyName());
607 | if (dialServer.getModelName() != null) {
608 | buffer.append("/").append(dialServer.getModelName());
609 | }
610 | if (dialServer.getManufacturer() != null) {
611 | buffer.append("/").append(dialServer.getManufacturer());
612 | }
613 | } else {
614 | if (dialServer.getModelName() != null) {
615 | buffer.append("/").append(dialServer.getModelName());
616 | }
617 | if (dialServer.getManufacturer() != null) {
618 | buffer.append("/").append(dialServer.getManufacturer());
619 | }
620 | }
621 | return buffer.toString();
622 | }
623 |
624 | private AlertDialog buildManualIpDialog() {
625 | AlertDialog.Builder builder = new AlertDialog.Builder(this);
626 | View view = LayoutInflater.from(this).inflate(R.layout.manual_ip, null);
627 | final EditText ipEditText = (EditText) view.findViewById(R.id.manual_ip_entry);
628 |
629 | ipEditText.setFilters(new InputFilter[] { new NumberKeyListener() {
630 | @Override
631 | protected char[] getAcceptedChars() {
632 | return "0123456789.:".toCharArray();
633 | }
634 |
635 | public int getInputType() {
636 | return InputType.TYPE_CLASS_NUMBER;
637 | }
638 | } });
639 |
640 | SharedPreferences settings = getSharedPreferences(MainActivity.PREFS_NAME, Activity.MODE_PRIVATE);
641 | String ip = settings.getString(MANUAL_IP_ADDRESS, "");
642 | ipEditText.setText(ip);
643 |
644 | builder.setPositiveButton(R.string.manual_ip_connect, new DialogInterface.OnClickListener() {
645 | public void onClick(DialogInterface dialog, int which) {
646 | DialServer DialServer = DialServerFromString(ipEditText.getText().toString());
647 | if (DialServer != null) {
648 | connectToEntry(DialServer);
649 | try {
650 | SharedPreferences settings = getSharedPreferences(MainActivity.PREFS_NAME, Activity.MODE_PRIVATE);
651 | SharedPreferences.Editor editor = settings.edit();
652 | editor.putString(MANUAL_IP_ADDRESS, ipEditText.getText().toString());
653 | editor.commit();
654 | } catch (Exception e) {
655 | }
656 | } else {
657 | Toast.makeText(ServerFinder.this, getString(R.string.manual_ip_error_address), Toast.LENGTH_LONG).show();
658 | }
659 | }
660 | }).setNegativeButton(R.string.manual_ip_cancel, new DialogInterface.OnClickListener() {
661 | public void onClick(DialogInterface dialog, int which) {
662 | // do nothing
663 | }
664 | }).setCancelable(true).setTitle(R.string.manual_ip_label).setMessage(R.string.manual_ip_entry_label).setView(view);
665 | return builder.create();
666 | }
667 |
668 | private AlertDialog buildBroadcastTimeoutDialog() {
669 | String message;
670 | String networkName = getNetworkName();
671 | if (!TextUtils.isEmpty(networkName)) {
672 | message = getString(R.string.finder_no_devices_with_ssid, networkName);
673 | } else {
674 | message = getString(R.string.finder_no_devices);
675 | }
676 |
677 | return buildTimeoutDialog(message, new DialogInterface.OnClickListener() {
678 | public void onClick(DialogInterface dialogInterface, int id) {
679 | startBroadcast();
680 | }
681 | });
682 | }
683 |
684 | private AlertDialog buildTimeoutDialog(CharSequence message, DialogInterface.OnClickListener retryListener) {
685 | AlertDialog.Builder builder = new AlertDialog.Builder(this);
686 |
687 | return builder.setMessage(message).setCancelable(false).setPositiveButton(R.string.finder_manual, new DialogInterface.OnClickListener() {
688 | public void onClick(DialogInterface dialogInterface, int id) {
689 | buildManualIpDialog().show();
690 | }
691 | }).setNegativeButton(R.string.finder_cancel, new DialogInterface.OnClickListener() {
692 | public void onClick(DialogInterface dialogInterface, int id) {
693 | setResult(RESULT_CANCELED, null);
694 | finish();
695 | }
696 | }).create();
697 | }
698 |
699 | private AlertDialog buildNoWifiDialog() {
700 | AlertDialog.Builder builder = new AlertDialog.Builder(this);
701 |
702 | builder.setMessage(R.string.finder_wifi_not_available);
703 | builder.setCancelable(false);
704 | builder.setPositiveButton(R.string.finder_configure, new DialogInterface.OnClickListener() {
705 | public void onClick(DialogInterface dialogInterface, int id) {
706 | Intent intent = new Intent(Settings.ACTION_WIRELESS_SETTINGS);
707 | startActivityForResult(intent, CODE_WIFI_SETTINGS);
708 | }
709 | });
710 | builder.setNegativeButton(R.string.finder_cancel, new DialogInterface.OnClickListener() {
711 | public void onClick(DialogInterface dialogInterface, int id) {
712 | setResult(RESULT_CANCELED, null);
713 | finish();
714 | }
715 | });
716 | return builder.create();
717 | }
718 |
719 | private void showProgressDialog(ProgressDialog newDialog) {
720 | try {
721 | try {
722 | if ((progressDialog != null) && progressDialog.isShowing()) {
723 | progressDialog.dismiss();
724 | }
725 | } catch (Throwable e) {
726 | Log.e(LOG_TAG, "showProgressDialog", e);
727 | }
728 | progressDialog = newDialog;
729 | newDialog.show();
730 | } catch (Exception e) {
731 | Log.e(LOG_TAG, "showProgressDialog", e);
732 | }
733 | }
734 |
735 | private DialServer DialServerFromString(String text) {
736 | String[] ipPort = text.split(":");
737 | int port;
738 | if (ipPort.length == 1) {
739 | port = getResources().getInteger(R.integer.manual_default_port);
740 | } else if (ipPort.length == 2) {
741 | try {
742 | port = Integer.parseInt(ipPort[1]);
743 | } catch (NumberFormatException e) {
744 | return null;
745 | }
746 | } else {
747 | return null;
748 | }
749 |
750 | try {
751 | InetAddress address = InetAddress.getByName(ipPort[0]);
752 | // TODO
753 | return new DialServer(null, address, port, null, getString(R.string.manual_ip_default_box_name), null, null, null);
754 | } catch (UnknownHostException e) {
755 | }
756 | return null;
757 | }
758 |
759 | private boolean isSimulator() {
760 | return Build.FINGERPRINT.startsWith("generic");
761 | }
762 |
763 | private boolean isWifiAvailable() {
764 | try {
765 | if (isSimulator()) {
766 | return true;
767 | }
768 | if (!wifiManager.isWifiEnabled()) {
769 | return false;
770 | }
771 | WifiInfo info = wifiManager.getConnectionInfo();
772 | return info != null && info.getIpAddress() != 0;
773 | } catch (Exception e) {
774 | Log.e(LOG_TAG, "isWifiAvailable", e);
775 | }
776 | return false;
777 | }
778 |
779 | private String getNetworkName() {
780 | if (isSimulator()) {
781 | return "generic";
782 | }
783 | if (!isWifiAvailable()) {
784 | return null;
785 | }
786 | WifiInfo info = wifiManager.getConnectionInfo();
787 | return info != null ? info.getSSID() : null;
788 | }
789 | }
790 |
--------------------------------------------------------------------------------
/src/com/entertailion/android/dial/TrackedDialServers.java:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright (C) 2013 ENTERTAILION, LLC.
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
5 | * in compliance with the License. You may obtain a copy of the License at
6 | *
7 | * http://www.apache.org/licenses/LICENSE-2.0
8 | *
9 | * Unless required by applicable law or agreed to in writing, software distributed under the License
10 | * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
11 | * or implied. See the License for the specific language governing permissions and limitations under
12 | * the License.
13 | */
14 | package com.entertailion.android.dial;
15 |
16 | import java.net.InetAddress;
17 | import java.util.Comparator;
18 | import java.util.HashMap;
19 | import java.util.Iterator;
20 | import java.util.Map;
21 | import java.util.SortedSet;
22 | import java.util.TreeSet;
23 |
24 | import android.util.Log;
25 |
26 | // Keep track of the found DIAL servers
27 | public class TrackedDialServers implements Iterable {
28 | private static final String LOG_TAG = "TrackedDialServers";
29 |
30 | private final Map serversByAddress;
31 | private final SortedSet servers;
32 | private DialServer[] serverArray;
33 |
34 | private static Comparator COMPARATOR = new Comparator() {
35 | public int compare(DialServer remote1, DialServer remote2) {
36 | int result = remote1.getFriendlyName().compareToIgnoreCase(remote2.getFriendlyName());
37 | if (result != 0) {
38 | return result;
39 | }
40 | return remote1.getIpAddress().getHostAddress().compareTo(remote2.getIpAddress().getHostAddress());
41 | }
42 | };
43 |
44 | TrackedDialServers() {
45 | serversByAddress = new HashMap();
46 | servers = new TreeSet(COMPARATOR);
47 | }
48 |
49 | public boolean add(DialServer DialServer) {
50 | InetAddress address = DialServer.getIpAddress();
51 | if (!serversByAddress.containsKey(address)) {
52 | serversByAddress.put(address, DialServer);
53 | servers.add(DialServer);
54 | serverArray = null;
55 | return true;
56 | }
57 | return false;
58 | }
59 |
60 | public int size() {
61 | return servers.size();
62 | }
63 |
64 | public DialServer get(int index) {
65 | return getServerArray()[index];
66 | }
67 |
68 | private DialServer[] getServerArray() {
69 | if (serverArray == null) {
70 | serverArray = servers.toArray(new DialServer[0]);
71 | }
72 | return serverArray;
73 | }
74 |
75 | public Iterator iterator() {
76 | return servers.iterator();
77 | }
78 |
79 | public DialServer findDialServer(DialServer DialServer) {
80 | DialServer byIp = serversByAddress.get(DialServer.getIpAddress());
81 | if (byIp != null && byIp.getFriendlyName().equals(DialServer.getFriendlyName())) {
82 | return byIp;
83 | }
84 |
85 | for (DialServer server : servers) {
86 | Log.d(LOG_TAG, "New server: " + server);
87 | if (DialServer.getFriendlyName().equals(server.getFriendlyName())) {
88 | return server;
89 | }
90 | }
91 | return byIp;
92 | }
93 |
94 | public TrackedDialServers clone() {
95 | TrackedDialServers trackedServers = new TrackedDialServers();
96 | for (DialServer server : servers) {
97 | trackedServers.add(server.clone());
98 | }
99 | return trackedServers;
100 | }
101 | }
--------------------------------------------------------------------------------
/src/com/entertailion/android/dial/Utils.java:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright (C) 2013 ENTERTAILION, LLC.
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
5 | * in compliance with the License. You may obtain a copy of the License at
6 | *
7 | * http://www.apache.org/licenses/LICENSE-2.0
8 | *
9 | * Unless required by applicable law or agreed to in writing, software distributed under the License
10 | * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
11 | * or implied. See the License for the specific language governing permissions and limitations under
12 | * the License.
13 | */
14 | package com.entertailion.android.dial;
15 |
16 | import java.io.IOException;
17 | import java.net.Inet4Address;
18 | import java.net.InetAddress;
19 | import java.net.NetworkInterface;
20 | import java.util.Enumeration;
21 |
22 | import android.content.Context;
23 | import android.net.DhcpInfo;
24 | import android.net.wifi.WifiManager;
25 | import android.util.Log;
26 |
27 |
28 | public class Utils {
29 |
30 | private static final String LOG_TAG = "Utils";
31 |
32 | public static InetAddress getLocalInetAddress() {
33 | InetAddress selectedInetAddress = null;
34 | try {
35 | for (Enumeration en = NetworkInterface.getNetworkInterfaces(); en.hasMoreElements();) {
36 | NetworkInterface intf = en.nextElement();
37 | if (intf.isUp()) {
38 | for (Enumeration enumIpAddr = intf.getInetAddresses(); enumIpAddr.hasMoreElements();) {
39 | InetAddress inetAddress = enumIpAddr.nextElement();
40 | if (!inetAddress.isLoopbackAddress()) {
41 | if (inetAddress instanceof Inet4Address) { // only want ipv4 address
42 | if (inetAddress.getHostAddress().toString().charAt(0)!='0') {
43 | if (selectedInetAddress==null) {
44 | selectedInetAddress = inetAddress;
45 | } else if (intf.getName().startsWith("wlan")) { // prefer wlan interface
46 | selectedInetAddress = inetAddress;
47 | }
48 | }
49 | }
50 | }
51 | }
52 | }
53 | }
54 | return selectedInetAddress;
55 | } catch (Throwable e) {
56 | Log.e(LOG_TAG, "Failed to get the IP address", e);
57 | }
58 | return null;
59 | }
60 |
61 | public static InetAddress getBroadcastAddress(Context context) throws IOException {
62 | WifiManager wifi = (WifiManager) context.getSystemService(Context.WIFI_SERVICE);
63 | DhcpInfo dhcp = wifi.getDhcpInfo();
64 | int broadcast = (dhcp.ipAddress & dhcp.netmask) | ~dhcp.netmask;
65 | byte[] quads = new byte[4];
66 | for (int k = 0; k < 4; k++) {
67 | quads[k] = (byte) ((broadcast >> k * 8) & 0xFF);
68 | }
69 | return InetAddress.getByAddress(quads);
70 | }
71 | }
72 |
--------------------------------------------------------------------------------