Compare commits

10 Commits

Author SHA1 Message Date
pi
130906a6b0 added retries and fixxed status update 2025-11-11 01:39:35 +01:00
pi
62f44a1654 play alert at the end + english comments 2025-10-27 12:42:45 +01:00
pi
cb7e9ac8ac 3 retys added frst attempt 2025-10-27 11:39:42 +01:00
pi
adafa6279a now correct "streaming" notice 2025-10-25 12:14:21 +02:00
pi
46cf690ba3 klappt mit multiplen vom originalen child monitor 2025-10-25 10:28:54 +02:00
edr
a08967059b Add VPN hint to README.md 2025-06-07 11:04:15 +02:00
edr
06d793fe16 Merge pull request #27 from yurtpage/rm_tx
Delete unsupported .tx/config for Transifex
2025-05-18 20:28:52 +02:00
Yurt Page
6baa9eb203 Delete unsupported .tx/config 2025-05-18 11:30:05 +03:00
edr
79165dfe5e Update CONTRIBUTING.md
No more translations please
2025-05-12 19:01:33 +02:00
edr
01f357a985 v1.4 2024-04-10 21:30:11 +02:00
26 changed files with 436 additions and 417 deletions

View File

@@ -1,10 +0,0 @@
[main]
host = https://www.transifex.com
[child-monitor.stringsxml]
file_filter = app/src/main/res/values-<lang>/strings.xml
minimum_perc = 0
source_file = app/src/main/res/values/strings.xml
source_lang = en
type = ANDROID

View File

@@ -79,3 +79,9 @@ and potentially merged into the main Child Monitor repository. The preferred
way to do this is to submit a Pull Request to the Child Monitor project. way to do this is to submit a Pull Request to the Child Monitor project.
Changes need to apply cleanly onto the master branch and pass all Changes need to apply cleanly onto the master branch and pass all
unit tests and produce no errors during static analysis. unit tests and produce no errors during static analysis.
## Translations
I decided to stop accepting any PRs for translations except for languages that I speak well enough to ensure a minimum quality level.
These would be english, german, french.
For all other languages I will neither accept new strings.xml nor guarantee to keep the present ones around.

View File

@@ -21,6 +21,11 @@ and streams audio. Room for improvement includes:
At the time this project was forked from _Protect Baby Monitor_ there was no obvious open source solution for a At the time this project was forked from _Protect Baby Monitor_ there was no obvious open source solution for a
baby monitor for Android in F-Droid. baby monitor for Android in F-Droid.
# Running on different networks
To use this App with two phones that are not connected to the same WIFI network a VPN can be used.
Since auto discovery is not supported in this scenario the child device's ip address must be entered manually in the parent device. You can find the VPN ip address among the listed ip addresses on the child device once the listen mode was entered.
The child device will usually bind to port 10000 (unless that port is already taken by another application).
# License information # License information
_Child Monitor_ is licensed under the GPLv3. The Ulaw encoding / decoding code is licensed under the Apache License, Version 2.0 and taken from the Android Open Source Project. _Child Monitor_ is licensed under the GPLv3. The Ulaw encoding / decoding code is licensed under the Apache License, Version 2.0 and taken from the Android Open Source Project.

View File

