Merge pull request #10 from fwiesel/foreground-service-child

Move Monitor Mode for Child to its own Service
This commit is contained in:
edr
2024-02-21 22:14:05 +01:00
committed by GitHub
4 changed files with 375 additions and 187 deletions

View File

@@ -26,10 +26,15 @@
android:icon="@mipmap/ic_launcher" android:icon="@mipmap/ic_launcher"
android:label="@string/app_name" android:label="@string/app_name"
android:theme="@android:style/Theme.Holo"> android:theme="@android:style/Theme.Holo">
<service <service
android:name=".ListenService" android:name=".ListenService"
android:enabled="true" android:enabled="true"
android:exported="false"/> android:exported="false"/>
<service
android:name=".MonitorService"
android:enabled="true"
android:exported="false"/>
<activity <activity
android:name=".StartActivity" android:name=".StartActivity"
@@ -55,5 +60,4 @@
android:configChanges="orientation|screenSize" android:configChanges="orientation|screenSize"
android:parentActivityName=".DiscoverActivity" /> android:parentActivityName=".DiscoverActivity" />
</application> </application>
</manifest> </manifest>

View File

@@ -82,7 +82,7 @@ public class ListenActivity extends Activity {
// applications). // applications).
if (bindService(intent, connection, Context.BIND_AUTO_CREATE)) { if (bindService(intent, connection, Context.BIND_AUTO_CREATE)) {
shouldUnbind = true; shouldUnbind = true;
Log.i(TAG, "Bound service"); Log.i(TAG, "Bound listen service");
} else { } else {
Log.e(TAG, "Error: The requested service doesn't " + Log.e(TAG, "Error: The requested service doesn't " +
"exist, or this client isn't allowed access to it."); "exist, or this client isn't allowed access to it.");

View File

@@ -16,144 +16,64 @@
*/ */
package de.rochefort.childmonitor; package de.rochefort.childmonitor;
import static de.rochefort.childmonitor.AudioCodecDefines.CODEC;
import android.app.Activity; import android.app.Activity;
import android.content.ComponentName;
import android.content.Context; import android.content.Context;
import android.media.AudioRecord; import android.content.Intent;
import android.media.MediaRecorder; import android.content.ServiceConnection;
import android.net.ConnectivityManager; import android.net.ConnectivityManager;
import android.net.LinkAddress; import android.net.LinkAddress;
import android.net.Network; import android.net.Network;
import android.net.NetworkInfo; 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.Bundle;
import android.os.IBinder;
import android.support.v4.content.ContextCompat;
import android.util.Log; import android.util.Log;
import android.widget.TextView; import android.widget.TextView;
import android.widget.Toast;
import java.io.IOException;
import java.io.OutputStream;
import java.net.InetAddress; import java.net.InetAddress;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List; import java.util.List;
import java.util.Objects;
public class MonitorActivity extends Activity { public class MonitorActivity extends Activity {
final static String TAG = "ChildMonitor"; final static String TAG = "ChildMonitor";
private final ServiceConnection connection = new ServiceConnection() {
private NsdManager nsdManager; public void onServiceConnected(ComponentName className, IBinder service) {
// This is called when the connection with the service has been
private NsdManager.RegistrationListener registrationListener; // established, giving us the service object we can use to
// interact with the service. Because we have bound to an explicit
private ServerSocket currentSocket; // service that we know is running in our own process, we can
// cast its IBinder to a concrete class and directly access it.
private Object connectionToken; MonitorService bs = ((MonitorService.MonitorBinder) service).getService();
bs.setMonitorActivity(MonitorActivity.this);
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);
} }
final int pcmBufferSize = bufferSize*2; public void onServiceDisconnected(ComponentName className) {
final short[] pcmBuffer = new short[pcmBufferSize]; // This is called when the connection with the service has been
final byte[] ulawBuffer = new byte[pcmBufferSize]; // unexpectedly disconnected -- that is, its process crashed.
// Because it is running in our same process, we should never
try { // see this happen.
audioRecord.startRecording(); Toast.makeText(MonitorActivity.this, R.string.disconnected,
final OutputStream out = socket.getOutputStream(); Toast.LENGTH_SHORT).show();
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); private boolean shouldUnbind;
} finally {
audioRecord.stop();
}
}
@Override @Override
protected void onCreate(Bundle savedInstanceState) { protected void onCreate(Bundle savedInstanceState) {
Log.i(TAG, "ChildMonitor start"); Log.i(TAG, "ChildMonitor start");
super.onCreate(savedInstanceState); super.onCreate(savedInstanceState);
setContentView(R.layout.activity_monitor); 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); final TextView addressText = findViewById(R.id.address);
List<String> listenAddresses = getListenAddresses(); List<String> listenAddresses = getListenAddresses();
if(!listenAddresses.isEmpty()) { if (!listenAddresses.isEmpty()) {
StringBuilder sb = new StringBuilder(); StringBuilder sb = new StringBuilder();
for (int i = 0; i < listenAddresses.size(); i++) { for (int i = 0; i < listenAddresses.size(); i++) {
String listenAddress = listenAddresses.get(i); String listenAddress = listenAddresses.get(i);
sb.append(listenAddress); sb.append(listenAddress);
if (i != listenAddresses.size() -1) { if (i != listenAddresses.size() - 1) {
sb.append("\n\n"); sb.append("\n\n");
} }
} }
@@ -162,10 +82,11 @@ public class MonitorActivity extends Activity {
addressText.setText(R.string.notConnected); addressText.setText(R.string.notConnected);
} }
ensureServiceRunningAndBind();
} }
private List<String> getListenAddresses() { private List<String> getListenAddresses() {
ConnectivityManager cm = (ConnectivityManager)getSystemService(Context.CONNECTIVITY_SERVICE); ConnectivityManager cm = (ConnectivityManager) getSystemService(Context.CONNECTIVITY_SERVICE);
List<String> listenAddresses = new ArrayList<>(); List<String> listenAddresses = new ArrayList<>();
if (cm != null) { if (cm != null) {
for (Network network : cm.getAllNetworks()) { for (Network network : cm.getAllNetworks()) {
@@ -186,87 +107,37 @@ public class MonitorActivity extends Activity {
} }
@Override @Override
protected void onStop() { public void onDestroy() {
Log.i(TAG, "ChildMonitor stop"); doUnbindAndStopService();
super.onDestroy();
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();
} }
private void registerService(final int port) { void ensureServiceRunningAndBind() {
final NsdServiceInfo serviceInfo = new NsdServiceInfo(); final Context context = this;
serviceInfo.setServiceName("ChildMonitor on " + Build.MODEL); final Intent intent = new Intent(context, MonitorService.class);
serviceInfo.setServiceType("_childmonitor._tcp."); ContextCompat.startForegroundService(context, intent);
serviceInfo.setPort(port); // Attempts to establish a connection with the service. We use an
// explicit class name because we want a specific service
registrationListener = new NsdManager.RegistrationListener() { // implementation that we know will be running in our own process
@Override // (and thus won't be supporting component replacement by other
public void onServiceRegistered(NsdServiceInfo nsdServiceInfo) { // applications).
// Save the service name. Android may have changed it in order to if (bindService(intent, connection, Context.BIND_AUTO_CREATE)) {
// resolve a conflict, so update the name you initially requested shouldUnbind = true;
// with the name Android actually used. Log.i(TAG, "Bound monitor service");
final String serviceName = nsdServiceInfo.getServiceName(); } else {
Log.e(TAG, "Error: The requested service doesn't " +
Log.i(TAG, "Service name: " + serviceName); "exist, or this client isn't allowed access to it.");
}
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 void doUnbindAndStopService() {
public void onRegistrationFailed(NsdServiceInfo serviceInfo, int errorCode) { if (shouldUnbind) {
// Registration failed! Put debugging code here to determine why. // Release information about the service's state.
Log.e(TAG, "Registration failed: " + errorCode); unbindService(connection);
} shouldUnbind = false;
@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);
}
/**
* 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;
} }
final Context context = this;
final Intent intent = new Intent(context, MonitorService.class);
context.stopService(intent);
} }
} }

View File

@@ -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 <http://www.gnu.org/licenses/>.
*/
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;
}
}
}