diff --git a/app/src/main/kotlin/com/example/childmonitor_multiple/ListenActivity.kt b/app/src/main/kotlin/com/example/childmonitor_multiple/ListenActivity.kt index 73b2f1e..c55a54d 100644 --- a/app/src/main/kotlin/com/example/childmonitor_multiple/ListenActivity.kt +++ b/app/src/main/kotlin/com/example/childmonitor_multiple/ListenActivity.kt @@ -1,19 +1,3 @@ -/* - * This file is part of Child Monitor. - * - * Child Monitor is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * Child Monitor is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with Child Monitor. If not, see . - */ package com.example.childmonitor_multiple import android.app.Activity @@ -34,6 +18,8 @@ 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,10 +28,11 @@ 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(R.id.connectedTo) connectedText.text = bs.childDeviceName + val volumeView = findViewById(R.id.volume) volumeView.volumeHistory = bs.volumeHistory bs.onUpdate = { volumeView.postInvalidate() } @@ -77,7 +64,7 @@ class ListenActivity : Activity() { // (and thus won't be supporting component replacement by other // applications). if (bindService(intent, connection, BIND_AUTO_CREATE)) { - this.shouldUnbind = true + shouldUnbind = true Log.i(TAG, "Bound listen service") } else { Log.e(TAG, "Error: The requested service doesn't " + @@ -86,32 +73,29 @@ class ListenActivity : Activity() { } 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(R.id.textStatus) - status.post { status.setText(R.string.disconnected) } + statusText.post { + statusText.text = "Verbindung getrennt (3 Fehlversuche)" + } } 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(R.id.textStatus) - statusText.setText(R.string.listening) + statusText = findViewById(R.id.textStatus) + statusText.text = "Starte Verbindung…" + volumeControlStream = AudioManager.STREAM_MUSIC + ensureServiceRunningAndBind(intent.extras) } - public override fun onDestroy() { + override fun onDestroy() { doUnbindAndStopService() super.onDestroy() } diff --git a/app/src/main/kotlin/com/example/childmonitor_multiple/ListenService.kt b/app/src/main/kotlin/com/example/childmonitor_multiple/ListenService.kt index 9e5bc38..1c90bab 100644 --- a/app/src/main/kotlin/com/example/childmonitor_multiple/ListenService.kt +++ b/app/src/main/kotlin/com/example/childmonitor_multiple/ListenService.kt @@ -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,12 @@ 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,61 +86,76 @@ class ListenService : Service() { Toast.makeText(this, R.string.stopped, Toast.LENGTH_SHORT).show() } - override fun onBind(intent: Intent): IBinder { - return binder - } - - private fun buildNotification(name: String?): Notification { - // In this sample, we'll use the same text for the ticker and the expanded notification - val text = getText(R.string.listening) - - // The PendingIntent to launch our activity if the user selects this notification - val contentIntent = PendingIntent.getActivity(this, 0, - Intent(this, ListenActivity::class.java), PendingIntent.FLAG_IMMUTABLE) - - // Set the info for the views that show in the notification panel. - val b = NotificationCompat.Builder(this, CHANNEL_ID) - b.setSmallIcon(R.drawable.listening_notification) // the status icon - .setOngoing(true) - .setTicker(text) // the status text - .setContentTitle(text) // the label of the entry - .setContentText(name) // the contents of the entry - .setContentIntent(contentIntent) - return b.build() - } - - private fun createNotificationChannel() { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { - val serviceChannel = NotificationChannel( - CHANNEL_ID, - "Foreground Service Channel", - NotificationManager.IMPORTANCE_DEFAULT - ) - notificationManager.createNotificationChannel(serviceChannel) - } - } + override fun onBind(intent: Intent): IBinder = binder 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) { + private fun buildNotification(name: String?): Notification { + // In this sample, we'll use the same text for the ticker and the expanded notification + val text = getText(R.string.listening) + val contentIntent = PendingIntent.getActivity( + this, 0, + Intent(this, ListenActivity::class.java), + PendingIntent.FLAG_IMMUTABLE + ) + return NotificationCompat.Builder(this, CHANNEL_ID) + .setSmallIcon(R.drawable.listening_notification) + .setOngoing(true) + .setTicker(text) + .setContentTitle(text) + .setContentText(name) + .setContentIntent(contentIntent) + .build() + } + + private fun createNotificationChannel() { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + val serviceChannel = NotificationChannel( + CHANNEL_ID, + "Foreground Service Channel", + NotificationManager.IMPORTANCE_DEFAULT + ) + notificationManager.createNotificationChannel(serviceChannel) + } + } + + /** Neuer Wrapper mit 3 automatischen Reconnect-Versuchen **/ + private fun doListenWithRetries(address: String?, port: Int) { val lt = Thread { - try { - val socket = Socket(address, port) - socket.soTimeout = 30_000 - val success = streamAudio(socket) - if (!success) { - playAlert() - onError?.invoke() + var attempts = 0 + var connected = false + + while (attempts < 3 && !connected && !Thread.currentThread().isInterrupted) { + try { + Log.i(TAG, "Verbindungsversuch ${attempts + 1} zu $address:$port …") + val socket = Socket(address, port) + socket.soTimeout = 30_000 + connected = streamAudio(socket) + if (!connected) { + Log.w(TAG, "Streaming fehlgeschlagen, Versuch ${attempts + 1}") + playAlert() + attempts++ + Thread.sleep(2000) + } + } catch (e: IOException) { + attempts++ + Log.e(TAG, "Fehler beim Verbindungsaufbau (Versuch $attempts von 3)", e) + if (attempts < 3) Thread.sleep(2000) } - } catch (e : IOException) { - Log.e(TAG, "Error opening socket to $address on port $port", e) + } + + if (!connected) { + Log.e(TAG, "Nach 3 Versuchen keine Verbindung möglich.") + playAlert() + onError?.invoke() + } else { + Log.i(TAG, "Verbindung erfolgreich aufgebaut.") } } + this.listenThread = lt lt.start() } @@ -148,10 +167,12 @@ class ListenService : Service() { channelConfiguration, audioEncoding, bufferSize, - AudioTrack.MODE_STREAM) + AudioTrack.MODE_STREAM + ) + try { audioTrack.play() - } catch (e : java.lang.IllegalStateException) { + } catch (e: IllegalStateException) { Log.e(TAG, "Failed to play streamed audio audio for other reason", e) return false } @@ -165,14 +186,12 @@ class ListenService : Service() { 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,13 +200,16 @@ class ListenService : Service() { onUpdate?.invoke() } } - return true + true } catch (e: Exception) { - Log.e(TAG, "Connection failed", e) - return false + Log.e(TAG, "Verbindungsfehler im Stream", e) + false } finally { - audioTrack.stop() - socket.close() + try { + audioTrack.stop() + audioTrack.release() + socket.close() + } catch (_: Exception) {} } } @@ -195,7 +217,7 @@ class ListenService : Service() { 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() } + mp.setOnCompletionListener { it.release() } mp.start() } else { Log.e(TAG, "Failed to play alert")