@@ -5,11 +5,11 @@ android {
compileSdk 34 compileSdk 34
defaultConfig { defaultConfig {
applicationId "de.rochefort.childmonitor" applicationId "com.example.childmonitor_multiple"
minSdkVersion 21 minSdkVersion 21
targetSdkVersion 34 targetSdkVersion 34
versionCode 13 versionCode 14
versionName "1.3" versionName "1.4"
} }
buildTypes { buildTypes {
@@ -21,7 +21,7 @@ android {
dependencies { dependencies {
} }
namespace 'de.rochefort.childmonitor' namespace 'com.example.childmonitor_multiple'
lint { lint {
abortOnError true abortOnError true
warning 'MissingTranslation' warning 'MissingTranslation'

View File

@@ -14,10 +14,9 @@
* You should have received a copy of the GNU General Public License * You should have received a copy of the GNU General Public License
* along with Child Monitor. If not, see <http://www.gnu.org/licenses/>. * along with Child Monitor. If not, see <http://www.gnu.org/licenses/>.
*/ */
package de.rochefort.childmonitor package com.example.childmonitor_multiple
import android.media.AudioFormat import android.media.AudioFormat
import de.rochefort.childmonitor.audio.G711UCodec
object AudioCodecDefines { object AudioCodecDefines {
const val FREQUENCY = 8000 const val FREQUENCY = 8000

View File

@@ -14,7 +14,7 @@
* You should have received a copy of the GNU General Public License * You should have received a copy of the GNU General Public License
* along with Child Monitor. If not, see <http://www.gnu.org/licenses/>. * along with Child Monitor. If not, see <http://www.gnu.org/licenses/>.
*/ */
package de.rochefort.childmonitor package com.example.childmonitor_multiple
import android.app.Activity import android.app.Activity
import android.content.Intent import android.content.Intent

View File

@@ -1,20 +1,4 @@
/* package com.example.childmonitor_multiple
* 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 android.app.Activity import android.app.Activity
import android.content.ComponentName import android.content.ComponentName
@@ -28,12 +12,14 @@ import android.util.Log
import android.widget.TextView import android.widget.TextView
import android.widget.Toast import android.widget.Toast
import androidx.core.content.ContextCompat import androidx.core.content.ContextCompat
import de.rochefort.childmonitor.ListenService.ListenBinder import com.example.childmonitor_multiple.ListenService.ListenBinder
class ListenActivity : Activity() { class ListenActivity : Activity() {
// Don't attempt to unbind from the service unless the client has received some // Don't attempt to unbind from the service unless the client has received some
// information about the service's state. // information about the service's state.
private var shouldUnbind = false private var shouldUnbind = false
private lateinit var statusText: TextView
private val connection: ServiceConnection = object : ServiceConnection { private val connection: ServiceConnection = object : ServiceConnection {
override fun onServiceConnected(className: ComponentName, service: IBinder) { override fun onServiceConnected(className: ComponentName, service: IBinder) {
// This is called when the connection with the service has been // This is called when the connection with the service has been
@@ -42,14 +28,17 @@ class ListenActivity : Activity() {
// service that we know is running in our own process, we can // service that we know is running in our own process, we can
// cast its IBinder to a concrete class and directly access it. // cast its IBinder to a concrete class and directly access it.
val bs = (service as ListenBinder).service val bs = (service as ListenBinder).service
Toast.makeText(this@ListenActivity, R.string.connect, Toast.makeText(this@ListenActivity, R.string.connect, Toast.LENGTH_SHORT).show()
Toast.LENGTH_SHORT).show()
val connectedText = findViewById<TextView>(R.id.connectedTo) val connectedText = findViewById<TextView>(R.id.connectedTo)
connectedText.text = bs.childDeviceName connectedText.text = bs.childDeviceName
val volumeView = findViewById<VolumeView>(R.id.volume) val volumeView = findViewById<VolumeView>(R.id.volume)
volumeView.volumeHistory = bs.volumeHistory volumeView.volumeHistory = bs.volumeHistory
bs.onUpdate = { volumeView.postInvalidate() } bs.onUpdate = { volumeView.postInvalidate() }
bs.onError = { postErrorMessage() } bs.onError = { postErrorMessage() }
statusText.text = "Connected and listening..."
} }
override fun onServiceDisconnected(className: ComponentName) { override fun onServiceDisconnected(className: ComponentName) {
@@ -57,8 +46,8 @@ class ListenActivity : Activity() {
// unexpectedly disconnected -- that is, its process crashed. // unexpectedly disconnected -- that is, its process crashed.
// Because it is running in our same process, we should never // Because it is running in our same process, we should never
// see this happen. // see this happen.
Toast.makeText(this@ListenActivity, R.string.disconnected, Toast.makeText(this@ListenActivity, R.string.disconnected, Toast.LENGTH_SHORT).show()
Toast.LENGTH_SHORT).show() statusText.text = "Disconnected"
} }
} }
@@ -77,41 +66,38 @@ class ListenActivity : Activity() {
// (and thus won't be supporting component replacement by other // (and thus won't be supporting component replacement by other
// applications). // applications).
if (bindService(intent, connection, BIND_AUTO_CREATE)) { if (bindService(intent, connection, BIND_AUTO_CREATE)) {
this.shouldUnbind = true shouldUnbind = true
Log.i(TAG, "Bound listen service") Log.i(TAG, "Bound to ListenService")
} else { } else {
Log.e(TAG, "Error: The requested service doesn't " + Log.e(TAG, "Error: Could not bind to ListenService.")
"exist, or this client isn't allowed access to it.") statusText.text = "Failed to bind service."
} }
} }
private fun doUnbindAndStopService() { private fun doUnbindAndStopService() {
if (this.shouldUnbind) { if (shouldUnbind) {
// Release information about the service's state.
unbindService(connection) unbindService(connection)
this.shouldUnbind = false shouldUnbind = false
} }
val context: Context = this stopService(Intent(this, ListenService::class.java))
val intent = Intent(context, ListenService::class.java)
context.stopService(intent)
} }
fun postErrorMessage() { fun postErrorMessage() {
val status = findViewById<TextView>(R.id.textStatus) statusText.post {
status.post { status.setText(R.string.disconnected) } statusText.text = "Connection failed after 3 attempts."
}
} }
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
val bundle = this.intent.extras
ensureServiceRunningAndBind(bundle)
this.volumeControlStream = AudioManager.STREAM_MUSIC
setContentView(R.layout.activity_listen) setContentView(R.layout.activity_listen)
val statusText = findViewById<TextView>(R.id.textStatus) statusText = findViewById(R.id.textStatus)
statusText.setText(R.string.listening) statusText.text = "Attempting to connect..."
volumeControlStream = AudioManager.STREAM_MUSIC
ensureServiceRunningAndBind(intent.extras)
} }
public override fun onDestroy() { override fun onDestroy() {
doUnbindAndStopService() doUnbindAndStopService()
super.onDestroy() super.onDestroy()
} }

View File

@@ -14,7 +14,7 @@
* You should have received a copy of the GNU General Public License * You should have received a copy of the GNU General Public License
* along with Child Monitor. If not, see <http://www.gnu.org/licenses/>. * along with Child Monitor. If not, see <http://www.gnu.org/licenses/>.
*/ */
package de.rochefort.childmonitor package com.example.childmonitor_multiple
import android.app.Notification import android.app.Notification
import android.app.NotificationChannel import android.app.NotificationChannel
@@ -49,6 +49,9 @@ class ListenService : Service() {
var childDeviceName: String? = null var childDeviceName: String? = null
private set private set
var onError: (() -> Unit)? = null
var onUpdate: (() -> Unit)? = null
override fun onCreate() { override fun onCreate() {
super.onCreate() super.onCreate()
this.notificationManager = getSystemService(NOTIFICATION_SERVICE) as NotificationManager this.notificationManager = getSystemService(NOTIFICATION_SERVICE) as NotificationManager
@@ -62,11 +65,14 @@ class ListenService : Service() {
val name = it.getString("name") val name = it.getString("name")
childDeviceName = name childDeviceName = name
val n = buildNotification(name) val n = buildNotification(name)
val foregroundServiceType = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) ServiceInfo.FOREGROUND_SERVICE_TYPE_MEDIA_PLAYBACK else 0 // Keep the linter happy val foregroundServiceType =
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R)
ServiceInfo.FOREGROUND_SERVICE_TYPE_MEDIA_PLAYBACK else 0
ServiceCompat.startForeground(this, ID, n, foregroundServiceType) ServiceCompat.startForeground(this, ID, n, foregroundServiceType)
val address = it.getString("address") val address = it.getString("address")
val port = it.getInt("port") val port = it.getInt("port")
doListen(address, port) doListenWithRetries(address, port)
} }
return START_REDELIVER_INTENT return START_REDELIVER_INTENT
} }
@@ -82,97 +88,113 @@ class ListenService : Service() {
Toast.makeText(this, R.string.stopped, Toast.LENGTH_SHORT).show() Toast.makeText(this, R.string.stopped, Toast.LENGTH_SHORT).show()
} }
override fun onBind(intent: Intent): IBinder { override fun onBind(intent: Intent): IBinder = binder
return binder
}
private fun buildNotification(name: String?): Notification {
// In this sample, we'll use the same text for the ticker and the expanded notification
val text = getText(R.string.listening)
// The PendingIntent to launch our activity if the user selects this notification
val contentIntent = PendingIntent.getActivity(this, 0,
Intent(this, ListenActivity::class.java), PendingIntent.FLAG_IMMUTABLE)
// Set the info for the views that show in the notification panel.
val b = 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
.setContentText(name) // the contents of the entry
.setContentIntent(contentIntent)
return b.build()
}
private fun createNotificationChannel() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
val serviceChannel = NotificationChannel(
CHANNEL_ID,
"Foreground Service Channel",
NotificationManager.IMPORTANCE_DEFAULT
)
notificationManager.createNotificationChannel(serviceChannel)
}
}
inner class ListenBinder : Binder() { inner class ListenBinder : Binder() {
val service: ListenService val service: ListenService
get() = this@ListenService get() = this@ListenService
} }
var onError: (() -> Unit)? = null private fun buildNotification(name: String?, status: String = getString(R.string.listening)): Notification {
var onUpdate: (() -> Unit)? = null // In this sample, we'll use the same text for the ticker and the expanded notification
private fun doListen(address: String?, port: Int) { val text = status
val contentIntent = PendingIntent.getActivity(
this, 0,
Intent(this, ListenActivity::class.java),
PendingIntent.FLAG_IMMUTABLE
)
return NotificationCompat.Builder(this, CHANNEL_ID)
.setSmallIcon(R.drawable.listening_notification)
.setOngoing(true)
.setTicker(text)
.setContentTitle(text)
.setContentText(name)
.setContentIntent(contentIntent)
.build()
}
private fun createNotificationChannel() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
val serviceChannel = NotificationChannel(
CHANNEL_ID,
"Foreground Service Channel",
NotificationManager.IMPORTANCE_DEFAULT
)
notificationManager.createNotificationChannel(serviceChannel)
}
}
/** New logic: automatically retry connection up to 3 times **/
private fun doListenWithRetries(address: String?, port: Int) {
val lt = Thread { val lt = Thread {
try { var attempts = 0
val socket = Socket(address, port) var connected = false
socket.soTimeout = 30_000
val success = streamAudio(socket) while (attempts < 3 && !connected && !Thread.currentThread().isInterrupted) {
if (!success) { try {
playAlert() Log.i(TAG, "Connection attempt ${attempts + 1} to $address:$port ...")
onError?.invoke() val socket = Socket(address, port)
socket.soTimeout = 30_000
connected = streamAudio(socket)
if (!connected) {
Log.w(TAG, "Streaming failed, attempt ${attempts + 1}")
attempts++
Thread.sleep(2000)
}
} catch (e: IOException) {
attempts++
Log.e(TAG, "Error while connecting (attempt $attempts of 3)", e)
if (attempts < 3) Thread.sleep(2000)
} }
} catch (e : IOException) { }
Log.e(TAG, "Error opening socket to $address on port $port", e)
if (!connected) {
Log.e(TAG, "Failed to connect after 3 attempts.")
playAlert()
updateNotification(getString(R.string.disconnected))
onError?.invoke()
} else {
Log.i(TAG, "Connection established successfully.")
} }
} }
this.listenThread = lt this.listenThread = lt
lt.start() lt.start()
} }
private fun streamAudio(socket: Socket): Boolean { private fun streamAudio(socket: Socket): Boolean {
Log.i(TAG, "Setting up stream") Log.i(TAG, "Starting audio stream")
val audioTrack = AudioTrack(AudioManager.STREAM_MUSIC, val audioTrack = AudioTrack(
AudioManager.STREAM_MUSIC,
frequency, frequency,
channelConfiguration, channelConfiguration,
audioEncoding, audioEncoding,
bufferSize, bufferSize,
AudioTrack.MODE_STREAM) AudioTrack.MODE_STREAM
)
try { try {
audioTrack.play() audioTrack.play()
} catch (e : java.lang.IllegalStateException) { } catch (e: IllegalStateException) {
Log.e(TAG, "Failed to play streamed audio audio for other reason", e) Log.e(TAG, "Failed to start AudioTrack", e)
return false return false
} }
val inputStream = try { val inputStream = try {
socket.getInputStream() socket.getInputStream()
} catch (e: IOException) { } catch (e: IOException) {
Log.e(TAG, "Failed to read audio audio for socket", e) Log.e(TAG, "Failed to open input stream", e)
return false return false
} }
val readBuffer = ByteArray(byteBufferSize) val readBuffer = ByteArray(byteBufferSize)
val decodedBuffer = ShortArray(byteBufferSize * 2) val decodedBuffer = ShortArray(byteBufferSize * 2)
try {
return try {
while (!Thread.currentThread().isInterrupted) { while (!Thread.currentThread().isInterrupted) {
val len = inputStream.read(readBuffer) val len = inputStream.read(readBuffer)
if (len < 0) { if (len < 0) return false
// If the current thread was not interrupted this means the remote stopped streaming val decoded = AudioCodecDefines.CODEC.decode(decodedBuffer, readBuffer, len, 0)
return Thread.currentThread().isInterrupted
}
val decoded: Int = AudioCodecDefines.CODEC.decode(decodedBuffer, readBuffer, len, 0)
if (decoded > 0) { if (decoded > 0) {
audioTrack.write(decodedBuffer, 0, decoded) audioTrack.write(decodedBuffer, 0, decoded)
val decodedBytes = ShortArray(decoded) val decodedBytes = ShortArray(decoded)
@@ -181,24 +203,35 @@ class ListenService : Service() {
onUpdate?.invoke() onUpdate?.invoke()
} }
} }
return true true
} catch (e: Exception) { } catch (e: Exception) {
Log.e(TAG, "Connection failed", e) Log.e(TAG, "Connection lost during streaming", e)
return false updateNotification(getString(R.string.disconnected))
playAlert()
onError?.invoke()
false
} finally { } finally {
audioTrack.stop() try {
socket.close() audioTrack.stop()
audioTrack.release()
socket.close()
} catch (_: Exception) {}
} }
} }
private fun updateNotification(status: String) {
val n = buildNotification(childDeviceName, status)
notificationManager.notify(ID, n)
}
private fun playAlert() { private fun playAlert() {
val mp = MediaPlayer.create(this, R.raw.upward_beep_chromatic_fifths) val mp = MediaPlayer.create(this, R.raw.upward_beep_chromatic_fifths)
if (mp != null) { if (mp != null) {
Log.i(TAG, "Playing alert") Log.i(TAG, "Playing alert sound")
mp.setOnCompletionListener { obj: MediaPlayer -> obj.release() } mp.setOnCompletionListener { it.release() }
mp.start() mp.start()
} else { } else {
Log.e(TAG, "Failed to play alert") Log.e(TAG, "Failed to play alert sound")
} }
} }

View File

@@ -14,7 +14,7 @@
* You should have received a copy of the GNU General Public License * You should have received a copy of the GNU General Public License
* along with Child Monitor. If not, see <http://www.gnu.org/licenses/>. * along with Child Monitor. If not, see <http://www.gnu.org/licenses/>.
*/ */
package de.rochefort.childmonitor package com.example.childmonitor_multiple
import android.app.Activity import android.app.Activity
import android.content.ComponentName import android.content.ComponentName
@@ -28,7 +28,7 @@ import android.util.Log
import android.widget.TextView import android.widget.TextView
import android.widget.Toast import android.widget.Toast
import androidx.core.content.ContextCompat import androidx.core.content.ContextCompat
import de.rochefort.childmonitor.MonitorService.MonitorBinder import com.example.childmonitor_multiple.MonitorService.MonitorBinder
class MonitorActivity : Activity() { class MonitorActivity : Activity() {
private val connection: ServiceConnection = object : ServiceConnection { private val connection: ServiceConnection = object : ServiceConnection {

View File

@@ -0,0 +1,252 @@
package com.example.childmonitor_multiple
import android.app.Notification
import android.app.NotificationChannel
import android.app.NotificationManager
import android.app.Service
import android.content.Intent
import android.content.pm.ServiceInfo
import android.media.AudioRecord
import android.media.MediaRecorder
import android.net.nsd.NsdManager
import android.net.nsd.NsdManager.RegistrationListener
import android.net.nsd.NsdServiceInfo
import android.os.Binder
import android.os.Build
import android.os.IBinder
import android.util.Log
import android.widget.TextView
import android.widget.Toast
import androidx.core.app.NotificationCompat
import androidx.core.app.ServiceCompat
import java.io.IOException
import java.net.ServerSocket
import java.net.Socket
class MonitorService : Service() {
private val binder: IBinder = MonitorBinder()
private lateinit var nsdManager: NsdManager
private var registrationListener: RegistrationListener? = null
private var currentSocket: ServerSocket? = null
private var connectionToken: Any? = null
private var currentPort = 0
private lateinit var notificationManager: NotificationManager
private var monitorThread: Thread? = null
var monitorActivity: MonitorActivity? = null
override fun onCreate() {
super.onCreate()
this.notificationManager = getSystemService(NOTIFICATION_SERVICE) as NotificationManager
this.nsdManager = this.getSystemService(NSD_SERVICE) as NsdManager
this.currentPort = 10000
this.currentSocket = null
Log.i(TAG, "ChildMonitor start")
}
override fun onStartCommand(intent: Intent, flags: Int, startId: Int): Int {
createNotificationChannel()
val n = buildNotification()
val foregroundServiceType =
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R)
ServiceInfo.FOREGROUND_SERVICE_TYPE_MICROPHONE
else 0
ServiceCompat.startForeground(this, ID, n, foregroundServiceType)
ensureMonitorThread()
return START_REDELIVER_INTENT
}
private fun ensureMonitorThread() {
if (monitorThread?.isAlive == true) return
val currentToken = Any()
this.connectionToken = currentToken
monitorThread = Thread {
while (connectionToken == currentToken) {
try {
ServerSocket(currentPort).use { serverSocket ->
currentSocket = serverSocket
registerService(serverSocket.localPort)
val clients = mutableListOf<Socket>()
// Thread: neue Clients akzeptieren
Thread {
try {
while (!Thread.currentThread().isInterrupted) {
val client = serverSocket.accept()
client.tcpNoDelay = true
synchronized(clients) { clients.add(client) }
Log.i(TAG, "Neuer Client: ${client.inetAddress}")
// Statusanzeige auf Streaming
monitorActivity?.runOnUiThread {
monitorActivity?.findViewById<TextView>(R.id.textStatus)
?.setText(R.string.streaming)
}
}
} catch (e: IOException) {
Log.e(TAG, "Client-Akzeptierungsfehler: ${e.message}")
}
}.start()
// Aufnahme + Multi-Client-Streaming starten
handleMultiClientStreaming(serverSocket, clients)
}
} catch (e: Exception) {
if (connectionToken == currentToken) {
currentPort++
Log.e(TAG, "Failed to open server socket. Port increased to $currentPort", e)
}
}
}
}.also { it.start() }
}
private fun handleMultiClientStreaming(serverSocket: ServerSocket, clients: MutableList<Socket>) {
val frequency = AudioCodecDefines.FREQUENCY
val channelConfiguration = AudioCodecDefines.CHANNEL_CONFIGURATION_IN
val audioEncoding = AudioCodecDefines.ENCODING
val bufferSize = AudioRecord.getMinBufferSize(frequency, channelConfiguration, audioEncoding)
val audioRecord = AudioRecord(
MediaRecorder.AudioSource.MIC,
frequency,
channelConfiguration,
audioEncoding,
bufferSize
)
val pcmBufferSize = bufferSize * 2
val pcmBuffer = ShortArray(pcmBufferSize)
val ulawBuffer = ByteArray(pcmBufferSize)
try {
audioRecord.startRecording()
Log.i(TAG, "Mehrere Clients aktiv Aufnahme gestartet.")
while (!Thread.currentThread().isInterrupted && !serverSocket.isClosed) {
val read = audioRecord.read(pcmBuffer, 0, bufferSize)
if (read > 0) {
val encoded = AudioCodecDefines.CODEC.encode(pcmBuffer, read, ulawBuffer, 0)
synchronized(clients) {
val iterator = clients.iterator()
while (iterator.hasNext()) {
val client = iterator.next()
try {
client.getOutputStream().write(ulawBuffer, 0, encoded)
} catch (e: IOException) {
try { client.close() } catch (_: IOException) {}
iterator.remove()
Log.w(TAG, "Client getrennt: ${client.inetAddress}")
}
}
// Wenn keine Clients mehr da, Status auf "Warte auf Eltern"
if (clients.isEmpty()) {
monitorActivity?.runOnUiThread {
monitorActivity?.findViewById<TextView>(R.id.textStatus)
?.setText(R.string.waitingForParent)
}
}
}
}
}
} catch (e: Exception) {
Log.e(TAG, "Streaming-Fehler", e)
} finally {
audioRecord.stop()
audioRecord.release()
synchronized(clients) {
clients.forEach {
try { it.close() } catch (_: IOException) {}
}
clients.clear()
}
Log.i(TAG, "Audioaufnahme beendet, alle Clients getrennt.")
monitorActivity?.runOnUiThread {
monitorActivity?.findViewById<TextView>(R.id.textStatus)
?.setText(R.string.waitingForParent)
}
}
}
private fun registerService(port: Int) {
val serviceInfo = NsdServiceInfo().apply {
serviceName = "ChildMonitor on ${Build.MODEL}"
serviceType = "_childmonitor._tcp."
this.port = port
}
registrationListener = object : RegistrationListener {
override fun onServiceRegistered(nsdServiceInfo: NsdServiceInfo) {
monitorActivity?.runOnUiThread {
monitorActivity?.findViewById<TextView>(R.id.textStatus)
?.setText(R.string.waitingForParent)
monitorActivity?.findViewById<TextView>(R.id.textService)
?.text = nsdServiceInfo.serviceName
monitorActivity?.findViewById<TextView>(R.id.port)
?.text = port.toString()
}
}
override fun onRegistrationFailed(serviceInfo: NsdServiceInfo, errorCode: Int) {
Log.e(TAG, "Registration failed: $errorCode")
}
override fun onServiceUnregistered(arg0: NsdServiceInfo) {}
override fun onUnregistrationFailed(serviceInfo: NsdServiceInfo, errorCode: Int) {}
}
nsdManager.registerService(serviceInfo, NsdManager.PROTOCOL_DNS_SD, registrationListener)
}
private fun unregisterService() {
registrationListener?.let {
registrationListener = null
nsdManager.unregisterService(it)
}
}
private fun createNotificationChannel() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
val channel = NotificationChannel(
CHANNEL_ID,
"Foreground Service Channel",
NotificationManager.IMPORTANCE_DEFAULT
)
notificationManager.createNotificationChannel(channel)
}
}
private fun buildNotification(): Notification {
return NotificationCompat.Builder(this, CHANNEL_ID)
.setSmallIcon(R.drawable.listening_notification)
.setOngoing(true)
.setTicker("Child Device")
.setContentTitle("Child Device")
.build()
}
override fun onDestroy() {
monitorThread?.interrupt()
monitorThread = null
unregisterService()
connectionToken = null
currentSocket?.close()
currentSocket = null
notificationManager.cancel(R.string.listening)
ServiceCompat.stopForeground(this, ServiceCompat.STOP_FOREGROUND_REMOVE)
Toast.makeText(this, R.string.stopped, Toast.LENGTH_SHORT).show()
super.onDestroy()
}
override fun onBind(intent: Intent): IBinder = binder
inner class MonitorBinder : Binder() {
val service: MonitorService get() = this@MonitorService
}
companion object {
const val TAG = "MonitorService"
const val CHANNEL_ID = TAG
const val ID = 1338
}
}

View File

@@ -14,7 +14,7 @@
* You should have received a copy of the GNU General Public License * You should have received a copy of the GNU General Public License
* along with Child Monitor. If not, see <http://www.gnu.org/licenses/>. * along with Child Monitor. If not, see <http://www.gnu.org/licenses/>.
*/ */
package de.rochefort.childmonitor package com.example.childmonitor_multiple
import android.Manifest import android.Manifest
import android.app.Activity import android.app.Activity

View File

@@ -14,7 +14,7 @@
* You should have received a copy of the GNU General Public License * You should have received a copy of the GNU General Public License
* along with Child Monitor. If not, see <http://www.gnu.org/licenses/>. * along with Child Monitor. If not, see <http://www.gnu.org/licenses/>.
*/ */
package de.rochefort.childmonitor package com.example.childmonitor_multiple
import android.os.Handler import android.os.Handler
import android.os.Looper import android.os.Looper

View File

@@ -14,7 +14,7 @@
* You should have received a copy of the GNU General Public License * You should have received a copy of the GNU General Public License
* along with Child Monitor. If not, see <http://www.gnu.org/licenses/>. * along with Child Monitor. If not, see <http://www.gnu.org/licenses/>.
*/ */
package de.rochefort.childmonitor package com.example.childmonitor_multiple
import android.content.Context import android.content.Context
import android.graphics.Canvas import android.graphics.Canvas

View File

@@ -16,7 +16,7 @@
* Taken from https://android.googlesource.com/platform/external/nist-sip/+/6f95fdeab4481188b6260041b41d1db12b101266/src/com/android/sip/media/G711UCodec.java * Taken from https://android.googlesource.com/platform/external/nist-sip/+/6f95fdeab4481188b6260041b41d1db12b101266/src/com/android/sip/media/G711UCodec.java
* Adopted to the needs of the Child Monitor project. * Adopted to the needs of the Child Monitor project.
*/ */
package de.rochefort.childmonitor.audio package com.example.childmonitor_multiple
/** /**
* G.711 codec. This class provides u-law conversion. * G.711 codec. This class provides u-law conversion.

View File

@@ -1,269 +0,0 @@
/*
* 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 android.app.Notification
import android.app.NotificationChannel
import android.app.NotificationManager
import android.app.Service
import android.content.Intent
import android.content.pm.ServiceInfo
import android.media.AudioRecord
import android.media.MediaRecorder
import android.net.nsd.NsdManager
import android.net.nsd.NsdManager.RegistrationListener
import android.net.nsd.NsdServiceInfo
import android.os.Binder
import android.os.Build
import android.os.IBinder
import android.util.Log
import android.widget.TextView
import android.widget.Toast
import androidx.core.app.NotificationCompat
import androidx.core.app.ServiceCompat
import java.io.IOException
import java.net.ServerSocket
import java.net.Socket
class MonitorService : Service() {
private val binder: IBinder = MonitorBinder()
private lateinit var nsdManager: NsdManager
private var registrationListener: RegistrationListener? = null
private var currentSocket: ServerSocket? = null
private var connectionToken: Any? = null
private var currentPort = 0
private lateinit var notificationManager: NotificationManager
private var monitorThread: Thread? = null
var monitorActivity: MonitorActivity? = null
private fun serviceConnection(socket: Socket) {
val ma = this.monitorActivity
ma?.runOnUiThread {
val statusText = ma.findViewById<TextView>(R.id.textStatus)
statusText.setText(R.string.streaming)
}
val frequency: Int = AudioCodecDefines.FREQUENCY
val channelConfiguration: Int = AudioCodecDefines.CHANNEL_CONFIGURATION_IN
val audioEncoding: Int = AudioCodecDefines.ENCODING
val bufferSize = AudioRecord.getMinBufferSize(frequency, channelConfiguration, audioEncoding)
val audioRecord: AudioRecord = try {
AudioRecord(
MediaRecorder.AudioSource.MIC,
frequency,
channelConfiguration,
audioEncoding,
bufferSize
)
} catch (e: SecurityException) {
// This should never happen, we asked for permission before
throw RuntimeException(e)
}
val pcmBufferSize = bufferSize * 2
val pcmBuffer = ShortArray(pcmBufferSize)
val ulawBuffer = ByteArray(pcmBufferSize)
try {
audioRecord.startRecording()
val out = socket.getOutputStream()
socket.sendBufferSize = pcmBufferSize
Log.d(TAG, "Socket send buffer size: " + socket.sendBufferSize)
while (socket.isConnected && (this.currentSocket != null) && !Thread.currentThread().isInterrupted) {
val read = audioRecord.read(pcmBuffer, 0, bufferSize)
val encoded: Int = AudioCodecDefines.CODEC.encode(pcmBuffer, read, ulawBuffer, 0)
out.write(ulawBuffer, 0, encoded)
}
} catch (e: Exception) {
Log.e(TAG, "Connection failed", e)
} finally {
audioRecord.stop()
}
}
override fun onCreate() {
Log.i(TAG, "ChildMonitor start")
super.onCreate()
this.notificationManager = getSystemService(NOTIFICATION_SERVICE) as NotificationManager
this.nsdManager = this.getSystemService(NSD_SERVICE) as NsdManager
this.currentPort = 10000
this.currentSocket = null
}
override fun onStartCommand(intent: Intent, flags: Int, startId: Int): Int {
Log.i(TAG, "Received start id $startId: $intent")
// Display a notification about us starting. We put an icon in the status bar.
createNotificationChannel()
val n = buildNotification()
val foregroundServiceType = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) ServiceInfo.FOREGROUND_SERVICE_TYPE_MICROPHONE else 0 // Keep the linter happy
ServiceCompat.startForeground(this, ID, n, foregroundServiceType)
ensureMonitorThread()
return START_REDELIVER_INTENT
}
private fun ensureMonitorThread() {
var mt = this.monitorThread
if (mt != null && mt.isAlive) {
return
}
val currentToken = Any()
this.connectionToken = currentToken
mt = Thread {
while (this.connectionToken == currentToken) {
try {
ServerSocket(this.currentPort).use { serverSocket ->
this.currentSocket = serverSocket
// Store the chosen port.
val localPort = serverSocket.localPort
// Register the service so that parent devices can
// locate the child device
registerService(localPort)
serverSocket.accept().use { socket ->
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 (e: Exception) {
if (this.connectionToken == currentToken) {
// Just in case
this.currentPort++
Log.e(TAG, "Failed to open server socket. Port increased to $currentPort", e)
}
}
}
}
this.monitorThread = mt
mt.start()
}
private fun registerService(port: Int) {
val serviceInfo = NsdServiceInfo()
serviceInfo.serviceName = "ChildMonitor on " + Build.MODEL
serviceInfo.serviceType = "_childmonitor._tcp."
serviceInfo.port = port
this.registrationListener = object : RegistrationListener {
override fun 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.
nsdServiceInfo.serviceName.let { serviceName ->
Log.i(TAG, "Service name: $serviceName")
monitorActivity?.let { ma ->
ma.runOnUiThread {
val statusText = ma.findViewById<TextView>(R.id.textStatus)
statusText.setText(R.string.waitingForParent)
val serviceText = ma.findViewById<TextView>(R.id.textService)
serviceText.text = serviceName
val portText = ma.findViewById<TextView>(R.id.port)
portText.text = port.toString()
}
}
}
}
override fun onRegistrationFailed(serviceInfo: NsdServiceInfo, errorCode: Int) {
// Registration failed! Put debugging code here to determine why.
Log.e(TAG, "Registration failed: $errorCode")
}
override fun onServiceUnregistered(arg0: NsdServiceInfo) {
// Service has been unregistered. This only happens when you call
// NsdManager.unregisterService() and pass in this listener.
Log.i(TAG, "Unregistering service")
}
override fun onUnregistrationFailed(serviceInfo: NsdServiceInfo, errorCode: Int) {
// Unregistration failed. Put debugging code here to determine why.
Log.e(TAG, "Unregistration failed: $errorCode")
}
}
nsdManager.registerService(
serviceInfo, NsdManager.PROTOCOL_DNS_SD, registrationListener)
}
private fun unregisterService() {
this.registrationListener?.let {
this.registrationListener = null
Log.i(TAG, "Unregistering monitoring service")
this.nsdManager.unregisterService(it)
}
}
private fun createNotificationChannel() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
val serviceChannel = NotificationChannel(
CHANNEL_ID,
"Foreground Service Channel",
NotificationManager.IMPORTANCE_DEFAULT
)
this.notificationManager.createNotificationChannel(serviceChannel)
}
}
private fun buildNotification(): Notification {
val text: CharSequence = "Child Device"
// Set the info for the views that show in the notification panel.
val b = 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 fun onDestroy() {
this.monitorThread?.let {
this.monitorThread = null
it.interrupt()
}
unregisterService()
this.connectionToken = null
this.currentSocket?.let {
try {
it.close()
} catch (e: IOException) {
Log.e(TAG, "Failed to close active socket on port $currentPort")
}
}
this.currentSocket = null
// Cancel the persistent notification.
this.notificationManager.cancel(R.string.listening)
ServiceCompat.stopForeground(this, ServiceCompat.STOP_FOREGROUND_REMOVE)
// Tell the user we stopped.
Toast.makeText(this, R.string.stopped, Toast.LENGTH_SHORT).show()
super.onDestroy()
}
override fun onBind(intent: Intent): IBinder {
return binder
}
inner class MonitorBinder : Binder() {
val service: MonitorService
get() = this@MonitorService
}
companion object {
const val TAG = "MonitorService"
const val CHANNEL_ID = TAG
const val ID = 1338
}
}

View File

@@ -8,7 +8,7 @@
android:paddingLeft="@dimen/activity_horizontal_margin" android:paddingLeft="@dimen/activity_horizontal_margin"
android:paddingRight="@dimen/activity_horizontal_margin" android:paddingRight="@dimen/activity_horizontal_margin"
android:paddingTop="@dimen/activity_vertical_margin" android:paddingTop="@dimen/activity_vertical_margin"
tools:context="de.rochefort.childmonitor.MonitorActivity" > tools:context="com.example.childmonitor_multiple.MonitorActivity" >
<TextView <TextView
android:id="@+id/parentDeviceTitle" android:id="@+id/parentDeviceTitle"

View File

@@ -8,7 +8,7 @@
android:paddingLeft="@dimen/activity_horizontal_margin" android:paddingLeft="@dimen/activity_horizontal_margin"
android:paddingRight="@dimen/activity_horizontal_margin" android:paddingRight="@dimen/activity_horizontal_margin"
android:paddingTop="@dimen/activity_vertical_margin" android:paddingTop="@dimen/activity_vertical_margin"
tools:context="de.rochefort.childmonitor.MonitorActivity" > tools:context="com.example.childmonitor_multiple.MonitorActivity">
<TextView <TextView
android:id="@+id/parentDeviceTitle" android:id="@+id/parentDeviceTitle"
@@ -67,7 +67,26 @@
android:inputType="number" android:inputType="number"
android:maxLength="5" android:maxLength="5"
android:hint="@string/examplePort" android:hint="@string/examplePort"
android:nextFocusForward="@+id/connectViaAddressButton"/> android:nextFocusForward="@+id/retryField" />
<TextView
android:id="@+id/retryTitle"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textSize="20sp"
android:text="Retry attempts" />
<EditText
android:id="@+id/retryField"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:textSize="20sp"
android:ems="10"
android:inputType="number"
android:maxLength="2"
android:text="3"
android:hint="Default: 3"
android:nextFocusForward="@+id/connectViaAddressButton" />
<Space <Space
android:layout_width="match_parent" android:layout_width="match_parent"
@@ -80,4 +99,4 @@
android:layout_gravity="center" android:layout_gravity="center"
android:text="@string/connect" /> android:text="@string/connect" />
</LinearLayout> </LinearLayout>

View File

@@ -8,7 +8,7 @@
android:paddingLeft="@dimen/activity_horizontal_margin" android:paddingLeft="@dimen/activity_horizontal_margin"
android:paddingRight="@dimen/activity_horizontal_margin" android:paddingRight="@dimen/activity_horizontal_margin"
android:paddingTop="@dimen/activity_vertical_margin" android:paddingTop="@dimen/activity_vertical_margin"
tools:context="de.rochefort.childmonitor.MonitorActivity" > tools:context="com.example.childmonitor_multiple.MonitorActivity" >
<TextView <TextView
android:id="@+id/parentDeviceTitle" android:id="@+id/parentDeviceTitle"

View File

@@ -9,7 +9,7 @@
android:paddingTop="@dimen/activity_vertical_margin" android:paddingTop="@dimen/activity_vertical_margin"
android:paddingRight="@dimen/activity_horizontal_margin" android:paddingRight="@dimen/activity_horizontal_margin"
android:paddingBottom="@dimen/activity_vertical_margin" android:paddingBottom="@dimen/activity_vertical_margin"
tools:context="de.rochefort.childmonitor.MonitorActivity"> tools:context="com.example.childmonitor_multiple.MonitorActivity">
<TextView <TextView
android:id="@id/parentDeviceTitle" android:id="@id/parentDeviceTitle"
@@ -59,7 +59,7 @@
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="15dip" /> android:layout_height="15dip" />
<de.rochefort.childmonitor.VolumeView <com.example.childmonitor_multiple.VolumeView
android:id="@+id/volume" android:id="@+id/volume"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent" /> android:layout_height="match_parent" />

View File

@@ -9,7 +9,7 @@
android:paddingRight="@dimen/activity_horizontal_margin" android:paddingRight="@dimen/activity_horizontal_margin"
android:paddingTop="@dimen/activity_vertical_margin" android:paddingTop="@dimen/activity_vertical_margin"
android:keepScreenOn="true" android:keepScreenOn="true"
tools:context="de.rochefort.childmonitor.MonitorActivity" > tools:context="com.example.childmonitor_multiple.MonitorActivity" >
<TextView <TextView
android:id="@+id/childDeviceTitle" android:id="@+id/childDeviceTitle"
@@ -34,7 +34,7 @@
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:orientation="vertical" android:orientation="vertical"
android:keepScreenOn="true" android:keepScreenOn="true"
tools:context="de.rochefort.childmonitor.MonitorActivity" > tools:context="com.example.childmonitor_multiple.MonitorActivity" >
<TextView <TextView
android:id="@+id/serviceTitle" android:id="@+id/serviceTitle"

View File

@@ -8,7 +8,7 @@
android:paddingLeft="@dimen/activity_horizontal_margin" android:paddingLeft="@dimen/activity_horizontal_margin"
android:paddingRight="@dimen/activity_horizontal_margin" android:paddingRight="@dimen/activity_horizontal_margin"
android:paddingTop="@dimen/activity_vertical_margin" android:paddingTop="@dimen/activity_vertical_margin"
tools:context="de.rochefort.childmonitor.StartActivity" > tools:context="com.example.childmonitor_multiple.StartActivity" >
<Button <Button
android:id="@+id/useChildDevice" android:id="@+id/useChildDevice"

View File

@@ -1,7 +1,7 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<resources> <resources>
<string name="app_name">Child Monitor</string> <string name="app_name">Child Monitor Multiple Clients</string>
<string name="action_settings">Einstellungen</string> <string name="action_settings">Einstellungen</string>
<string name="useAsParentDevice">Als Eltern-Gerät benutzen</string> <string name="useAsParentDevice">Als Eltern-Gerät benutzen</string>
<string name="useAsChildDevice">Als Kind-Gerät benutzen</string> <string name="useAsChildDevice">Als Kind-Gerät benutzen</string>

View File

@@ -1,7 +1,7 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<resources> <resources>
<string name="app_name">ChildMonitor</string> <string name="app_name">Child Monitor Multiple Clients</string>
<string name="action_settings">Instellingen</string> <string name="action_settings">Instellingen</string>
<string name="useAsParentDevice">Gebruiken als ouderapparaat</string> <string name="useAsParentDevice">Gebruiken als ouderapparaat</string>
<string name="useAsChildDevice">Gebruiken als kindapparaat</string> <string name="useAsChildDevice">Gebruiken als kindapparaat</string>

View File

@@ -1,7 +1,7 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<resources> <resources>
<string name="app_name">Child Monitor</string> <string name="app_name">Child Monitor Multiple Clients</string>
<string name="action_settings">Settings</string> <string name="action_settings">Settings</string>
<string name="useAsParentDevice">Use as Parent Device</string> <string name="useAsParentDevice">Use as Parent Device</string>
<string name="useAsChildDevice">Use as Child Device</string> <string name="useAsChildDevice">Use as Child Device</string>

View File

@@ -3,5 +3,4 @@
<p>Dieses Projekt ist ein Fork von Protect Baby Monitor, welches von seinem Entwickler als "on hiatus" (pausiert) gekennzeichnet wurde.</p> <p>Dieses Projekt ist ein Fork von Protect Baby Monitor, welches von seinem Entwickler als "on hiatus" (pausiert) gekennzeichnet wurde.</p>
<p>Child Monitor funktioniert auf Android 5.0 (Lollipop) und neuer, d.h. ab Android SDK 21.</p> <p>Child Monitor funktioniert auf Android 5.0 (Lollipop) und neuer, d.h. ab Android SDK 21.</p>
<p>Achtung: Diese Version von Child Monitor ist inkompatibel zu älteren Versionen als 1.0!</p> <p>Achtung: Diese Version von Child Monitor ist inkompatibel zu älteren Versionen als 1.0!</p>
<p>Fehlerbehebung in dieser Version: Das Elterngerät erkennt jetzt, wenn das Kindgerät keine Daten mehr schickt <p>Fehlerbehebung in dieser Version: Falsche Berechnung des Hintergrundfarbe des Lautstärkemonitors.</p>
(z.B. aufgrund von Netzwerkproblemen), trennt die Verbindung und spielt den Warnton ab.</p>

View File

@@ -3,5 +3,4 @@
<p>The project is a fork of Protect Baby Monitor which is declared as "on hiatus" by its developer.</p> <p>The project is a fork of Protect Baby Monitor which is declared as "on hiatus" by its developer.</p>
<p>Child Monitor works on Android 5.0 (Lollipop) and newer, i.e. Android SDK 21.</p> <p>Child Monitor works on Android 5.0 (Lollipop) and newer, i.e. Android SDK 21.</p>
<p>Warning: This version of Child Monitor is incompatible with versions older than 1.0!</p> <p>Warning: This version of Child Monitor is incompatible with versions older than 1.0!</p>
<p>Bugfix in this release: The parent device detects when the child device no longer sends data (e.g. <p>Bugfix in this release: Wrong color scaling of volume monitor background.</p>
because of network problems), disconnects and plays the notfication sound.</p>