diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index 1d1c324..6c0b841 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -26,10 +26,15 @@
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:theme="@android:style/Theme.Holo">
+
+
-
\ No newline at end of file
diff --git a/app/src/main/java/de/rochefort/childmonitor/ListenActivity.java b/app/src/main/java/de/rochefort/childmonitor/ListenActivity.java
index d9edd31..557e823 100644
--- a/app/src/main/java/de/rochefort/childmonitor/ListenActivity.java
+++ b/app/src/main/java/de/rochefort/childmonitor/ListenActivity.java
@@ -82,7 +82,7 @@ public class ListenActivity extends Activity {
// applications).
if (bindService(intent, connection, Context.BIND_AUTO_CREATE)) {
shouldUnbind = true;
- Log.i(TAG, "Bound service");
+ Log.i(TAG, "Bound listen service");
} else {
Log.e(TAG, "Error: The requested service doesn't " +
"exist, or this client isn't allowed access to it.");
diff --git a/app/src/main/java/de/rochefort/childmonitor/MonitorActivity.java b/app/src/main/java/de/rochefort/childmonitor/MonitorActivity.java
index df92769..625697a 100644
--- a/app/src/main/java/de/rochefort/childmonitor/MonitorActivity.java
+++ b/app/src/main/java/de/rochefort/childmonitor/MonitorActivity.java
@@ -16,144 +16,64 @@
*/
package de.rochefort.childmonitor;
-import static de.rochefort.childmonitor.AudioCodecDefines.CODEC;
-
import android.app.Activity;
+import android.content.ComponentName;
import android.content.Context;
-import android.media.AudioRecord;
-import android.media.MediaRecorder;
+import android.content.Intent;
+import android.content.ServiceConnection;
import android.net.ConnectivityManager;
import android.net.LinkAddress;
import android.net.Network;
import android.net.NetworkInfo;
-import android.net.nsd.NsdManager;
-import android.net.nsd.NsdServiceInfo;
-import android.os.Build;
import android.os.Bundle;
+import android.os.IBinder;
+import android.support.v4.content.ContextCompat;
import android.util.Log;
import android.widget.TextView;
+import android.widget.Toast;
-import java.io.IOException;
-import java.io.OutputStream;
import java.net.InetAddress;
-import java.net.ServerSocket;
-import java.net.Socket;
import java.util.ArrayList;
import java.util.List;
-import java.util.Objects;
public class MonitorActivity extends Activity {
final static String TAG = "ChildMonitor";
-
- private NsdManager nsdManager;
-
- private NsdManager.RegistrationListener registrationListener;
-
- private ServerSocket currentSocket;
-
- private Object connectionToken;
-
- private int currentPort;
-
- private void serviceConnection(Socket socket) {
- runOnUiThread(() -> {
- final TextView statusText = findViewById(R.id.textStatus);
- statusText.setText(R.string.streaming);
- });
-
- final int frequency = AudioCodecDefines.FREQUENCY;
- final int channelConfiguration = AudioCodecDefines.CHANNEL_CONFIGURATION_IN;
- final int audioEncoding = AudioCodecDefines.ENCODING;
-
- final int bufferSize = AudioRecord.getMinBufferSize(frequency, channelConfiguration, audioEncoding);
- final AudioRecord audioRecord;
- try {
- audioRecord = new AudioRecord(
- MediaRecorder.AudioSource.MIC,
- frequency,
- channelConfiguration,
- audioEncoding,
- bufferSize
- );
- } catch (SecurityException e) {
- // This should never happen, we asked for permission before
- throw new RuntimeException(e);
+ private final ServiceConnection connection = new ServiceConnection() {
+ public void onServiceConnected(ComponentName className, IBinder service) {
+ // This is called when the connection with the service has been
+ // established, giving us the service object we can use to
+ // interact with the service. Because we have bound to an explicit
+ // service that we know is running in our own process, we can
+ // cast its IBinder to a concrete class and directly access it.
+ MonitorService bs = ((MonitorService.MonitorBinder) service).getService();
+ bs.setMonitorActivity(MonitorActivity.this);
}
- final int pcmBufferSize = bufferSize*2;
- final short[] pcmBuffer = new short[pcmBufferSize];
- final byte[] ulawBuffer = new byte[pcmBufferSize];
-
- try {
- audioRecord.startRecording();
- final OutputStream out = socket.getOutputStream();
-
- socket.setSendBufferSize(pcmBufferSize);
- Log.d(TAG, "Socket send buffer size: " + socket.getSendBufferSize());
-
- while (socket.isConnected() && currentSocket != null && !Thread.currentThread().isInterrupted()) {
- final int read = audioRecord.read(pcmBuffer, 0, bufferSize);
- int encoded = CODEC.encode(pcmBuffer, read, ulawBuffer, 0);
- out.write(ulawBuffer, 0, encoded);
- }
- } catch (Exception e) {
- Log.e(TAG, "Connection failed", e);
- } finally {
- audioRecord.stop();
- }
- }
-
+ public void onServiceDisconnected(ComponentName className) {
+ // This is called when the connection with the service has been
+ // unexpectedly disconnected -- that is, its process crashed.
+ // Because it is running in our same process, we should never
+ // see this happen.
+ Toast.makeText(MonitorActivity.this, R.string.disconnected,
+ Toast.LENGTH_SHORT).show();
+ }
+ };
+ private boolean shouldUnbind;
@Override
protected void onCreate(Bundle savedInstanceState) {
Log.i(TAG, "ChildMonitor start");
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_monitor);
-
- nsdManager = (NsdManager)this.getSystemService(Context.NSD_SERVICE);
- currentPort = 10000;
- currentSocket = null;
- final Object currentToken = new Object();
- connectionToken = currentToken;
-
- new Thread(() -> {
- while(Objects.equals(connectionToken, currentToken)) {
- try (ServerSocket serverSocket = new ServerSocket(currentPort)) {
- currentSocket = serverSocket;
- // Store the chosen port.
- final int localPort = serverSocket.getLocalPort();
-
- // Register the service so that parent devices can
- // locate the child device
- registerService(localPort);
-
- // Wait for a parent to find us and connect
- try (Socket socket = serverSocket.accept()) {
- Log.i(TAG, "Connection from parent device received");
-
- // We now have a client connection.
- // Unregister so no other clients will
- // attempt to connect
- unregisterService();
- serviceConnection(socket);
- }
- } catch(Exception e) {
- // Just in case
- currentPort++;
- Log.e(TAG, "Failed to open server socket. Port increased to " + currentPort, e);
- }
- }
- }).start();
-
final TextView addressText = findViewById(R.id.address);
List listenAddresses = getListenAddresses();
- if(!listenAddresses.isEmpty()) {
+ if (!listenAddresses.isEmpty()) {
StringBuilder sb = new StringBuilder();
for (int i = 0; i < listenAddresses.size(); i++) {
String listenAddress = listenAddresses.get(i);
sb.append(listenAddress);
- if (i != listenAddresses.size() -1) {
+ if (i != listenAddresses.size() - 1) {
sb.append("\n\n");
}
}
@@ -162,10 +82,11 @@ public class MonitorActivity extends Activity {
addressText.setText(R.string.notConnected);
}
+ ensureServiceRunningAndBind();
}
private List getListenAddresses() {
- ConnectivityManager cm = (ConnectivityManager)getSystemService(Context.CONNECTIVITY_SERVICE);
+ ConnectivityManager cm = (ConnectivityManager) getSystemService(Context.CONNECTIVITY_SERVICE);
List listenAddresses = new ArrayList<>();
if (cm != null) {
for (Network network : cm.getAllNetworks()) {
@@ -186,87 +107,37 @@ public class MonitorActivity extends Activity {
}
@Override
- protected void onStop() {
- Log.i(TAG, "ChildMonitor stop");
-
- unregisterService();
-
- connectionToken = null;
- if(currentSocket != null) {
- try {
- currentSocket.close();
- currentSocket = null;
- } catch (IOException e) {
- Log.e(TAG, "Failed to close active socket on port "+currentPort);
- }
- }
- super.onStop();
+ public void onDestroy() {
+ doUnbindAndStopService();
+ super.onDestroy();
}
- private void registerService(final int port) {
- final NsdServiceInfo serviceInfo = new NsdServiceInfo();
- serviceInfo.setServiceName("ChildMonitor on " + Build.MODEL);
- serviceInfo.setServiceType("_childmonitor._tcp.");
- serviceInfo.setPort(port);
-
- registrationListener = new NsdManager.RegistrationListener() {
- @Override
- public void onServiceRegistered(NsdServiceInfo nsdServiceInfo) {
- // Save the service name. Android may have changed it in order to
- // resolve a conflict, so update the name you initially requested
- // with the name Android actually used.
- final String serviceName = nsdServiceInfo.getServiceName();
-
- Log.i(TAG, "Service name: " + serviceName);
-
- MonitorActivity.this.runOnUiThread(() -> {
- final TextView statusText = findViewById(R.id.textStatus);
- statusText.setText(R.string.waitingForParent);
-
- final TextView serviceText = findViewById(R.id.textService);
- serviceText.setText(serviceName);
-
- final TextView portText = findViewById(R.id.port);
- portText.setText(Integer.toString(port));
- });
- }
-
- @Override
- public void onRegistrationFailed(NsdServiceInfo serviceInfo, int errorCode) {
- // Registration failed! Put debugging code here to determine why.
- Log.e(TAG, "Registration failed: " + errorCode);
- }
-
- @Override
- public void onServiceUnregistered(NsdServiceInfo arg0) {
- // Service has been unregistered. This only happens when you call
- // NsdManager.unregisterService() and pass in this listener.
-
- Log.i(TAG, "Unregistering service");
- }
-
- @Override
- public void onUnregistrationFailed(NsdServiceInfo serviceInfo, int errorCode) {
- // Unregistration failed. Put debugging code here to determine why.
-
- Log.e(TAG, "Unregistration failed: " + errorCode);
- }
- };
-
- nsdManager.registerService(
- serviceInfo, NsdManager.PROTOCOL_DNS_SD, registrationListener);
+ void ensureServiceRunningAndBind() {
+ final Context context = this;
+ final Intent intent = new Intent(context, MonitorService.class);
+ ContextCompat.startForegroundService(context, intent);
+ // Attempts to establish a connection with the service. We use an
+ // explicit class name because we want a specific service
+ // implementation that we know will be running in our own process
+ // (and thus won't be supporting component replacement by other
+ // applications).
+ if (bindService(intent, connection, Context.BIND_AUTO_CREATE)) {
+ shouldUnbind = true;
+ Log.i(TAG, "Bound monitor service");
+ } else {
+ Log.e(TAG, "Error: The requested service doesn't " +
+ "exist, or this client isn't allowed access to it.");
+ }
}
- /**
- * Uhregistered the service and assigns the listener
- * to null.
- */
- private void unregisterService() {
- if(registrationListener != null) {
- Log.i(TAG, "Unregistering monitoring service");
-
- nsdManager.unregisterService(registrationListener);
- registrationListener = null;
+ void doUnbindAndStopService() {
+ if (shouldUnbind) {
+ // Release information about the service's state.
+ unbindService(connection);
+ shouldUnbind = false;
}
+ final Context context = this;
+ final Intent intent = new Intent(context, MonitorService.class);
+ context.stopService(intent);
}
}
diff --git a/app/src/main/java/de/rochefort/childmonitor/MonitorService.java b/app/src/main/java/de/rochefort/childmonitor/MonitorService.java
new file mode 100644
index 0000000..9ac1642
--- /dev/null
+++ b/app/src/main/java/de/rochefort/childmonitor/MonitorService.java
@@ -0,0 +1,313 @@
+/*
+ * This file is part of Child Monitor.
+ *
+ * Child Monitor is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * Child Monitor is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with Child Monitor. If not, see .
+ */
+package de.rochefort.childmonitor;
+
+import static de.rochefort.childmonitor.AudioCodecDefines.CODEC;
+
+import android.app.Notification;
+import android.app.NotificationChannel;
+import android.app.NotificationManager;
+import android.app.Service;
+import android.content.Context;
+import android.content.Intent;
+import android.media.AudioRecord;
+import android.media.MediaRecorder;
+import android.net.nsd.NsdManager;
+import android.net.nsd.NsdServiceInfo;
+import android.os.Binder;
+import android.os.Build;
+import android.os.IBinder;
+import android.support.v4.app.NotificationCompat;
+import android.util.Log;
+import android.widget.TextView;
+import android.widget.Toast;
+
+import java.io.IOException;
+import java.io.OutputStream;
+import java.net.ServerSocket;
+import java.net.Socket;
+import java.util.Objects;
+
+public class MonitorService extends Service {
+ final static String TAG = "MonitorService";
+ final static String CHANNEL_ID = TAG;
+ public static final int ID = 1338;
+ private final IBinder binder = new MonitorBinder();
+ private NsdManager nsdManager;
+ private NsdManager.RegistrationListener registrationListener;
+ private ServerSocket currentSocket;
+ private Object connectionToken;
+ private int currentPort;
+ private NotificationManager notificationManager;
+ private Thread monitorThread;
+ private MonitorActivity monitorActivity;
+
+ public void setMonitorActivity(MonitorActivity monitorActivity) {
+ this.monitorActivity = monitorActivity;
+ }
+
+ private void serviceConnection(Socket socket) {
+ final MonitorActivity ma = monitorActivity;
+ if (ma != null) {
+ ma.runOnUiThread(() -> {
+ final TextView statusText = monitorActivity.findViewById(R.id.textStatus);
+ statusText.setText(R.string.streaming);
+ });
+ }
+
+ final int frequency = AudioCodecDefines.FREQUENCY;
+ final int channelConfiguration = AudioCodecDefines.CHANNEL_CONFIGURATION_IN;
+ final int audioEncoding = AudioCodecDefines.ENCODING;
+
+ final int bufferSize = AudioRecord.getMinBufferSize(frequency, channelConfiguration, audioEncoding);
+ final AudioRecord audioRecord;
+ try {
+ audioRecord = new AudioRecord(
+ MediaRecorder.AudioSource.MIC,
+ frequency,
+ channelConfiguration,
+ audioEncoding,
+ bufferSize
+ );
+ } catch (SecurityException e) {
+ // This should never happen, we asked for permission before
+ throw new RuntimeException(e);
+ }
+
+ final int pcmBufferSize = bufferSize * 2;
+ final short[] pcmBuffer = new short[pcmBufferSize];
+ final byte[] ulawBuffer = new byte[pcmBufferSize];
+
+ try {
+ audioRecord.startRecording();
+ final OutputStream out = socket.getOutputStream();
+
+ socket.setSendBufferSize(pcmBufferSize);
+ Log.d(TAG, "Socket send buffer size: " + socket.getSendBufferSize());
+
+ while (socket.isConnected() && currentSocket != null && !Thread.currentThread().isInterrupted()) {
+ final int read = audioRecord.read(pcmBuffer, 0, bufferSize);
+ int encoded = CODEC.encode(pcmBuffer, read, ulawBuffer, 0);
+ out.write(ulawBuffer, 0, encoded);
+ }
+ } catch (Exception e) {
+ Log.e(TAG, "Connection failed", e);
+ } finally {
+ audioRecord.stop();
+ }
+ }
+
+
+ @Override
+ public void onCreate() {
+ Log.i(TAG, "ChildMonitor start");
+ super.onCreate();
+ notificationManager = (NotificationManager) getSystemService(NOTIFICATION_SERVICE);
+
+ nsdManager = (NsdManager) this.getSystemService(Context.NSD_SERVICE);
+ currentPort = 10000;
+ currentSocket = null;
+ }
+
+ @Override
+ public int onStartCommand(Intent intent, int flags, int startId) {
+ Log.i(TAG, "Received start id " + startId + ": " + intent);
+ // Display a notification about us starting. We put an icon in the status bar.
+ createNotificationChannel();
+ Notification n = buildNotification();
+ startForeground(ID, n);
+ ensureMonitorThread();
+
+ return START_REDELIVER_INTENT;
+ }
+
+ private void ensureMonitorThread() {
+ Thread mt = monitorThread;
+ if (mt != null && mt.isAlive()) {
+ return;
+ }
+
+ final Object currentToken = new Object();
+ connectionToken = currentToken;
+
+ mt = new Thread(() -> {
+ while (Objects.equals(connectionToken, currentToken)) {
+ try (ServerSocket serverSocket = new ServerSocket(currentPort)) {
+ currentSocket = serverSocket;
+ // Store the chosen port.
+ final int localPort = serverSocket.getLocalPort();
+
+ // Register the service so that parent devices can
+ // locate the child device
+ registerService(localPort);
+
+ // Wait for a parent to find us and connect
+ try (Socket socket = serverSocket.accept()) {
+ Log.i(TAG, "Connection from parent device received");
+
+ // We now have a client connection.
+ // Unregister so no other clients will
+ // attempt to connect
+ unregisterService();
+ serviceConnection(socket);
+ }
+ } catch (Exception e) {
+ if (Objects.equals(connectionToken, currentToken)) {
+ // Just in case
+ currentPort++;
+ Log.e(TAG, "Failed to open server socket. Port increased to " + currentPort, e);
+ }
+ }
+ }
+ });
+ monitorThread = mt;
+ mt.start();
+ }
+
+ private void registerService(final int port) {
+ final NsdServiceInfo serviceInfo = new NsdServiceInfo();
+ serviceInfo.setServiceName("ChildMonitor on " + Build.MODEL);
+ serviceInfo.setServiceType("_childmonitor._tcp.");
+ serviceInfo.setPort(port);
+
+ registrationListener = new NsdManager.RegistrationListener() {
+ @Override
+ public void onServiceRegistered(NsdServiceInfo nsdServiceInfo) {
+ // Save the service name. Android may have changed it in order to
+ // resolve a conflict, so update the name you initially requested
+ // with the name Android actually used.
+ final String serviceName = nsdServiceInfo.getServiceName();
+
+ Log.i(TAG, "Service name: " + serviceName);
+
+ final MonitorActivity ma = monitorActivity;
+ if (ma != null) {
+ ma.runOnUiThread(() -> {
+ final TextView statusText = ma.findViewById(R.id.textStatus);
+ statusText.setText(R.string.waitingForParent);
+
+ final TextView serviceText = ma.findViewById(R.id.textService);
+ serviceText.setText(serviceName);
+
+ final TextView portText = ma.findViewById(R.id.port);
+ portText.setText(Integer.toString(port));
+ });
+ }
+
+ }
+
+ @Override
+ public void onRegistrationFailed(NsdServiceInfo serviceInfo, int errorCode) {
+ // Registration failed! Put debugging code here to determine why.
+ Log.e(TAG, "Registration failed: " + errorCode);
+ }
+
+ @Override
+ public void onServiceUnregistered(NsdServiceInfo arg0) {
+ // Service has been unregistered. This only happens when you call
+ // NsdManager.unregisterService() and pass in this listener.
+
+ Log.i(TAG, "Unregistering service");
+ }
+
+ @Override
+ public void onUnregistrationFailed(NsdServiceInfo serviceInfo, int errorCode) {
+ // Unregistration failed. Put debugging code here to determine why.
+
+ Log.e(TAG, "Unregistration failed: " + errorCode);
+ }
+ };
+
+ nsdManager.registerService(
+ serviceInfo, NsdManager.PROTOCOL_DNS_SD, registrationListener);
+ }
+
+ private void unregisterService() {
+ NsdManager.RegistrationListener currentListener = registrationListener;
+ if (currentListener != null) {
+ Log.i(TAG, "Unregistering monitoring service");
+
+ nsdManager.unregisterService(currentListener);
+ registrationListener = null;
+ }
+ }
+
+ private void createNotificationChannel() {
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
+ NotificationChannel serviceChannel = new NotificationChannel(
+ CHANNEL_ID,
+ "Foreground Service Channel",
+ NotificationManager.IMPORTANCE_DEFAULT
+ );
+ notificationManager.createNotificationChannel(serviceChannel);
+ }
+ }
+
+ private Notification buildNotification() {
+ CharSequence text = "Child Device";
+ // Set the info for the views that show in the notification panel.
+ NotificationCompat.Builder b = new NotificationCompat.Builder(this, CHANNEL_ID);
+ b.setSmallIcon(R.drawable.listening_notification) // the status icon
+ .setOngoing(true)
+ .setTicker(text) // the status text
+ .setContentTitle(text); // the label of the entry
+ return b.build();
+ }
+
+
+ @Override
+ public void onDestroy() {
+ Thread mt = monitorThread;
+ if (mt != null) {
+ mt.interrupt();
+ monitorThread = null;
+ }
+
+ unregisterService();
+
+ connectionToken = null;
+ if (currentSocket != null) {
+ try {
+ currentSocket.close();
+ currentSocket = null;
+ } catch (IOException e) {
+ Log.e(TAG, "Failed to close active socket on port " + currentPort);
+ }
+ }
+
+ // Cancel the persistent notification.
+ int NOTIFICATION = R.string.listening;
+ notificationManager.cancel(NOTIFICATION);
+
+ stopForeground(true);
+ // Tell the user we stopped.
+ Toast.makeText(this, R.string.stopped, Toast.LENGTH_SHORT).show();
+ super.onDestroy();
+ }
+
+ @Override
+ public IBinder onBind(Intent intent) {
+ return binder;
+ }
+
+ public class MonitorBinder extends Binder {
+ MonitorService getService() {
+ return MonitorService.this;
+ }
+ }
+
+}