package de.duffy.screencast import android.Manifest import android.content.Context import android.content.Intent import android.content.pm.PackageManager import android.media.projection.MediaProjectionManager import android.os.Build import android.os.Bundle import android.provider.Settings import android.view.View import android.widget.Toast import androidx.activity.result.contract.ActivityResultContracts import androidx.appcompat.app.AppCompatActivity import androidx.core.content.ContextCompat import com.google.android.material.button.MaterialButton import de.duffy.screencast.connect.SystemDisplayRoutes import de.duffy.screencast.connect.WifiDirectController import de.duffy.screencast.databinding.ActivityDeviceBinding import java.net.InetAddress /** * Sucht Fernseher, die auf eine Verbindung warten, und stellt sie her - ohne * dass am Fernseher irgendetwas eingetippt werden muss. * * Zwei Wege stehen offen, die Liste mischt beide: * - Der Übertragungsdienst, den Android selbst mitbringt. Vielen Geräten * fehlt nur die Bedienkachel, nicht die Technik. Klappt das, spiegelt das * System selbst - beste Qualität. * - Wi-Fi Direct: die App baut die Verbindung selbst auf und schickt das Bild * im Miracast-Format hinüber. */ class DeviceActivity : AppCompatActivity() { private lateinit var binding: ActivityDeviceBinding private lateinit var routes: SystemDisplayRoutes private lateinit var direct: WifiDirectController private var pendingPeer: WifiDirectController.Peer? = null private var pendingPeerPort = 7236 private var sinkAddress: InetAddress? = null private val projectionRequest = registerForActivityResult( ActivityResultContracts.StartActivityForResult() ) { result -> val peer = pendingPeer if (result.resultCode != RESULT_OK || result.data == null || peer == null) { log("Ohne Freigabe der Bildschirmaufnahme geht es nicht weiter") pendingPeer = null return@registerForActivityResult } // Erst den Dienst hochfahren: Er hoert ab sofort auf den Fernseher. // Der meldet sich naemlich von sich aus, sobald die Direktverbindung // steht - und zwar sofort, nicht erst wenn wir bereit sind. val intent = Intent(this, CastService::class.java).apply { action = CastService.ACTION_START putExtra(CastService.EXTRA_MODE, CastService.MODE_MIRACAST) putExtra(CastService.EXTRA_RESULT_CODE, result.resultCode) putExtra(CastService.EXTRA_RESULT_DATA, result.data) putExtra(CastService.EXTRA_SINK_PORT, peer.rtspPort) putExtra(CastService.EXTRA_QUALITY, CastService.Quality.HD.ordinal) } ContextCompat.startForegroundService(this, intent) pendingPeerPort = peer.rtspPort log("Baue Direktverbindung zu ${peer.name} auf. Am Fernseher ggf. bestätigen.") direct.connect(peer) } private val permissionRequest = registerForActivityResult( ActivityResultContracts.RequestMultiplePermissions() ) { granted -> if (granted.values.any { it }) startSearch() else log("Ohne die Berechtigung findet Android keine Geräte in der Nähe") } override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) binding = ActivityDeviceBinding.inflate(layoutInflater) setContentView(binding.root) supportActionBar?.setDisplayHomeAsUpEnabled(true) routes = SystemDisplayRoutes(this) direct = WifiDirectController(this) routes.onRoutesChanged = { render() } direct.onPeersChanged = { peers -> discoveredPeers = peers; render() } direct.onLog = { log(it) } direct.onConnected = { address, isOwner -> onDirectConnected(address, isOwner) } binding.buttonSearch.setOnClickListener { requestPermissionsAndSearch() } binding.buttonSystemSettings.setOnClickListener { openSystemCastSettings() } binding.buttonDisconnect.setOnClickListener { routes.disconnect() direct.disconnect() startService(Intent(this, CastService::class.java).setAction(CastService.ACTION_STOP)) log("Verbindung getrennt") } CastState.log.observe(this) { lines -> binding.textLog.text = lines.joinToString("\n") binding.scrollLog.post { binding.scrollLog.fullScroll(View.FOCUS_DOWN) } } CastState.clearLog() requestPermissionsAndSearch() } override fun onDestroy() { super.onDestroy() routes.stopScan() direct.stop() } override fun onSupportNavigateUp(): Boolean { finish(); return true } // ------------------------------------------------------------------ Suche private fun requestPermissionsAndSearch() { val needed = ArrayList() if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { if (checkSelfPermission(Manifest.permission.NEARBY_WIFI_DEVICES) != PackageManager.PERMISSION_GRANTED) needed.add(Manifest.permission.NEARBY_WIFI_DEVICES) } else { if (checkSelfPermission(Manifest.permission.ACCESS_FINE_LOCATION) != PackageManager.PERMISSION_GRANTED) needed.add(Manifest.permission.ACCESS_FINE_LOCATION) } if (needed.isEmpty()) startSearch() else permissionRequest.launch(needed.toTypedArray()) } private fun startSearch() { log("Suche nach Fernsehern ...") routes.startScan() if (direct.start()) direct.discover() else log("Dieses Gerät kann kein Wi-Fi Direct") render() binding.root.postDelayed({ render() }, 4000) } // -------------------------------------------------------------- Anzeige private fun render() { val container = binding.deviceList container.removeAllViews() var count = 0 for (route in routes.routes()) { count++ addDevice( title = route.name, subtitle = buildString { append("Über den Übertragungsdienst von Android") if (route.status.isNotEmpty()) append(" · ${route.status}") if (route.connected) append(" · verbunden") }, enabled = route.enabled ) { selectSystemRoute(route) } } for (peer in discoveredPeers) { count++ addDevice( title = peer.name, subtitle = buildString { append("Direktverbindung über Wi-Fi Direct") if (peer.isDisplaySink) append(" · meldet sich als Bildschirm") if (peer.sessionAvailable) append(" · wartet auf Verbindung") }, enabled = true ) { connectDirect(peer) } } binding.textEmpty.visibility = if (count == 0) View.VISIBLE else View.GONE } private var discoveredPeers: List = emptyList() private fun addDevice(title: String, subtitle: String, enabled: Boolean, onClick: () -> Unit) { val button = MaterialButton(this, null, com.google.android.material.R.attr.materialButtonOutlinedStyle) button.text = "$title\n$subtitle" button.isAllCaps = false button.isEnabled = enabled button.setPadding(40, 30, 40, 30) button.setOnClickListener { onClick() } val params = android.widget.LinearLayout.LayoutParams( android.widget.LinearLayout.LayoutParams.MATCH_PARENT, android.widget.LinearLayout.LayoutParams.WRAP_CONTENT ).apply { bottomMargin = 24 } binding.deviceList.addView(button, params) } // ---------------------------------------------------------- Verbindungen private fun selectSystemRoute(route: SystemDisplayRoutes.Route) { log("Verbinde über den Systemdienst mit ${route.name} ...") if (routes.select(route)) { log("Anfrage gestellt. Wenn der Fernseher annimmt, spiegelt Android ab jetzt selbst.") Toast.makeText(this, "Verbindung angefordert", Toast.LENGTH_LONG).show() } else { log("Der Systemdienst hat die Anfrage abgelehnt") } } private fun connectDirect(peer: WifiDirectController.Peer) { pendingPeer = peer log("Bitte die Bildschirmaufnahme freigeben - danach wird verbunden.") val manager = getSystemService(Context.MEDIA_PROJECTION_SERVICE) as MediaProjectionManager projectionRequest.launch(manager.createScreenCaptureIntent()) } private fun onDirectConnected(address: InetAddress, isGroupOwner: Boolean) { if (sinkAddress == address) return // der Broadcast kommt mehrfach sinkAddress = address log("Direktverbindung steht (${address.hostAddress}). Warte auf den Fernseher.") startService(Intent(this, CastService::class.java).apply { action = CastService.ACTION_SINK_READY putExtra(CastService.EXTRA_SINK_ADDRESS, address.hostAddress) putExtra(CastService.EXTRA_SINK_PORT, pendingPeerPort) }) } /** Die versteckte Systemseite fürs Spiegeln - falls das Gerät sie hat. */ private fun openSystemCastSettings() { val candidates = listOf( Settings.ACTION_CAST_SETTINGS, "android.settings.WIFI_DISPLAY_SETTINGS", "com.samsung.settings.WIFI_DISPLAY_SETTINGS" ) for (action in candidates) { try { startActivity(Intent(action).addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)) log("Systemeinstellungen geöffnet ($action)") return } catch (_: Exception) { /* naechsten Kandidaten probieren */ } } log("Dieses Gerät hat keine eingebaute Seite fürs Bildschirmspiegeln") Toast.makeText(this, "Keine Systemseite fürs Spiegeln vorhanden", Toast.LENGTH_LONG).show() } private fun log(line: String) = CastState.appendLog(line) }