├── ic_launcher-web.png
├── libs
└── android-support-v4.jar
├── res
├── drawable-hdpi
│ └── ic_launcher.png
├── drawable-mdpi
│ └── ic_launcher.png
├── drawable-xhdpi
│ └── ic_launcher.png
├── drawable-xxhdpi
│ └── ic_launcher.png
├── values-sw600dp
│ └── dimens.xml
├── values
│ ├── dimens.xml
│ ├── styles.xml
│ └── strings.xml
├── menu
│ ├── call.xml
│ ├── main.xml
│ ├── make_call.xml
│ └── receive_call.xml
├── values-sw720dp-land
│ └── dimens.xml
├── values-v11
│ └── styles.xml
├── values-v14
│ └── styles.xml
└── layout
│ ├── activity_make_call.xml
│ ├── activity_receive_call.xml
│ └── activity_main.xml
├── project.properties
├── proguard-project.txt
├── .gitignore
├── AndroidManifest.xml
├── README.md
└── src
└── hw
└── dt83
└── udpchat
├── AudioCall.java
├── MakeCallActivity.java
├── ReceiveCallActivity.java
├── ContactManager.java
└── MainActivity.java
/ic_launcher-web.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/DeanThomson/android-udp-audio-chat/HEAD/ic_launcher-web.png
--------------------------------------------------------------------------------
/libs/android-support-v4.jar:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/DeanThomson/android-udp-audio-chat/HEAD/libs/android-support-v4.jar
--------------------------------------------------------------------------------
/res/drawable-hdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/DeanThomson/android-udp-audio-chat/HEAD/res/drawable-hdpi/ic_launcher.png
--------------------------------------------------------------------------------
/res/drawable-mdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/DeanThomson/android-udp-audio-chat/HEAD/res/drawable-mdpi/ic_launcher.png
--------------------------------------------------------------------------------
/res/drawable-xhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/DeanThomson/android-udp-audio-chat/HEAD/res/drawable-xhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/res/drawable-xxhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/DeanThomson/android-udp-audio-chat/HEAD/res/drawable-xxhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/res/values-sw600dp/dimens.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
7 |
8 |
--------------------------------------------------------------------------------
/res/values/dimens.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | 16dp
5 | 16dp
6 |
7 |
--------------------------------------------------------------------------------
/res/menu/call.xml:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/res/menu/main.xml:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/res/menu/make_call.xml:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/res/menu/receive_call.xml:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/res/values-sw720dp-land/dimens.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
7 | 128dp
8 |
9 |
--------------------------------------------------------------------------------
/res/values-v11/styles.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
7 |
10 |
11 |
--------------------------------------------------------------------------------
/res/values-v14/styles.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
8 |
11 |
12 |
--------------------------------------------------------------------------------
/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/values/styles.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
7 |
14 |
15 |
16 |
19 |
20 |
--------------------------------------------------------------------------------
/proguard-project.txt:
--------------------------------------------------------------------------------
1 | # To enable ProGuard in your project, edit project.properties
2 | # to define the proguard.config property as described in that file.
3 | #
4 | # Add project specific ProGuard rules here.
5 | # By default, the flags in this file are appended to flags specified
6 | # in ${sdk.dir}/tools/proguard/proguard-android.txt
7 | # You can edit the include path and order by changing the ProGuard
8 | # include property in project.properties.
9 | #
10 | # For more details, see
11 | # http://developer.android.com/guide/developing/tools/proguard.html
12 |
13 | # Add any project specific keep options here:
14 |
15 | # If your project uses WebView with JS, uncomment the following
16 | # and specify the fully qualified class name to the JavaScript interface
17 | # class:
18 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview {
19 | # public *;
20 | #}
21 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # built application files
2 | *.apk
3 | *.ap_
4 |
5 | # files for the dex VM
6 | *.dex
7 |
8 | # Java class files
9 | *.class
10 |
11 | # generated files
12 | bin/
13 | gen/
14 |
15 | # Local configuration file (sdk path, etc)
16 | local.properties
17 |
18 | # Eclipse project files
19 | .classpath
20 | .project
21 |
22 | # Proguard folder generated by Eclipse
23 | proguard/
24 |
25 | # Intellij project files
26 | *.iml
27 | *.ipr
28 | *.iws
29 | .idea/
30 |
31 | *.pydevproject
32 | .project
33 | .metadata
34 | bin/**
35 | tmp/**
36 | tmp/**/*
37 | *.tmp
38 | *.bak
39 | *.swp
40 | *~.nib
41 | local.properties
42 | .classpath
43 | .settings/
44 | .loadpath
45 |
46 | # External tool builders
47 | .externalToolBuilders/
48 |
49 | # Locally stored "Eclipse launch configurations"
50 | *.launch
51 |
52 | # CDT-specific
53 | .cproject
54 |
55 | # PDT-specific
56 | .buildpath
57 |
--------------------------------------------------------------------------------
/res/values/strings.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | UDPchat
5 | Settings
6 | Enter a display name:
7 | Start
8 | Call Contact
9 | Select a contact to call:
10 | Update Contacts
11 | Hello world!
12 | UDPChat
13 | "Calling: "
14 | Mute Mic
15 | End Call
16 | UDPChat
17 | "Incoming call: "
18 | Accept
19 | Reject
20 |
21 |
--------------------------------------------------------------------------------
/res/layout/activity_make_call.xml:
--------------------------------------------------------------------------------
1 |
12 |
13 |
20 |
21 |
26 |
27 |
32 |
33 |
34 |
35 |
--------------------------------------------------------------------------------
/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
2 |
6 |
7 |
10 |
11 |
12 |
13 |
14 |
15 |
20 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
34 |
35 |
39 |
40 |
41 |
42 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | #Android UDP Audio Chat Application
2 |
3 | ###What is it?
4 |
5 | A simple UDP audio chat application for Android, developed for my final year university project.
6 |
7 | This application is designed to allow users of Android devices to engage in audio conversations over a local wireless network. The application has the ability to discover other devices on the network which are currently running the application. Users choose a display name for their device which will be broadcast across the network and shown to other users who are currently running the application. Users can select one of the display names and attempt to initiate an audio chat session with the chosen device.
8 |
9 | ### Requirements
10 |
11 | - At least API version 10 (Android version 2.3.3)
12 | - A network that delivers UDP broadcast packets
13 | - Currently, devices must be on the same subnet
14 |
15 | ### License
16 |
17 | The MIT License (MIT)
18 |
19 | Copyright (c) 2013 Dean Thomson
20 |
21 | Permission is hereby granted, free of charge, to any person obtaining a copy
22 | of this software and associated documentation files (the "Software"), to deal
23 | in the Software without restriction, including without limitation the rights
24 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
25 | copies of the Software, and to permit persons to whom the Software is
26 | furnished to do so, subject to the following conditions:
27 |
28 | The above copyright notice and this permission notice shall be included in
29 | all copies or substantial portions of the Software.
30 |
31 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
32 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
33 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
34 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
35 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
36 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
37 | THE SOFTWARE.
38 |
--------------------------------------------------------------------------------
/res/layout/activity_receive_call.xml:
--------------------------------------------------------------------------------
1 |
12 |
13 |
20 |
21 |
26 |
27 |
32 |
33 |
38 |
39 |
40 |
41 |
45 |
46 |
51 |
52 |
53 |
54 |
--------------------------------------------------------------------------------
/res/layout/activity_main.xml:
--------------------------------------------------------------------------------
1 |
12 |
13 |
18 |
19 |
22 |
23 |
32 |
33 |
34 |
35 |
36 |
43 |
44 |
45 |
51 |
52 |
55 |
56 |
63 |
64 |
71 |
72 |
73 |
78 |
79 |
83 |
84 |
85 |
86 |
87 |
--------------------------------------------------------------------------------
/src/hw/dt83/udpchat/AudioCall.java:
--------------------------------------------------------------------------------
1 | package hw.dt83.udpchat;
2 |
3 | import java.io.IOException;
4 | import java.net.DatagramPacket;
5 | import java.net.DatagramSocket;
6 | import java.net.InetAddress;
7 | import java.net.SocketException;
8 | import java.net.UnknownHostException;
9 |
10 | import android.media.AudioFormat;
11 | import android.media.AudioManager;
12 | import android.media.AudioRecord;
13 | import android.media.AudioTrack;
14 | import android.media.MediaRecorder;
15 | import android.util.Log;
16 |
17 | public class AudioCall {
18 |
19 | private static final String LOG_TAG = "AudioCall";
20 | private static final int SAMPLE_RATE = 8000; // Hertz
21 | private static final int SAMPLE_INTERVAL = 20; // Milliseconds
22 | private static final int SAMPLE_SIZE = 2; // Bytes
23 | private static final int BUF_SIZE = SAMPLE_INTERVAL * SAMPLE_INTERVAL * SAMPLE_SIZE * 2; //Bytes
24 | private InetAddress address; // Address to call
25 | private int port = 50000; // Port the packets are addressed to
26 | private boolean mic = false; // Enable mic?
27 | private boolean speakers = false; // Enable speakers?
28 |
29 | public AudioCall(InetAddress address) {
30 |
31 | this.address = address;
32 | }
33 |
34 | public void startCall() {
35 |
36 | startMic();
37 | startSpeakers();
38 | }
39 |
40 | public void endCall() {
41 |
42 | Log.i(LOG_TAG, "Ending call!");
43 | muteMic();
44 | muteSpeakers();
45 | }
46 |
47 | public void muteMic() {
48 |
49 | mic = false;
50 | }
51 |
52 | public void muteSpeakers() {
53 |
54 | speakers = false;
55 | }
56 |
57 | public void startMic() {
58 | // Creates the thread for capturing and transmitting audio
59 | mic = true;
60 | Thread thread = new Thread(new Runnable() {
61 |
62 | @Override
63 | public void run() {
64 | // Create an instance of the AudioRecord class
65 | Log.i(LOG_TAG, "Send thread started. Thread id: " + Thread.currentThread().getId());
66 | AudioRecord audioRecorder = new AudioRecord (MediaRecorder.AudioSource.VOICE_COMMUNICATION, SAMPLE_RATE,
67 | AudioFormat.CHANNEL_IN_MONO, AudioFormat.ENCODING_PCM_16BIT,
68 | AudioRecord.getMinBufferSize(SAMPLE_RATE, AudioFormat.CHANNEL_IN_MONO, AudioFormat.ENCODING_PCM_16BIT)*10);
69 | int bytes_read = 0;
70 | int bytes_sent = 0;
71 | byte[] buf = new byte[BUF_SIZE];
72 | try {
73 | // Create a socket and start recording
74 | Log.i(LOG_TAG, "Packet destination: " + address.toString());
75 | DatagramSocket socket = new DatagramSocket();
76 | audioRecorder.startRecording();
77 | while(mic) {
78 | // Capture audio from the mic and transmit it
79 | bytes_read = audioRecorder.read(buf, 0, BUF_SIZE);
80 | DatagramPacket packet = new DatagramPacket(buf, bytes_read, address, port);
81 | socket.send(packet);
82 | bytes_sent += bytes_read;
83 | Log.i(LOG_TAG, "Total bytes sent: " + bytes_sent);
84 | Thread.sleep(SAMPLE_INTERVAL, 0);
85 | }
86 | // Stop recording and release resources
87 | audioRecorder.stop();
88 | audioRecorder.release();
89 | socket.disconnect();
90 | socket.close();
91 | mic = false;
92 | return;
93 | }
94 | catch(InterruptedException e) {
95 |
96 | Log.e(LOG_TAG, "InterruptedException: " + e.toString());
97 | mic = false;
98 | }
99 | catch(SocketException e) {
100 |
101 | Log.e(LOG_TAG, "SocketException: " + e.toString());
102 | mic = false;
103 | }
104 | catch(UnknownHostException e) {
105 |
106 | Log.e(LOG_TAG, "UnknownHostException: " + e.toString());
107 | mic = false;
108 | }
109 | catch(IOException e) {
110 |
111 | Log.e(LOG_TAG, "IOException: " + e.toString());
112 | mic = false;
113 | }
114 | }
115 | });
116 | thread.start();
117 | }
118 |
119 | public void startSpeakers() {
120 | // Creates the thread for receiving and playing back audio
121 | if(!speakers) {
122 |
123 | speakers = true;
124 | Thread receiveThread = new Thread(new Runnable() {
125 |
126 | @Override
127 | public void run() {
128 | // Create an instance of AudioTrack, used for playing back audio
129 | Log.i(LOG_TAG, "Receive thread started. Thread id: " + Thread.currentThread().getId());
130 | AudioTrack track = new AudioTrack(AudioManager.STREAM_MUSIC, SAMPLE_RATE, AudioFormat.CHANNEL_OUT_MONO,
131 | AudioFormat.ENCODING_PCM_16BIT, BUF_SIZE, AudioTrack.MODE_STREAM);
132 | track.play();
133 | try {
134 | // Define a socket to receive the audio
135 | DatagramSocket socket = new DatagramSocket(port);
136 | byte[] buf = new byte[BUF_SIZE];
137 | while(speakers) {
138 | // Play back the audio received from packets
139 | DatagramPacket packet = new DatagramPacket(buf, BUF_SIZE);
140 | socket.receive(packet);
141 | Log.i(LOG_TAG, "Packet received: " + packet.getLength());
142 | track.write(packet.getData(), 0, BUF_SIZE);
143 | }
144 | // Stop playing back and release resources
145 | socket.disconnect();
146 | socket.close();
147 | track.stop();
148 | track.flush();
149 | track.release();
150 | speakers = false;
151 | return;
152 | }
153 | catch(SocketException e) {
154 |
155 | Log.e(LOG_TAG, "SocketException: " + e.toString());
156 | speakers = false;
157 | }
158 | catch(IOException e) {
159 |
160 | Log.e(LOG_TAG, "IOException: " + e.toString());
161 | speakers = false;
162 | }
163 | }
164 | });
165 | receiveThread.start();
166 | }
167 | }
168 | }
169 |
--------------------------------------------------------------------------------
/src/hw/dt83/udpchat/MakeCallActivity.java:
--------------------------------------------------------------------------------
1 | package hw.dt83.udpchat;
2 |
3 | import java.io.IOException;
4 | import java.net.DatagramPacket;
5 | import java.net.DatagramSocket;
6 | import java.net.InetAddress;
7 | import java.net.SocketException;
8 | import java.net.SocketTimeoutException;
9 | import java.net.UnknownHostException;
10 |
11 | import android.app.Activity;
12 | import android.content.Intent;
13 | import android.os.Bundle;
14 | import android.util.Log;
15 | import android.view.Menu;
16 | import android.view.View;
17 | import android.view.View.OnClickListener;
18 | import android.widget.Button;
19 | import android.widget.TextView;
20 |
21 | public class MakeCallActivity extends Activity {
22 |
23 | private static final String LOG_TAG = "MakeCall";
24 | private static final int BROADCAST_PORT = 50002;
25 | private static final int BUF_SIZE = 1024;
26 | private String displayName;
27 | private String contactName;
28 | private String contactIp;
29 | private boolean LISTEN = true;
30 | private boolean IN_CALL = false;
31 | private AudioCall call;
32 |
33 | @Override
34 | protected void onCreate(Bundle savedInstanceState) {
35 |
36 | super.onCreate(savedInstanceState);
37 | setContentView(R.layout.activity_make_call);
38 |
39 | Log.i(LOG_TAG, "MakeCallActivity started!");
40 |
41 | Intent intent = getIntent();
42 | displayName = intent.getStringExtra(MainActivity.EXTRA_DISPLAYNAME);
43 | contactName = intent.getStringExtra(MainActivity.EXTRA_CONTACT);
44 | contactIp = intent.getStringExtra(MainActivity.EXTRA_IP);
45 |
46 | TextView textView = (TextView) findViewById(R.id.textViewCalling);
47 | textView.setText("Calling: " + contactName);
48 |
49 | startListener();
50 | makeCall();
51 |
52 | Button endButton = (Button) findViewById(R.id.buttonEndCall);
53 | endButton.setOnClickListener(new OnClickListener() {
54 |
55 | @Override
56 | public void onClick(View v) {
57 | // Button to end the call has been pressed
58 | endCall();
59 | }
60 | });
61 | }
62 |
63 | private void makeCall() {
64 | // Send a request to start a call
65 | sendMessage("CAL:"+displayName, 50003);
66 | }
67 |
68 | private void endCall() {
69 | // Ends the chat sessions
70 | stopListener();
71 | if(IN_CALL) {
72 |
73 | call.endCall();
74 | }
75 | sendMessage("END:", BROADCAST_PORT);
76 | finish();
77 | }
78 |
79 | private void startListener() {
80 | // Create listener thread
81 | LISTEN = true;
82 | Thread listenThread = new Thread(new Runnable() {
83 |
84 | @Override
85 | public void run() {
86 |
87 | try {
88 |
89 | Log.i(LOG_TAG, "Listener started!");
90 | DatagramSocket socket = new DatagramSocket(BROADCAST_PORT);
91 | socket.setSoTimeout(15000);
92 | byte[] buffer = new byte[BUF_SIZE];
93 | DatagramPacket packet = new DatagramPacket(buffer, BUF_SIZE);
94 | while(LISTEN) {
95 |
96 | try {
97 |
98 | Log.i(LOG_TAG, "Listening for packets");
99 | socket.receive(packet);
100 | String data = new String(buffer, 0, packet.getLength());
101 | Log.i(LOG_TAG, "Packet received from "+ packet.getAddress() +" with contents: " + data);
102 | String action = data.substring(0, 4);
103 | if(action.equals("ACC:")) {
104 | // Accept notification received. Start call
105 | call = new AudioCall(packet.getAddress());
106 | call.startCall();
107 | IN_CALL = true;
108 | }
109 | else if(action.equals("REJ:")) {
110 | // Reject notification received. End call
111 | endCall();
112 | }
113 | else if(action.equals("END:")) {
114 | // End call notification received. End call
115 | endCall();
116 | }
117 | else {
118 | // Invalid notification received
119 | Log.w(LOG_TAG, packet.getAddress() + " sent invalid message: " + data);
120 | }
121 | }
122 | catch(SocketTimeoutException e) {
123 | if(!IN_CALL) {
124 |
125 | Log.i(LOG_TAG, "No reply from contact. Ending call");
126 | endCall();
127 | return;
128 | }
129 | }
130 | catch(IOException e) {
131 |
132 | }
133 | }
134 | Log.i(LOG_TAG, "Listener ending");
135 | socket.disconnect();
136 | socket.close();
137 | return;
138 | }
139 | catch(SocketException e) {
140 |
141 | Log.e(LOG_TAG, "SocketException in Listener");
142 | endCall();
143 | }
144 | }
145 | });
146 | listenThread.start();
147 | }
148 |
149 | private void stopListener() {
150 | // Ends the listener thread
151 | LISTEN = false;
152 | }
153 |
154 | private void sendMessage(final String message, final int port) {
155 | // Creates a thread used for sending notifications
156 | Thread replyThread = new Thread(new Runnable() {
157 |
158 | @Override
159 | public void run() {
160 |
161 | try {
162 |
163 | InetAddress address = InetAddress.getByName(contactIp);
164 | byte[] data = message.getBytes();
165 | DatagramSocket socket = new DatagramSocket();
166 | DatagramPacket packet = new DatagramPacket(data, data.length, address, port);
167 | socket.send(packet);
168 | Log.i(LOG_TAG, "Sent message( " + message + " ) to " + contactIp);
169 | socket.disconnect();
170 | socket.close();
171 | }
172 | catch(UnknownHostException e) {
173 |
174 | Log.e(LOG_TAG, "Failure. UnknownHostException in sendMessage: " + contactIp);
175 | }
176 | catch(SocketException e) {
177 |
178 | Log.e(LOG_TAG, "Failure. SocketException in sendMessage: " + e);
179 | }
180 | catch(IOException e) {
181 |
182 | Log.e(LOG_TAG, "Failure. IOException in sendMessage: " + e);
183 | }
184 | }
185 | });
186 | replyThread.start();
187 | }
188 |
189 | @Override
190 | public boolean onCreateOptionsMenu(Menu menu) {
191 | // Inflate the menu; this adds items to the action bar if it is present.
192 | getMenuInflater().inflate(R.menu.make_call, menu);
193 | return true;
194 | }
195 |
196 | }
197 |
--------------------------------------------------------------------------------
/src/hw/dt83/udpchat/ReceiveCallActivity.java:
--------------------------------------------------------------------------------
1 | package hw.dt83.udpchat;
2 |
3 | import java.io.IOException;
4 | import java.net.DatagramPacket;
5 | import java.net.DatagramSocket;
6 | import java.net.InetAddress;
7 | import java.net.SocketException;
8 | import java.net.UnknownHostException;
9 |
10 | import android.app.Activity;
11 | import android.content.Intent;
12 | import android.os.Bundle;
13 | import android.util.Log;
14 | import android.view.Menu;
15 | import android.view.View;
16 | import android.view.View.OnClickListener;
17 | import android.widget.Button;
18 | import android.widget.TextView;
19 |
20 | public class ReceiveCallActivity extends Activity {
21 |
22 | private static final String LOG_TAG = "ReceiveCall";
23 | private static final int BROADCAST_PORT = 50002;
24 | private static final int BUF_SIZE = 1024;
25 | private String contactIp;
26 | private String contactName;
27 | private boolean LISTEN = true;
28 | private boolean IN_CALL = false;
29 | private AudioCall call;
30 |
31 | @Override
32 | protected void onCreate(Bundle savedInstanceState) {
33 |
34 | super.onCreate(savedInstanceState);
35 | setContentView(R.layout.activity_receive_call);
36 |
37 | Intent intent = getIntent();
38 | contactName = intent.getStringExtra(MainActivity.EXTRA_CONTACT);
39 | contactIp = intent.getStringExtra(MainActivity.EXTRA_IP);
40 |
41 | TextView textView = (TextView) findViewById(R.id.textViewIncomingCall);
42 | textView.setText("Incoming call: " + contactName);
43 |
44 | final Button endButton = (Button) findViewById(R.id.buttonEndCall1);
45 | endButton.setVisibility(View.INVISIBLE);
46 |
47 | startListener();
48 |
49 | // ACCEPT BUTTON
50 | Button acceptButton = (Button) findViewById(R.id.buttonAccept);
51 | acceptButton.setOnClickListener(new OnClickListener() {
52 |
53 | @Override
54 | public void onClick(View v) {
55 |
56 | try {
57 | // Accepting call. Send a notification and start the call
58 | sendMessage("ACC:");
59 | InetAddress address = InetAddress.getByName(contactIp);
60 | Log.i(LOG_TAG, "Calling " + address.toString());
61 | IN_CALL = true;
62 | call = new AudioCall(address);
63 | call.startCall();
64 | // Hide the buttons as they're not longer required
65 | Button accept = (Button) findViewById(R.id.buttonAccept);
66 | accept.setEnabled(false);
67 |
68 | Button reject = (Button) findViewById(R.id.buttonReject);
69 | reject.setEnabled(false);
70 |
71 | endButton.setVisibility(View.VISIBLE);
72 | }
73 | catch(UnknownHostException e) {
74 |
75 | Log.e(LOG_TAG, "UnknownHostException in acceptButton: " + e);
76 | }
77 | catch(Exception e) {
78 |
79 | Log.e(LOG_TAG, "Exception in acceptButton: " + e);
80 | }
81 | }
82 | });
83 |
84 | // REJECT BUTTON
85 | Button rejectButton = (Button) findViewById(R.id.buttonReject);
86 | rejectButton.setOnClickListener(new OnClickListener() {
87 |
88 | @Override
89 | public void onClick(View v) {
90 | // Send a reject notification and end the call
91 | sendMessage("REJ:");
92 | endCall();
93 | }
94 | });
95 |
96 | // END BUTTON
97 | endButton.setOnClickListener(new OnClickListener() {
98 |
99 | @Override
100 | public void onClick(View v) {
101 |
102 | endCall();
103 | }
104 | });
105 | }
106 |
107 | private void endCall() {
108 | // End the call and send a notification
109 | stopListener();
110 | if(IN_CALL) {
111 |
112 | call.endCall();
113 | }
114 | sendMessage("END:");
115 | finish();
116 | }
117 |
118 | private void startListener() {
119 | // Creates the listener thread
120 | LISTEN = true;
121 | Thread listenThread = new Thread(new Runnable() {
122 |
123 | @Override
124 | public void run() {
125 |
126 | try {
127 |
128 | Log.i(LOG_TAG, "Listener started!");
129 | DatagramSocket socket = new DatagramSocket(BROADCAST_PORT);
130 | socket.setSoTimeout(1500);
131 | byte[] buffer = new byte[BUF_SIZE];
132 | DatagramPacket packet = new DatagramPacket(buffer, BUF_SIZE);
133 | while(LISTEN) {
134 |
135 | try {
136 |
137 | Log.i(LOG_TAG, "Listening for packets");
138 | socket.receive(packet);
139 | String data = new String(buffer, 0, packet.getLength());
140 | Log.i(LOG_TAG, "Packet received from "+ packet.getAddress() +" with contents: " + data);
141 | String action = data.substring(0, 4);
142 | if(action.equals("END:")) {
143 | // End call notification received. End call
144 | endCall();
145 | }
146 | else {
147 | // Invalid notification received.
148 | Log.w(LOG_TAG, packet.getAddress() + " sent invalid message: " + data);
149 | }
150 | }
151 | catch(IOException e) {
152 |
153 | Log.e(LOG_TAG, "IOException in Listener " + e);
154 | }
155 | }
156 | Log.i(LOG_TAG, "Listener ending");
157 | socket.disconnect();
158 | socket.close();
159 | return;
160 | }
161 | catch(SocketException e) {
162 |
163 | Log.e(LOG_TAG, "SocketException in Listener " + e);
164 | endCall();
165 | }
166 | }
167 | });
168 | listenThread.start();
169 | }
170 |
171 | private void stopListener() {
172 | // Ends the listener thread
173 | LISTEN = false;
174 | }
175 |
176 | private void sendMessage(final String message) {
177 | // Creates a thread for sending notifications
178 | Thread replyThread = new Thread(new Runnable() {
179 |
180 | @Override
181 | public void run() {
182 |
183 | try {
184 |
185 | InetAddress address = InetAddress.getByName(contactIp);
186 | byte[] data = message.getBytes();
187 | DatagramSocket socket = new DatagramSocket();
188 | DatagramPacket packet = new DatagramPacket(data, data.length, address, BROADCAST_PORT);
189 | socket.send(packet);
190 | Log.i(LOG_TAG, "Sent message( " + message + " ) to " + contactIp);
191 | socket.disconnect();
192 | socket.close();
193 | }
194 | catch(UnknownHostException e) {
195 |
196 | Log.e(LOG_TAG, "Failure. UnknownHostException in sendMessage: " + contactIp);
197 | }
198 | catch(SocketException e) {
199 |
200 | Log.e(LOG_TAG, "Failure. SocketException in sendMessage: " + e);
201 | }
202 | catch(IOException e) {
203 |
204 | Log.e(LOG_TAG, "Failure. IOException in sendMessage: " + e);
205 | }
206 | }
207 | });
208 | replyThread.start();
209 | }
210 |
211 | @Override
212 | public boolean onCreateOptionsMenu(Menu menu) {
213 | // Inflate the menu; this adds items to the action bar if it is present.
214 | getMenuInflater().inflate(R.menu.receive_call, menu);
215 | return true;
216 | }
217 |
218 | }
219 |
--------------------------------------------------------------------------------
/src/hw/dt83/udpchat/ContactManager.java:
--------------------------------------------------------------------------------
1 | package hw.dt83.udpchat;
2 |
3 | import java.io.IOException;
4 | import java.net.DatagramPacket;
5 | import java.net.DatagramSocket;
6 | import java.net.InetAddress;
7 | import java.net.SocketException;
8 | import java.net.SocketTimeoutException;
9 | import java.util.HashMap;
10 |
11 | import android.util.Log;
12 |
13 | public class ContactManager {
14 |
15 | private static final String LOG_TAG = "ContactManager";
16 | public static final int BROADCAST_PORT = 50001; // Socket on which packets are sent/received
17 | private static final int BROADCAST_INTERVAL = 10000; // Milliseconds
18 | private static final int BROADCAST_BUF_SIZE = 1024;
19 | private boolean BROADCAST = true;
20 | private boolean LISTEN = true;
21 | private HashMap contacts;
22 | private InetAddress broadcastIP;
23 |
24 | public ContactManager(String name, InetAddress broadcastIP) {
25 |
26 | contacts = new HashMap();
27 | this.broadcastIP = broadcastIP;
28 | listen();
29 | broadcastName(name, broadcastIP);
30 | }
31 |
32 | public HashMap getContacts() {
33 |
34 | return contacts;
35 | }
36 |
37 | public void addContact(String name, InetAddress address) {
38 | // If the contact is not already known to us, add it
39 | if(!contacts.containsKey(name)) {
40 |
41 | Log.i(LOG_TAG, "Adding contact: " + name);
42 | contacts.put(name, address);
43 | Log.i(LOG_TAG, "#Contacts: " + contacts.size());
44 | return;
45 | }
46 | Log.i(LOG_TAG, "Contact already exists: " + name);
47 | return;
48 | }
49 |
50 | public void removeContact(String name) {
51 | // If the contact is known to us, remove it
52 | if(contacts.containsKey(name)) {
53 |
54 | Log.i(LOG_TAG, "Removing contact: " + name);
55 | contacts.remove(name);
56 | Log.i(LOG_TAG, "#Contacts: " + contacts.size());
57 | return;
58 | }
59 | Log.i(LOG_TAG, "Cannot remove contact. " + name + " does not exist.");
60 | return;
61 | }
62 |
63 | public void bye(final String name) {
64 | // Sends a Bye notification to other devices
65 | Thread byeThread = new Thread(new Runnable() {
66 |
67 | @Override
68 | public void run() {
69 |
70 | try {
71 | Log.i(LOG_TAG, "Attempting to broadcast BYE notification!");
72 | String notification = "BYE:"+name;
73 | byte[] message = notification.getBytes();
74 | DatagramSocket socket = new DatagramSocket();
75 | socket.setBroadcast(true);
76 | DatagramPacket packet = new DatagramPacket(message, message.length, broadcastIP, BROADCAST_PORT);
77 | socket.send(packet);
78 | Log.i(LOG_TAG, "Broadcast BYE notification!");
79 | socket.disconnect();
80 | socket.close();
81 | return;
82 | }
83 | catch(SocketException e) {
84 |
85 | Log.e(LOG_TAG, "SocketException during BYE notification: " + e);
86 | }
87 | catch(IOException e) {
88 |
89 | Log.e(LOG_TAG, "IOException during BYE notification: " + e);
90 | }
91 | }
92 | });
93 | byeThread.start();
94 | }
95 |
96 | public void broadcastName(final String name, final InetAddress broadcastIP) {
97 | // Broadcasts the name of the device at a regular interval
98 | Log.i(LOG_TAG, "Broadcasting started!");
99 | Thread broadcastThread = new Thread(new Runnable() {
100 |
101 | @Override
102 | public void run() {
103 |
104 | try {
105 |
106 | String request = "ADD:"+name;
107 | byte[] message = request.getBytes();
108 | DatagramSocket socket = new DatagramSocket();
109 | socket.setBroadcast(true);
110 | DatagramPacket packet = new DatagramPacket(message, message.length, broadcastIP, BROADCAST_PORT);
111 | while(BROADCAST) {
112 |
113 | socket.send(packet);
114 | Log.i(LOG_TAG, "Broadcast packet sent: " + packet.getAddress().toString());
115 | Thread.sleep(BROADCAST_INTERVAL);
116 | }
117 | Log.i(LOG_TAG, "Broadcaster ending!");
118 | socket.disconnect();
119 | socket.close();
120 | return;
121 | }
122 | catch(SocketException e) {
123 |
124 | Log.e(LOG_TAG, "SocketExceltion in broadcast: " + e);
125 | Log.i(LOG_TAG, "Broadcaster ending!");
126 | return;
127 | }
128 | catch(IOException e) {
129 |
130 | Log.e(LOG_TAG, "IOException in broadcast: " + e);
131 | Log.i(LOG_TAG, "Broadcaster ending!");
132 | return;
133 | }
134 | catch(InterruptedException e) {
135 |
136 | Log.e(LOG_TAG, "InterruptedException in broadcast: " + e);
137 | Log.i(LOG_TAG, "Broadcaster ending!");
138 | return;
139 | }
140 | }
141 | });
142 | broadcastThread.start();
143 | }
144 |
145 | public void stopBroadcasting() {
146 | // Ends the broadcasting thread
147 | BROADCAST = false;
148 | }
149 |
150 | public void listen() {
151 | // Create the listener thread
152 | Log.i(LOG_TAG, "Listening started!");
153 | Thread listenThread = new Thread(new Runnable() {
154 |
155 | @Override
156 | public void run() {
157 |
158 | DatagramSocket socket;
159 | try {
160 |
161 | socket = new DatagramSocket(BROADCAST_PORT);
162 | }
163 | catch (SocketException e) {
164 |
165 | Log.e(LOG_TAG, "SocketExcepion in listener: " + e);
166 | return;
167 | }
168 | byte[] buffer = new byte[BROADCAST_BUF_SIZE];
169 |
170 | while(LISTEN) {
171 |
172 | listen(socket, buffer);
173 | }
174 | Log.i(LOG_TAG, "Listener ending!");
175 | socket.disconnect();
176 | socket.close();
177 | return;
178 | }
179 |
180 | public void listen(DatagramSocket socket, byte[] buffer) {
181 |
182 | try {
183 | //Listen in for new notifications
184 | Log.i(LOG_TAG, "Listening for a packet!");
185 | DatagramPacket packet = new DatagramPacket(buffer, BROADCAST_BUF_SIZE);
186 | socket.setSoTimeout(15000);
187 | socket.receive(packet);
188 | String data = new String(buffer, 0, packet.getLength());
189 | Log.i(LOG_TAG, "Packet received: " + data);
190 | String action = data.substring(0, 4);
191 | if(action.equals("ADD:")) {
192 | // Add notification received. Attempt to add contact
193 | Log.i(LOG_TAG, "Listener received ADD request");
194 | addContact(data.substring(4, data.length()), packet.getAddress());
195 | }
196 | else if(action.equals("BYE:")) {
197 | // Bye notification received. Attempt to remove contact
198 | Log.i(LOG_TAG, "Listener received BYE request");
199 | removeContact(data.substring(4, data.length()));
200 | }
201 | else {
202 | // Invalid notification received
203 | Log.w(LOG_TAG, "Listener received invalid request: " + action);
204 | }
205 |
206 | }
207 | catch(SocketTimeoutException e) {
208 |
209 | Log.i(LOG_TAG, "No packet received!");
210 | if(LISTEN) {
211 |
212 | listen(socket, buffer);
213 | }
214 | return;
215 | }
216 | catch(SocketException e) {
217 |
218 | Log.e(LOG_TAG, "SocketException in listen: " + e);
219 | Log.i(LOG_TAG, "Listener ending!");
220 | return;
221 | }
222 | catch(IOException e) {
223 |
224 | Log.e(LOG_TAG, "IOException in listen: " + e);
225 | Log.i(LOG_TAG, "Listener ending!");
226 | return;
227 | }
228 | }
229 | });
230 | listenThread.start();
231 | }
232 |
233 | public void stopListening() {
234 | // Stops the listener thread
235 | LISTEN = false;
236 | }
237 | }
238 |
--------------------------------------------------------------------------------
/src/hw/dt83/udpchat/MainActivity.java:
--------------------------------------------------------------------------------
1 | package hw.dt83.udpchat;
2 |
3 | import java.net.DatagramPacket;
4 | import java.net.DatagramSocket;
5 | import java.net.InetAddress;
6 | import java.net.SocketException;
7 | import java.net.UnknownHostException;
8 | import java.util.HashMap;
9 |
10 | import android.app.Activity;
11 | import android.app.AlertDialog;
12 | import android.content.DialogInterface;
13 | import android.content.Intent;
14 | import android.graphics.Color;
15 | import android.net.wifi.WifiInfo;
16 | import android.net.wifi.WifiManager;
17 | import android.os.Bundle;
18 | import android.util.Log;
19 | import android.view.View;
20 | import android.view.View.OnClickListener;
21 | import android.widget.Button;
22 | import android.widget.EditText;
23 | import android.widget.RadioButton;
24 | import android.widget.RadioGroup;
25 | import android.widget.ScrollView;
26 | import android.widget.TextView;
27 |
28 | public class MainActivity extends Activity {
29 |
30 | static final String LOG_TAG = "UDPchat";
31 | private static final int LISTENER_PORT = 50003;
32 | private static final int BUF_SIZE = 1024;
33 | private ContactManager contactManager;
34 | private String displayName;
35 | private boolean STARTED = false;
36 | private boolean IN_CALL = false;
37 | private boolean LISTEN = false;
38 |
39 | public final static String EXTRA_CONTACT = "hw.dt83.udpchat.CONTACT";
40 | public final static String EXTRA_IP = "hw.dt83.udpchat.IP";
41 | public final static String EXTRA_DISPLAYNAME = "hw.dt83.udpchat.DISPLAYNAME";
42 |
43 | @Override
44 | protected void onCreate(Bundle savedInstanceState) {
45 |
46 | super.onCreate(savedInstanceState);
47 | setContentView(R.layout.activity_main);
48 |
49 | Log.i(LOG_TAG, "UDPChat started");
50 |
51 | // START BUTTON
52 | // Pressing this buttons initiates the main functionality
53 | final Button btnStart = (Button) findViewById(R.id.buttonStart);
54 | btnStart.setOnClickListener(new OnClickListener() {
55 |
56 | @Override
57 | public void onClick(View v) {
58 |
59 | Log.i(LOG_TAG, "Start button pressed");
60 | STARTED = true;
61 |
62 | EditText displayNameText = (EditText) findViewById(R.id.editTextDisplayName);
63 | displayName = displayNameText.getText().toString();
64 |
65 | displayNameText.setEnabled(false);
66 | btnStart.setEnabled(false);
67 |
68 | TextView text = (TextView) findViewById(R.id.textViewSelectContact);
69 | text.setVisibility(View.VISIBLE);
70 |
71 | Button updateButton = (Button) findViewById(R.id.buttonUpdate);
72 | updateButton.setVisibility(View.VISIBLE);
73 |
74 | Button callButton = (Button) findViewById(R.id.buttonCall);
75 | callButton.setVisibility(View.VISIBLE);
76 |
77 | ScrollView scrollView = (ScrollView) findViewById(R.id.scrollView);
78 | scrollView.setVisibility(View.VISIBLE);
79 |
80 | contactManager = new ContactManager(displayName, getBroadcastIp());
81 | startCallListener();
82 | }
83 | });
84 |
85 | // UPDATE BUTTON
86 | // Updates the list of reachable devices
87 | final Button btnUpdate = (Button) findViewById(R.id.buttonUpdate);
88 | btnUpdate.setOnClickListener(new OnClickListener() {
89 |
90 | @Override
91 | public void onClick(View v) {
92 |
93 | updateContactList();
94 | }
95 | });
96 |
97 | // CALL BUTTON
98 | // Attempts to initiate an audio chat session with the selected device
99 | final Button btnCall = (Button) findViewById(R.id.buttonCall);
100 | btnCall.setOnClickListener(new OnClickListener() {
101 |
102 | @Override
103 | public void onClick(View v) {
104 |
105 | RadioGroup radioGroup = (RadioGroup) findViewById(R.id.contactList);
106 | int selectedButton = radioGroup.getCheckedRadioButtonId();
107 | if(selectedButton == -1) {
108 | // If no device was selected, present an error message to the user
109 | Log.w(LOG_TAG, "Warning: no contact selected");
110 | final AlertDialog alert = new AlertDialog.Builder(MainActivity.this).create();
111 | alert.setTitle("Oops");
112 | alert.setMessage("You must select a contact first");
113 | alert.setButton(-1, "OK", new DialogInterface.OnClickListener() {
114 | public void onClick(DialogInterface dialog, int which) {
115 |
116 | alert.dismiss();
117 | }
118 | });
119 | alert.show();
120 | return;
121 | }
122 | // Collect details about the selected contact
123 | RadioButton radioButton = (RadioButton) findViewById(selectedButton);
124 | String contact = radioButton.getText().toString();
125 | InetAddress ip = contactManager.getContacts().get(contact);
126 | IN_CALL = true;
127 |
128 | // Send this information to the MakeCallActivity and start that activity
129 | Intent intent = new Intent(MainActivity.this, MakeCallActivity.class);
130 | intent.putExtra(EXTRA_CONTACT, contact);
131 | String address = ip.toString();
132 | address = address.substring(1, address.length());
133 | intent.putExtra(EXTRA_IP, address);
134 | intent.putExtra(EXTRA_DISPLAYNAME, displayName);
135 | startActivity(intent);
136 | }
137 | });
138 | }
139 |
140 | private void updateContactList() {
141 | // Create a copy of the HashMap used by the ContactManager
142 | HashMap contacts = contactManager.getContacts();
143 | // Create a radio button for each contact in the HashMap
144 | RadioGroup radioGroup = (RadioGroup) findViewById(R.id.contactList);
145 | radioGroup.removeAllViews();
146 |
147 | for(String name : contacts.keySet()) {
148 |
149 | RadioButton radioButton = new RadioButton(getBaseContext());
150 | radioButton.setText(name);
151 | radioButton.setTextColor(Color.BLACK);
152 | radioGroup.addView(radioButton);
153 | }
154 |
155 | radioGroup.clearCheck();
156 | }
157 |
158 | private InetAddress getBroadcastIp() {
159 | // Function to return the broadcast address, based on the IP address of the device
160 | try {
161 |
162 | WifiManager wifiManager = (WifiManager) getSystemService(WIFI_SERVICE);
163 | WifiInfo wifiInfo = wifiManager.getConnectionInfo();
164 | int ipAddress = wifiInfo.getIpAddress();
165 | String addressString = toBroadcastIp(ipAddress);
166 | InetAddress broadcastAddress = InetAddress.getByName(addressString);
167 | return broadcastAddress;
168 | }
169 | catch(UnknownHostException e) {
170 |
171 | Log.e(LOG_TAG, "UnknownHostException in getBroadcastIP: " + e);
172 | return null;
173 | }
174 |
175 | }
176 |
177 | private String toBroadcastIp(int ip) {
178 | // Returns converts an IP address in int format to a formatted string
179 | return (ip & 0xFF) + "." +
180 | ((ip >> 8) & 0xFF) + "." +
181 | ((ip >> 16) & 0xFF) + "." +
182 | "255";
183 | }
184 |
185 | private void startCallListener() {
186 | // Creates the listener thread
187 | LISTEN = true;
188 | Thread listener = new Thread(new Runnable() {
189 |
190 | @Override
191 | public void run() {
192 |
193 | try {
194 | // Set up the socket and packet to receive
195 | Log.i(LOG_TAG, "Incoming call listener started");
196 | DatagramSocket socket = new DatagramSocket(LISTENER_PORT);
197 | socket.setSoTimeout(1000);
198 | byte[] buffer = new byte[BUF_SIZE];
199 | DatagramPacket packet = new DatagramPacket(buffer, BUF_SIZE);
200 | while(LISTEN) {
201 | // Listen for incoming call requests
202 | try {
203 | Log.i(LOG_TAG, "Listening for incoming calls");
204 | socket.receive(packet);
205 | String data = new String(buffer, 0, packet.getLength());
206 | Log.i(LOG_TAG, "Packet received from "+ packet.getAddress() +" with contents: " + data);
207 | String action = data.substring(0, 4);
208 | if(action.equals("CAL:")) {
209 | // Received a call request. Start the ReceiveCallActivity
210 | String address = packet.getAddress().toString();
211 | String name = data.substring(4, packet.getLength());
212 |
213 | Intent intent = new Intent(MainActivity.this, ReceiveCallActivity.class);
214 | intent.putExtra(EXTRA_CONTACT, name);
215 | intent.putExtra(EXTRA_IP, address.substring(1, address.length()));
216 | IN_CALL = true;
217 | //LISTEN = false;
218 | //stopCallListener();
219 | startActivity(intent);
220 | }
221 | else {
222 | // Received an invalid request
223 | Log.w(LOG_TAG, packet.getAddress() + " sent invalid message: " + data);
224 | }
225 | }
226 | catch(Exception e) {}
227 | }
228 | Log.i(LOG_TAG, "Call Listener ending");
229 | socket.disconnect();
230 | socket.close();
231 | }
232 | catch(SocketException e) {
233 |
234 | Log.e(LOG_TAG, "SocketException in listener " + e);
235 | }
236 | }
237 | });
238 | listener.start();
239 | }
240 |
241 | private void stopCallListener() {
242 | // Ends the listener thread
243 | LISTEN = false;
244 | }
245 |
246 | @Override
247 | public void onPause() {
248 |
249 | super.onPause();
250 | if(STARTED) {
251 |
252 | contactManager.bye(displayName);
253 | contactManager.stopBroadcasting();
254 | contactManager.stopListening();
255 | //STARTED = false;
256 | }
257 | stopCallListener();
258 | Log.i(LOG_TAG, "App paused!");
259 | }
260 |
261 | @Override
262 | public void onStop() {
263 |
264 | super.onStop();
265 | Log.i(LOG_TAG, "App stopped!");
266 | stopCallListener();
267 | if(!IN_CALL) {
268 |
269 | finish();
270 | }
271 | }
272 |
273 | @Override
274 | public void onRestart() {
275 |
276 | super.onRestart();
277 | Log.i(LOG_TAG, "App restarted!");
278 | IN_CALL = false;
279 | STARTED = true;
280 | contactManager = new ContactManager(displayName, getBroadcastIp());
281 | startCallListener();
282 | }
283 | }
284 |
--------------------------------------------------------------------------------