Compare commits
12 Commits
ebe395fc8f
...
v1.5
| Author | SHA1 | Date | |
|---|---|---|---|
| 3073f03323 | |||
| 453e265963 | |||
| 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.
|
||||
Changes need to apply cleanly onto the master branch and pass all
|
||||
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
|
||||
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
|
||||
_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
|
||||
|
||||
defaultConfig {
|
||||
applicationId "de.rochefort.childmonitor"
|
||||
applicationId "com.example.childmonitor_multiple"
|
||||
minSdkVersion 21
|
||||
targetSdkVersion 34
|
||||
versionCode 13
|
||||
versionName "1.3"
|
||||
versionCode 14
|
||||
versionName "1.5"
|
||||
}
|
||||
|
||||
buildTypes {
|
||||
@@ -21,7 +21,7 @@ android {
|
||||
|
||||
dependencies {
|
||||
}
|
||||
namespace 'de.rochefort.childmonitor'
|
||||
namespace 'com.example.childmonitor_multiple'
|
||||
lint {
|
||||
abortOnError true
|
||||
warning 'MissingTranslation'
|
||||
|
||||
@@ -14,10 +14,9 @@
|
||||
* 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
|
||||
package com.example.childmonitor_multiple
|
||||
|
||||
import android.media.AudioFormat
|
||||
import de.rochefort.childmonitor.audio.G711UCodec
|
||||
|
||||
object AudioCodecDefines {
|
||||
const val FREQUENCY = 8000
|
||||
@@ -14,7 +14,7 @@
|
||||
* 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
|
||||
package com.example.childmonitor_multiple
|
||||
|
||||
import android.app.Activity
|
||||
import android.content.Intent
|
||||
@@ -1,20 +1,4 @@
|
||||
/*
|
||||
* 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
|
||||
package com.example.childmonitor_multiple
|
||||
|
||||
import android.app.Activity
|
||||
import android.content.ComponentName
|
||||
@@ -28,12 +12,14 @@ import android.util.Log
|
||||
import android.widget.TextView
|
||||
import android.widget.Toast
|
||||
import androidx.core.content.ContextCompat
|
||||
import de.rochefort.childmonitor.ListenService.ListenBinder
|
||||
import com.example.childmonitor_multiple.ListenService.ListenBinder
|
||||
|
||||
class ListenActivity : Activity() {
|
||||
// Don't attempt to unbind from the service unless the client has received some
|
||||
// information about the service's state.
|
||||
private var shouldUnbind = false
|
||||
private lateinit var statusText: TextView
|
||||
|
||||
private val connection: ServiceConnection = object : ServiceConnection {
|
||||
override fun onServiceConnected(className: ComponentName, service: IBinder) {
|
||||
// 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
|
||||
// cast its IBinder to a concrete class and directly access it.
|
||||
val bs = (service as ListenBinder).service
|
||||
Toast.makeText(this@ListenActivity, R.string.connect,
|
||||
Toast.LENGTH_SHORT).show()
|
||||
Toast.makeText(this@ListenActivity, R.string.connect, Toast.LENGTH_SHORT).show()
|
||||
|
||||
val connectedText = findViewById<TextView>(R.id.connectedTo)
|
||||
connectedText.text = bs.childDeviceName
|
||||
|
||||
val volumeView = findViewById<VolumeView>(R.id.volume)
|
||||
volumeView.volumeHistory = bs.volumeHistory
|
||||
bs.onUpdate = { volumeView.postInvalidate() }
|
||||
bs.onError = { postErrorMessage() }
|
||||
|
||||
statusText.text = "Connected and listening..."
|
||||
}
|
||||
|
||||
override fun onServiceDisconnected(className: ComponentName) {
|
||||
@@ -57,8 +46,8 @@ class ListenActivity : Activity() {
|
||||
// unexpectedly disconnected -- that is, its process crashed.
|
||||
// Because it is running in our same process, we should never
|
||||
// see this happen.
|
||||
Toast.makeText(this@ListenActivity, R.string.disconnected,
|
||||
Toast.LENGTH_SHORT).show()
|
||||
Toast.makeText(this@ListenActivity, R.string.disconnected, Toast.LENGTH_SHORT).show()
|
||||
statusText.text = "Disconnected"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -77,41 +66,38 @@ class ListenActivity : Activity() {
|
||||
// (and thus won't be supporting component replacement by other
|
||||
// applications).
|
||||
if (bindService(intent, connection, BIND_AUTO_CREATE)) {
|
||||
this.shouldUnbind = true
|
||||
Log.i(TAG, "Bound listen service")
|
||||
shouldUnbind = true
|
||||
Log.i(TAG, "Bound to ListenService")
|
||||
} else {
|
||||
Log.e(TAG, "Error: The requested service doesn't " +
|
||||
"exist, or this client isn't allowed access to it.")
|
||||
Log.e(TAG, "Error: Could not bind to ListenService.")
|
||||
statusText.text = "Failed to bind service."
|
||||
}
|
||||
}
|
||||
|
||||
private fun doUnbindAndStopService() {
|
||||
if (this.shouldUnbind) {
|
||||
// Release information about the service's state.
|
||||
if (shouldUnbind) {
|
||||
unbindService(connection)
|
||||
this.shouldUnbind = false
|
||||
shouldUnbind = false
|
||||
}
|
||||
val context: Context = this
|
||||
val intent = Intent(context, ListenService::class.java)
|
||||
context.stopService(intent)
|
||||
stopService(Intent(this, ListenService::class.java))
|
||||
}
|
||||
|
||||
fun postErrorMessage() {
|
||||
val status = findViewById<TextView>(R.id.textStatus)
|
||||
status.post { status.setText(R.string.disconnected) }
|
||||
statusText.post {
|
||||
statusText.text = "Connection failed after 3 attempts."
|
||||
}
|
||||
}
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
val bundle = this.intent.extras
|
||||
ensureServiceRunningAndBind(bundle)
|
||||
this.volumeControlStream = AudioManager.STREAM_MUSIC
|
||||
setContentView(R.layout.activity_listen)
|
||||
val statusText = findViewById<TextView>(R.id.textStatus)
|
||||
statusText.setText(R.string.listening)
|
||||
statusText = findViewById(R.id.textStatus)
|
||||
statusText.text = "Attempting to connect..."
|
||||
volumeControlStream = AudioManager.STREAM_MUSIC
|
||||
ensureServiceRunningAndBind(intent.extras)
|
||||
}
|
||||
|
||||
public override fun onDestroy() {
|
||||
override fun onDestroy() {
|
||||
doUnbindAndStopService()
|
||||
super.onDestroy()
|
||||
}
|
||||
@@ -14,7 +14,7 @@
|
||||
* 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
|
||||
package com.example.childmonitor_multiple
|
||||
|
||||
import android.app.Notification
|
||||
import android.app.NotificationChannel
|
||||
@@ -49,6 +49,9 @@ class ListenService : Service() {
|
||||
var childDeviceName: String? = null
|
||||
private set
|
||||
|
||||
var onError: (() -> Unit)? = null
|
||||
var onUpdate: (() -> Unit)? = null
|
||||
|
||||
override fun onCreate() {
|
||||
super.onCreate()
|
||||
this.notificationManager = getSystemService(NOTIFICATION_SERVICE) as NotificationManager
|
||||
@@ -62,11 +65,14 @@ class ListenService : Service() {
|
||||
val name = it.getString("name")
|
||||
childDeviceName = 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)
|
||||
|
||||
val address = it.getString("address")
|
||||
val port = it.getInt("port")
|
||||
doListen(address, port)
|
||||
doListenWithRetries(address, port)
|
||||
}
|
||||
return START_REDELIVER_INTENT
|
||||
}
|
||||
@@ -82,27 +88,29 @@ class ListenService : Service() {
|
||||
Toast.makeText(this, R.string.stopped, Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
|
||||
override fun onBind(intent: Intent): IBinder {
|
||||
return binder
|
||||
override fun onBind(intent: Intent): IBinder = 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
|
||||
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
|
||||
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) // the status text
|
||||
.setContentTitle(text) // the label of the entry
|
||||
.setContentText(name) // the contents of the entry
|
||||
.setTicker(text)
|
||||
.setContentTitle(text)
|
||||
.setContentText(name)
|
||||
.setContentIntent(contentIntent)
|
||||
return b.build()
|
||||
.build()
|
||||
}
|
||||
|
||||
private fun createNotificationChannel() {
|
||||
@@ -116,63 +124,77 @@ class ListenService : Service() {
|
||||
}
|
||||
}
|
||||
|
||||
inner class ListenBinder : Binder() {
|
||||
val service: ListenService
|
||||
get() = this@ListenService
|
||||
}
|
||||
|
||||
var onError: (() -> Unit)? = null
|
||||
var onUpdate: (() -> Unit)? = null
|
||||
private fun doListen(address: String?, port: Int) {
|
||||
/** New logic: automatically retry connection up to 3 times **/
|
||||
private fun doListenWithRetries(address: String?, port: Int) {
|
||||
val lt = Thread {
|
||||
var attempts = 0
|
||||
var connected = false
|
||||
|
||||
while (attempts < 3 && !connected && !Thread.currentThread().isInterrupted) {
|
||||
try {
|
||||
Log.i(TAG, "Connection attempt ${attempts + 1} to $address:$port ...")
|
||||
val socket = Socket(address, port)
|
||||
socket.soTimeout = 30_000
|
||||
val success = streamAudio(socket)
|
||||
if (!success) {
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
if (!connected) {
|
||||
Log.e(TAG, "Failed to connect after 3 attempts.")
|
||||
playAlert()
|
||||
updateNotification(getString(R.string.disconnected))
|
||||
onError?.invoke()
|
||||
}
|
||||
} catch (e : IOException) {
|
||||
Log.e(TAG, "Error opening socket to $address on port $port", e)
|
||||
} else {
|
||||
Log.i(TAG, "Connection established successfully.")
|
||||
}
|
||||
}
|
||||
|
||||
this.listenThread = lt
|
||||
lt.start()
|
||||
}
|
||||
|
||||
private fun streamAudio(socket: Socket): Boolean {
|
||||
Log.i(TAG, "Setting up stream")
|
||||
val audioTrack = AudioTrack(AudioManager.STREAM_MUSIC,
|
||||
Log.i(TAG, "Starting audio stream")
|
||||
val audioTrack = AudioTrack(
|
||||
AudioManager.STREAM_MUSIC,
|
||||
frequency,
|
||||
channelConfiguration,
|
||||
audioEncoding,
|
||||
bufferSize,
|
||||
AudioTrack.MODE_STREAM)
|
||||
AudioTrack.MODE_STREAM
|
||||
)
|
||||
|
||||
try {
|
||||
audioTrack.play()
|
||||
} catch (e : java.lang.IllegalStateException) {
|
||||
Log.e(TAG, "Failed to play streamed audio audio for other reason", e)
|
||||
} catch (e: IllegalStateException) {
|
||||
Log.e(TAG, "Failed to start AudioTrack", e)
|
||||
return false
|
||||
}
|
||||
|
||||
val inputStream = try {
|
||||
socket.getInputStream()
|
||||
} 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
|
||||
}
|
||||
|
||||
val readBuffer = ByteArray(byteBufferSize)
|
||||
val decodedBuffer = ShortArray(byteBufferSize * 2)
|
||||
try {
|
||||
|
||||
return try {
|
||||
while (!Thread.currentThread().isInterrupted) {
|
||||
val len = inputStream.read(readBuffer)
|
||||
if (len < 0) {
|
||||
// If the current thread was not interrupted this means the remote stopped streaming
|
||||
return Thread.currentThread().isInterrupted
|
||||
}
|
||||
val decoded: Int = AudioCodecDefines.CODEC.decode(decodedBuffer, readBuffer, len, 0)
|
||||
if (len < 0) return false
|
||||
val decoded = AudioCodecDefines.CODEC.decode(decodedBuffer, readBuffer, len, 0)
|
||||
if (decoded > 0) {
|
||||
audioTrack.write(decodedBuffer, 0, decoded)
|
||||
val decodedBytes = ShortArray(decoded)
|
||||
@@ -181,24 +203,35 @@ class ListenService : Service() {
|
||||
onUpdate?.invoke()
|
||||
}
|
||||
}
|
||||
return true
|
||||
true
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "Connection failed", e)
|
||||
return false
|
||||
Log.e(TAG, "Connection lost during streaming", e)
|
||||
updateNotification(getString(R.string.disconnected))
|
||||
playAlert()
|
||||
onError?.invoke()
|
||||
false
|
||||
} finally {
|
||||
try {
|
||||
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() {
|
||||
val mp = MediaPlayer.create(this, R.raw.upward_beep_chromatic_fifths)
|
||||
if (mp != null) {
|
||||
Log.i(TAG, "Playing alert")
|
||||
mp.setOnCompletionListener { obj: MediaPlayer -> obj.release() }
|
||||
Log.i(TAG, "Playing alert sound")
|
||||
mp.setOnCompletionListener { it.release() }
|
||||
mp.start()
|
||||
} 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
|
||||
* 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.content.ComponentName
|
||||
@@ -28,7 +28,7 @@ import android.util.Log
|
||||
import android.widget.TextView
|
||||
import android.widget.Toast
|
||||
import androidx.core.content.ContextCompat
|
||||
import de.rochefort.childmonitor.MonitorService.MonitorBinder
|
||||
import com.example.childmonitor_multiple.MonitorService.MonitorBinder
|
||||
|
||||
class MonitorActivity : Activity() {
|
||||
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
|
||||
* 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.app.Activity
|
||||
@@ -14,7 +14,7 @@
|
||||
* 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
|
||||
package com.example.childmonitor_multiple
|
||||
|
||||
import android.os.Handler
|
||||
import android.os.Looper
|
||||
@@ -14,7 +14,7 @@
|
||||
* 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
|
||||
package com.example.childmonitor_multiple
|
||||
|
||||
import android.content.Context
|
||||
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
|
||||
* 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.
|
||||
@@ -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:paddingRight="@dimen/activity_horizontal_margin"
|
||||
android:paddingTop="@dimen/activity_vertical_margin"
|
||||
tools:context="de.rochefort.childmonitor.MonitorActivity" >
|
||||
tools:context="com.example.childmonitor_multiple.MonitorActivity" >
|
||||
|
||||
<TextView
|
||||
android:id="@+id/parentDeviceTitle"
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
android:paddingLeft="@dimen/activity_horizontal_margin"
|
||||
android:paddingRight="@dimen/activity_horizontal_margin"
|
||||
android:paddingTop="@dimen/activity_vertical_margin"
|
||||
tools:context="de.rochefort.childmonitor.MonitorActivity" >
|
||||
tools:context="com.example.childmonitor_multiple.MonitorActivity">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/parentDeviceTitle"
|
||||
@@ -67,7 +67,26 @@
|
||||
android:inputType="number"
|
||||
android:maxLength="5"
|
||||
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
|
||||
android:layout_width="match_parent"
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
android:paddingLeft="@dimen/activity_horizontal_margin"
|
||||
android:paddingRight="@dimen/activity_horizontal_margin"
|
||||
android:paddingTop="@dimen/activity_vertical_margin"
|
||||
tools:context="de.rochefort.childmonitor.MonitorActivity" >
|
||||
tools:context="com.example.childmonitor_multiple.MonitorActivity" >
|
||||
|
||||
<TextView
|
||||
android:id="@+id/parentDeviceTitle"
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
android:paddingTop="@dimen/activity_vertical_margin"
|
||||
android:paddingRight="@dimen/activity_horizontal_margin"
|
||||
android:paddingBottom="@dimen/activity_vertical_margin"
|
||||
tools:context="de.rochefort.childmonitor.MonitorActivity">
|
||||
tools:context="com.example.childmonitor_multiple.MonitorActivity">
|
||||
|
||||
<TextView
|
||||
android:id="@id/parentDeviceTitle"
|
||||
@@ -59,7 +59,7 @@
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="15dip" />
|
||||
|
||||
<de.rochefort.childmonitor.VolumeView
|
||||
<com.example.childmonitor_multiple.VolumeView
|
||||
android:id="@+id/volume"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent" />
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
android:paddingRight="@dimen/activity_horizontal_margin"
|
||||
android:paddingTop="@dimen/activity_vertical_margin"
|
||||
android:keepScreenOn="true"
|
||||
tools:context="de.rochefort.childmonitor.MonitorActivity" >
|
||||
tools:context="com.example.childmonitor_multiple.MonitorActivity" >
|
||||
|
||||
<TextView
|
||||
android:id="@+id/childDeviceTitle"
|
||||
@@ -34,7 +34,7 @@
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical"
|
||||
android:keepScreenOn="true"
|
||||
tools:context="de.rochefort.childmonitor.MonitorActivity" >
|
||||
tools:context="com.example.childmonitor_multiple.MonitorActivity" >
|
||||
|
||||
<TextView
|
||||
android:id="@+id/serviceTitle"
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
android:paddingLeft="@dimen/activity_horizontal_margin"
|
||||
android:paddingRight="@dimen/activity_horizontal_margin"
|
||||
android:paddingTop="@dimen/activity_vertical_margin"
|
||||
tools:context="de.rochefort.childmonitor.StartActivity" >
|
||||
tools:context="com.example.childmonitor_multiple.StartActivity" >
|
||||
|
||||
<Button
|
||||
android:id="@+id/useChildDevice"
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<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="useAsParentDevice">Als Eltern-Gerät benutzen</string>
|
||||
<string name="useAsChildDevice">Als Kind-Gerät benutzen</string>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
|
||||
<string name="app_name">ChildMonitor</string>
|
||||
<string name="app_name">Child Monitor Multiple Clients</string>
|
||||
<string name="action_settings">Instellingen</string>
|
||||
<string name="useAsParentDevice">Gebruiken als ouderapparaat</string>
|
||||
<string name="useAsChildDevice">Gebruiken als kindapparaat</string>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<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="useAsParentDevice">Use as Parent 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>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>Fehlerbehebung in dieser Version: Das Elterngerät erkennt jetzt, wenn das Kindgerät keine Daten mehr schickt
|
||||
(z.B. aufgrund von Netzwerkproblemen), trennt die Verbindung und spielt den Warnton ab.</p>
|
||||
<p>Fehlerbehebung in dieser Version: Falsche Berechnung des Hintergrundfarbe des Lautstärkemonitors.</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>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>Bugfix in this release: The parent device detects when the child device no longer sends data (e.g.
|
||||
because of network problems), disconnects and plays the notfication sound.</p>
|
||||
<p>Bugfix in this release: Wrong color scaling of volume monitor background.</p>
|
||||
|
||||
Reference in New Issue
Block a user