Compare commits
10 Commits
ebe395fc8f
...
130906a6b0
| Author | SHA1 | Date | |
|---|---|---|---|
| 130906a6b0 | |||
| 62f44a1654 | |||
| cb7e9ac8ac | |||
| adafa6279a | |||
| 46cf690ba3 | |||
|
|
a08967059b | ||
|
|
06d793fe16 | ||
|
|
6baa9eb203 | ||
|
|
79165dfe5e | ||
|
|
01f357a985 |
10
.tx/config
10
.tx/config
@@ -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
|
|
||||||
|
|
||||||
@@ -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.
|
||||||
|
|||||||
@@ -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.
|
||||||
|
|
||||||
|
|||||||
@@ -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'
|
||||||
|
|||||||
@@ -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
|
||||||
@@ -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
|
||||||
@@ -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()
|
||||||
}
|
}
|
||||||
@@ -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,27 +88,29 @@ 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
|
|
||||||
|
inner class ListenBinder : Binder() {
|
||||||
|
val service: ListenService
|
||||||
|
get() = this@ListenService
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun buildNotification(name: String?): Notification {
|
private fun buildNotification(name: String?, status: String = getString(R.string.listening)): Notification {
|
||||||
// In this sample, we'll use the same text for the ticker and the expanded notification
|
// In this sample, we'll use the same text for the ticker and the expanded notification
|
||||||
val text = getText(R.string.listening)
|
val text = status
|
||||||
|
val contentIntent = PendingIntent.getActivity(
|
||||||
// The PendingIntent to launch our activity if the user selects this notification
|
this, 0,
|
||||||
val contentIntent = PendingIntent.getActivity(this, 0,
|
Intent(this, ListenActivity::class.java),
|
||||||
Intent(this, ListenActivity::class.java), PendingIntent.FLAG_IMMUTABLE)
|
PendingIntent.FLAG_IMMUTABLE
|
||||||
|
)
|
||||||
// Set the info for the views that show in the notification panel.
|
return NotificationCompat.Builder(this, CHANNEL_ID)
|
||||||
val b = NotificationCompat.Builder(this, CHANNEL_ID)
|
.setSmallIcon(R.drawable.listening_notification)
|
||||||
b.setSmallIcon(R.drawable.listening_notification) // the status icon
|
|
||||||
.setOngoing(true)
|
.setOngoing(true)
|
||||||
.setTicker(text) // the status text
|
.setTicker(text)
|
||||||
.setContentTitle(text) // the label of the entry
|
.setContentTitle(text)
|
||||||
.setContentText(name) // the contents of the entry
|
.setContentText(name)
|
||||||
.setContentIntent(contentIntent)
|
.setContentIntent(contentIntent)
|
||||||
return b.build()
|
.build()
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun createNotificationChannel() {
|
private fun createNotificationChannel() {
|
||||||
@@ -116,63 +124,77 @@ class ListenService : Service() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
inner class ListenBinder : Binder() {
|
/** New logic: automatically retry connection up to 3 times **/
|
||||||
val service: ListenService
|
private fun doListenWithRetries(address: String?, port: Int) {
|
||||||
get() = this@ListenService
|
|
||||||
}
|
|
||||||
|
|
||||||
var onError: (() -> Unit)? = null
|
|
||||||
var onUpdate: (() -> Unit)? = null
|
|
||||||
private fun doListen(address: String?, port: Int) {
|
|
||||||
val lt = Thread {
|
val lt = Thread {
|
||||||
|
var attempts = 0
|
||||||
|
var connected = false
|
||||||
|
|
||||||
|
while (attempts < 3 && !connected && !Thread.currentThread().isInterrupted) {
|
||||||
try {
|
try {
|
||||||
|
Log.i(TAG, "Connection attempt ${attempts + 1} to $address:$port ...")
|
||||||
val socket = Socket(address, port)
|
val socket = Socket(address, port)
|
||||||
socket.soTimeout = 30_000
|
socket.soTimeout = 30_000
|
||||||
val success = streamAudio(socket)
|
connected = streamAudio(socket)
|
||||||
if (!success) {
|
if (!connected) {
|
||||||
playAlert()
|
Log.w(TAG, "Streaming failed, attempt ${attempts + 1}")
|
||||||
onError?.invoke()
|
attempts++
|
||||||
|
Thread.sleep(2000)
|
||||||
}
|
}
|
||||||
} catch (e: IOException) {
|
} catch (e: IOException) {
|
||||||
Log.e(TAG, "Error opening socket to $address on port $port", e)
|
attempts++
|
||||||
|
Log.e(TAG, "Error while connecting (attempt $attempts of 3)", e)
|
||||||
|
if (attempts < 3) Thread.sleep(2000)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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 {
|
||||||
|
try {
|
||||||
audioTrack.stop()
|
audioTrack.stop()
|
||||||
|
audioTrack.release()
|
||||||
socket.close()
|
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")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -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 {
|
||||||
@@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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
|
||||||
@@ -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
|
||||||
@@ -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
|
||||||
@@ -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.
|
||||||
@@ -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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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"
|
||||||
|
|||||||
@@ -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,6 +67,25 @@
|
|||||||
android:inputType="number"
|
android:inputType="number"
|
||||||
android:maxLength="5"
|
android:maxLength="5"
|
||||||
android:hint="@string/examplePort"
|
android:hint="@string/examplePort"
|
||||||
|
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" />
|
android:nextFocusForward="@+id/connectViaAddressButton" />
|
||||||
|
|
||||||
<Space
|
<Space
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|||||||
@@ -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" />
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
|
||||||
|
|||||||
@@ -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>
|
|
||||||
|
|||||||
Reference in New Issue
Block a user