mirror of
https://gitlab.com/futo-org/fcast.git
synced 2025-06-24 21:25:23 +00:00
Added support for DPAD left/right to skip and back button to hide controller. Added error message and loading overlay. Added demo mode (tap top-left 5 times).
This commit is contained in:
parent
10afb65456
commit
72bc635dfd
6 changed files with 180 additions and 35 deletions
|
@ -36,7 +36,7 @@ class FCastSession(private val _socket: Socket, private val _service: TcpListene
|
||||||
private var _bytesRead = 0
|
private var _bytesRead = 0
|
||||||
private var _packetLength = 0
|
private var _packetLength = 0
|
||||||
private var _state = SessionState.WaitingForLength
|
private var _state = SessionState.WaitingForLength
|
||||||
private var _outputStream: DataOutputStream? = DataOutputStream(_socket.outputStream);
|
private var _outputStream: DataOutputStream? = DataOutputStream(_socket.outputStream)
|
||||||
|
|
||||||
fun sendPlaybackUpdate(value: PlaybackUpdateMessage) {
|
fun sendPlaybackUpdate(value: PlaybackUpdateMessage) {
|
||||||
send(Opcode.PlaybackUpdate, value)
|
send(Opcode.PlaybackUpdate, value)
|
||||||
|
@ -48,40 +48,40 @@ class FCastSession(private val _socket: Socket, private val _service: TcpListene
|
||||||
|
|
||||||
private inline fun <reified T> send(opcode: Opcode, message: T) {
|
private inline fun <reified T> send(opcode: Opcode, message: T) {
|
||||||
try {
|
try {
|
||||||
val data: ByteArray;
|
val data: ByteArray
|
||||||
var jsonString: String? = null;
|
var jsonString: String? = null
|
||||||
if (message != null) {
|
if (message != null) {
|
||||||
jsonString = Json.encodeToString(message);
|
jsonString = Json.encodeToString(message)
|
||||||
data = jsonString.encodeToByteArray();
|
data = jsonString.encodeToByteArray()
|
||||||
} else {
|
} else {
|
||||||
data = ByteArray(0);
|
data = ByteArray(0)
|
||||||
}
|
}
|
||||||
|
|
||||||
val size = 1 + data.size;
|
val size = 1 + data.size
|
||||||
val outputStream = _outputStream;
|
val outputStream = _outputStream
|
||||||
if (outputStream == null) {
|
if (outputStream == null) {
|
||||||
Log.w(TAG, "Failed to send $size bytes, output stream is null.");
|
Log.w(TAG, "Failed to send $size bytes, output stream is null.")
|
||||||
return;
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
val serializedSizeLE = ByteArray(4);
|
val serializedSizeLE = ByteArray(4)
|
||||||
serializedSizeLE[0] = (size and 0xff).toByte();
|
serializedSizeLE[0] = (size and 0xff).toByte()
|
||||||
serializedSizeLE[1] = (size shr 8 and 0xff).toByte();
|
serializedSizeLE[1] = (size shr 8 and 0xff).toByte()
|
||||||
serializedSizeLE[2] = (size shr 16 and 0xff).toByte();
|
serializedSizeLE[2] = (size shr 16 and 0xff).toByte()
|
||||||
serializedSizeLE[3] = (size shr 24 and 0xff).toByte();
|
serializedSizeLE[3] = (size shr 24 and 0xff).toByte()
|
||||||
outputStream.write(serializedSizeLE);
|
outputStream.write(serializedSizeLE)
|
||||||
|
|
||||||
val opcodeBytes = ByteArray(1);
|
val opcodeBytes = ByteArray(1)
|
||||||
opcodeBytes[0] = opcode.value;
|
opcodeBytes[0] = opcode.value
|
||||||
outputStream.write(opcodeBytes);
|
outputStream.write(opcodeBytes)
|
||||||
|
|
||||||
if (data.isNotEmpty()) {
|
if (data.isNotEmpty()) {
|
||||||
outputStream.write(data);
|
outputStream.write(data)
|
||||||
}
|
}
|
||||||
|
|
||||||
Log.d(TAG, "Sent $size bytes: '$jsonString'.");
|
Log.d(TAG, "Sent $size bytes: '$jsonString'.")
|
||||||
} catch (e: Throwable) {
|
} catch (e: Throwable) {
|
||||||
Log.i(TAG, "Failed to send message.", e);
|
Log.i(TAG, "Failed to send message.", e)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -47,8 +47,11 @@ class MainActivity : AppCompatActivity() {
|
||||||
private lateinit var _imageSpinner: ImageView
|
private lateinit var _imageSpinner: ImageView
|
||||||
private lateinit var _layoutConnectionInfo: ConstraintLayout
|
private lateinit var _layoutConnectionInfo: ConstraintLayout
|
||||||
private lateinit var _videoBackground: StyledPlayerView
|
private lateinit var _videoBackground: StyledPlayerView
|
||||||
|
private lateinit var _viewDemo: View
|
||||||
private lateinit var _player: ExoPlayer
|
private lateinit var _player: ExoPlayer
|
||||||
private var _updating: Boolean = false
|
private var _updating: Boolean = false
|
||||||
|
private var _demoClickCount = 0
|
||||||
|
private var _lastDemoToast: Toast? = null
|
||||||
|
|
||||||
private val _scope: CoroutineScope = CoroutineScope(Dispatchers.Main)
|
private val _scope: CoroutineScope = CoroutineScope(Dispatchers.Main)
|
||||||
|
|
||||||
|
@ -64,6 +67,7 @@ class MainActivity : AppCompatActivity() {
|
||||||
_imageSpinner = findViewById(R.id.image_spinner)
|
_imageSpinner = findViewById(R.id.image_spinner)
|
||||||
_layoutConnectionInfo = findViewById(R.id.layout_connection_info)
|
_layoutConnectionInfo = findViewById(R.id.layout_connection_info)
|
||||||
_videoBackground = findViewById(R.id.video_background)
|
_videoBackground = findViewById(R.id.video_background)
|
||||||
|
_viewDemo = findViewById(R.id.view_demo)
|
||||||
|
|
||||||
startVideo()
|
startVideo()
|
||||||
startAnimations()
|
startAnimations()
|
||||||
|
@ -80,6 +84,18 @@ class MainActivity : AppCompatActivity() {
|
||||||
update()
|
update()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
_viewDemo.setOnClickListener {
|
||||||
|
_demoClickCount++
|
||||||
|
if (_demoClickCount in 2..4) {
|
||||||
|
val remainingClicks = 5 - _demoClickCount
|
||||||
|
_lastDemoToast?.cancel()
|
||||||
|
_lastDemoToast = Toast.makeText(this, "Click $remainingClicks more times to start demo", Toast.LENGTH_SHORT).apply { show() }
|
||||||
|
} else if (_demoClickCount == 5) {
|
||||||
|
TcpListenerService.instance?.onCastPlay(PlayMessage("video/mp4", "http://commondatastorage.googleapis.com/gtv-videos-bucket/sample/BigBuckBunny.mp4"))
|
||||||
|
_demoClickCount = 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (BuildConfig.IS_PLAYSTORE_VERSION) {
|
if (BuildConfig.IS_PLAYSTORE_VERSION) {
|
||||||
_text.visibility = View.INVISIBLE
|
_text.visibility = View.INVISIBLE
|
||||||
_buttonUpdate.visibility = View.INVISIBLE
|
_buttonUpdate.visibility = View.INVISIBLE
|
||||||
|
@ -249,15 +265,15 @@ class MainActivity : AppCompatActivity() {
|
||||||
}
|
}
|
||||||
|
|
||||||
private suspend fun checkForUpdates() {
|
private suspend fun checkForUpdates() {
|
||||||
Log.i(TAG, "Checking for updates...");
|
Log.i(TAG, "Checking for updates...")
|
||||||
|
|
||||||
withContext(Dispatchers.IO) {
|
withContext(Dispatchers.IO) {
|
||||||
try {
|
try {
|
||||||
val latestVersion = downloadVersionCode()
|
val latestVersion = downloadVersionCode()
|
||||||
|
|
||||||
if (latestVersion != null) {
|
if (latestVersion != null) {
|
||||||
val currentVersion = BuildConfig.VERSION_CODE;
|
val currentVersion = BuildConfig.VERSION_CODE
|
||||||
Log.i(TAG, "Current version $currentVersion latest version $latestVersion.");
|
Log.i(TAG, "Current version $currentVersion latest version $latestVersion.")
|
||||||
|
|
||||||
if (latestVersion > currentVersion) {
|
if (latestVersion > currentVersion) {
|
||||||
withContext(Dispatchers.Main) {
|
withContext(Dispatchers.Main) {
|
||||||
|
@ -267,8 +283,8 @@ class MainActivity : AppCompatActivity() {
|
||||||
setText(resources.getText(R.string.there_is_an_update_available_do_you_wish_to_update))
|
setText(resources.getText(R.string.there_is_an_update_available_do_you_wish_to_update))
|
||||||
_buttonUpdate.visibility = View.VISIBLE
|
_buttonUpdate.visibility = View.VISIBLE
|
||||||
} catch (e: Throwable) {
|
} catch (e: Throwable) {
|
||||||
Toast.makeText(this@MainActivity, "Failed to show update dialog", Toast.LENGTH_LONG).show();
|
Toast.makeText(this@MainActivity, "Failed to show update dialog", Toast.LENGTH_LONG).show()
|
||||||
Log.w(TAG, "Error occurred in update dialog.");
|
Log.w(TAG, "Error occurred in update dialog.")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
@ -277,21 +293,21 @@ class MainActivity : AppCompatActivity() {
|
||||||
_buttonUpdate.visibility = View.INVISIBLE
|
_buttonUpdate.visibility = View.INVISIBLE
|
||||||
//setText(getString(R.string.no_updates_available))
|
//setText(getString(R.string.no_updates_available))
|
||||||
setText(null)
|
setText(null)
|
||||||
//Toast.makeText(this@MainActivity, "Already on latest version", Toast.LENGTH_LONG).show();
|
//Toast.makeText(this@MainActivity, "Already on latest version", Toast.LENGTH_LONG).show()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
Log.w(TAG, "Failed to retrieve version from version URL.");
|
Log.w(TAG, "Failed to retrieve version from version URL.")
|
||||||
|
|
||||||
withContext(Dispatchers.Main) {
|
withContext(Dispatchers.Main) {
|
||||||
Toast.makeText(this@MainActivity, "Failed to retrieve version", Toast.LENGTH_LONG).show();
|
Toast.makeText(this@MainActivity, "Failed to retrieve version", Toast.LENGTH_LONG).show()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (e: Throwable) {
|
} catch (e: Throwable) {
|
||||||
Log.w(TAG, "Failed to check for updates.", e);
|
Log.w(TAG, "Failed to check for updates.", e)
|
||||||
|
|
||||||
withContext(Dispatchers.Main) {
|
withContext(Dispatchers.Main) {
|
||||||
Toast.makeText(this@MainActivity, "Failed to check for updates", Toast.LENGTH_LONG).show();
|
Toast.makeText(this@MainActivity, "Failed to check for updates", Toast.LENGTH_LONG).show()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -452,7 +468,7 @@ class MainActivity : AppCompatActivity() {
|
||||||
addr.hostAddress?.let { ips.add(it) }
|
addr.hostAddress?.let { ips.add(it) }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return ips;
|
return ips
|
||||||
}
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
|
|
|
@ -1,18 +1,25 @@
|
||||||
package com.futo.fcast.receiver
|
package com.futo.fcast.receiver
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
|
import android.graphics.drawable.Animatable
|
||||||
import android.net.*
|
import android.net.*
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
|
import android.view.KeyEvent
|
||||||
|
import android.view.View
|
||||||
import android.view.Window
|
import android.view.Window
|
||||||
import android.view.WindowInsets
|
import android.view.WindowInsets
|
||||||
import android.view.WindowManager
|
import android.view.WindowManager
|
||||||
|
import android.widget.ImageView
|
||||||
|
import android.widget.TextView
|
||||||
import androidx.appcompat.app.AppCompatActivity
|
import androidx.appcompat.app.AppCompatActivity
|
||||||
|
import androidx.constraintlayout.widget.ConstraintLayout
|
||||||
import com.google.android.exoplayer2.*
|
import com.google.android.exoplayer2.*
|
||||||
import com.google.android.exoplayer2.source.DefaultMediaSourceFactory
|
import com.google.android.exoplayer2.source.DefaultMediaSourceFactory
|
||||||
import com.google.android.exoplayer2.source.dash.DashMediaSource
|
import com.google.android.exoplayer2.source.dash.DashMediaSource
|
||||||
import com.google.android.exoplayer2.source.hls.HlsMediaSource
|
import com.google.android.exoplayer2.source.hls.HlsMediaSource
|
||||||
|
import com.google.android.exoplayer2.text.ExoplayerCuesDecoder
|
||||||
import com.google.android.exoplayer2.trackselection.DefaultTrackSelector
|
import com.google.android.exoplayer2.trackselection.DefaultTrackSelector
|
||||||
import com.google.android.exoplayer2.ui.StyledPlayerView
|
import com.google.android.exoplayer2.ui.StyledPlayerView
|
||||||
import com.google.android.exoplayer2.upstream.DefaultDataSource
|
import com.google.android.exoplayer2.upstream.DefaultDataSource
|
||||||
|
@ -20,14 +27,18 @@ import kotlinx.coroutines.*
|
||||||
import java.io.File
|
import java.io.File
|
||||||
import java.io.FileOutputStream
|
import java.io.FileOutputStream
|
||||||
import kotlin.math.abs
|
import kotlin.math.abs
|
||||||
|
import kotlin.math.max
|
||||||
|
|
||||||
class PlayerActivity : AppCompatActivity() {
|
class PlayerActivity : AppCompatActivity() {
|
||||||
private lateinit var _playerControlView: StyledPlayerView
|
private lateinit var _playerControlView: StyledPlayerView
|
||||||
|
private lateinit var _imageSpinner: ImageView
|
||||||
|
private lateinit var _textMessage: TextView
|
||||||
|
private lateinit var _layoutOverlay: ConstraintLayout
|
||||||
private lateinit var _exoPlayer: ExoPlayer
|
private lateinit var _exoPlayer: ExoPlayer
|
||||||
private var _shouldPlaybackRestartOnConnectivity: Boolean = false
|
private var _shouldPlaybackRestartOnConnectivity: Boolean = false
|
||||||
private lateinit var _connectivityManager: ConnectivityManager
|
private lateinit var _connectivityManager: ConnectivityManager
|
||||||
private lateinit var _scope: CoroutineScope
|
private lateinit var _scope: CoroutineScope
|
||||||
private var _wasPlaying = false;
|
private var _wasPlaying = false
|
||||||
|
|
||||||
val currentPosition get() = _exoPlayer.currentPosition
|
val currentPosition get() = _exoPlayer.currentPosition
|
||||||
val isPlaying get() = _exoPlayer.isPlaying
|
val isPlaying get() = _exoPlayer.isPlaying
|
||||||
|
@ -59,16 +70,25 @@ class PlayerActivity : AppCompatActivity() {
|
||||||
private val _playerEventListener = object: Player.Listener {
|
private val _playerEventListener = object: Player.Listener {
|
||||||
override fun onPlaybackStateChanged(playbackState: Int) {
|
override fun onPlaybackStateChanged(playbackState: Int) {
|
||||||
super.onPlaybackStateChanged(playbackState)
|
super.onPlaybackStateChanged(playbackState)
|
||||||
|
Log.i(TAG, "onPlaybackStateChanged playbackState=$playbackState")
|
||||||
|
|
||||||
if (_shouldPlaybackRestartOnConnectivity && playbackState == ExoPlayer.STATE_READY) {
|
if (_shouldPlaybackRestartOnConnectivity && playbackState == ExoPlayer.STATE_READY) {
|
||||||
Log.i(TAG, "_shouldPlaybackRestartOnConnectivity=false")
|
Log.i(TAG, "_shouldPlaybackRestartOnConnectivity=false")
|
||||||
_shouldPlaybackRestartOnConnectivity = false
|
_shouldPlaybackRestartOnConnectivity = false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (playbackState == ExoPlayer.STATE_READY) {
|
||||||
|
setStatus(false, null)
|
||||||
|
} else if (playbackState == ExoPlayer.STATE_BUFFERING) {
|
||||||
|
setStatus(true, null)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onPlayerError(error: PlaybackException) {
|
override fun onPlayerError(error: PlaybackException) {
|
||||||
super.onPlayerError(error)
|
super.onPlayerError(error)
|
||||||
|
|
||||||
|
Log.e(TAG, "onPlayerError: $error")
|
||||||
|
|
||||||
when (error.errorCode) {
|
when (error.errorCode) {
|
||||||
PlaybackException.ERROR_CODE_IO_BAD_HTTP_STATUS,
|
PlaybackException.ERROR_CODE_IO_BAD_HTTP_STATUS,
|
||||||
PlaybackException.ERROR_CODE_IO_CLEARTEXT_NOT_PERMITTED,
|
PlaybackException.ERROR_CODE_IO_CLEARTEXT_NOT_PERMITTED,
|
||||||
|
@ -83,6 +103,8 @@ class PlayerActivity : AppCompatActivity() {
|
||||||
_shouldPlaybackRestartOnConnectivity = true
|
_shouldPlaybackRestartOnConnectivity = true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
setStatus(false, getFullExceptionMessage(error))
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onVolumeChanged(volume: Float) {
|
override fun onVolumeChanged(volume: Float) {
|
||||||
|
@ -107,8 +129,13 @@ class PlayerActivity : AppCompatActivity() {
|
||||||
setFullScreen()
|
setFullScreen()
|
||||||
|
|
||||||
_playerControlView = findViewById(R.id.player_control_view)
|
_playerControlView = findViewById(R.id.player_control_view)
|
||||||
|
_imageSpinner = findViewById(R.id.image_spinner)
|
||||||
|
_textMessage = findViewById(R.id.text_message)
|
||||||
|
_layoutOverlay = findViewById(R.id.layout_overlay)
|
||||||
_scope = CoroutineScope(Dispatchers.Main)
|
_scope = CoroutineScope(Dispatchers.Main)
|
||||||
|
|
||||||
|
setStatus(true, null)
|
||||||
|
|
||||||
val trackSelector = DefaultTrackSelector(this)
|
val trackSelector = DefaultTrackSelector(this)
|
||||||
trackSelector.parameters = trackSelector.parameters
|
trackSelector.parameters = trackSelector.parameters
|
||||||
.buildUpon()
|
.buildUpon()
|
||||||
|
@ -148,6 +175,35 @@ class PlayerActivity : AppCompatActivity() {
|
||||||
if (hasFocus) setFullScreen()
|
if (hasFocus) setFullScreen()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun getFullExceptionMessage(ex: Throwable): String {
|
||||||
|
val messages = mutableListOf<String>()
|
||||||
|
var current: Throwable? = ex
|
||||||
|
while (current != null) {
|
||||||
|
messages.add(current.message ?: "Unknown error")
|
||||||
|
current = current.cause
|
||||||
|
}
|
||||||
|
return messages.joinToString(separator = " → ")
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun setStatus(isLoading: Boolean, message: String?) {
|
||||||
|
if (isLoading) {
|
||||||
|
(_imageSpinner.drawable as Animatable?)?.start()
|
||||||
|
_imageSpinner.visibility = View.VISIBLE
|
||||||
|
} else {
|
||||||
|
(_imageSpinner.drawable as Animatable?)?.stop()
|
||||||
|
_imageSpinner.visibility = View.GONE
|
||||||
|
}
|
||||||
|
|
||||||
|
if (message != null) {
|
||||||
|
_textMessage.visibility = View.VISIBLE
|
||||||
|
_textMessage.text = message
|
||||||
|
} else {
|
||||||
|
_textMessage.visibility = View.GONE
|
||||||
|
}
|
||||||
|
|
||||||
|
_layoutOverlay.visibility = if (isLoading || message != null) View.VISIBLE else View.GONE
|
||||||
|
}
|
||||||
|
|
||||||
private fun setFullScreen() {
|
private fun setFullScreen() {
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
|
||||||
window.insetsController?.hide(WindowInsets.Type.statusBars())
|
window.insetsController?.hide(WindowInsets.Type.statusBars())
|
||||||
|
@ -189,6 +245,28 @@ class PlayerActivity : AppCompatActivity() {
|
||||||
TcpListenerService.activityCount--
|
TcpListenerService.activityCount--
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun onKeyDown(keyCode: Int, event: KeyEvent?): Boolean {
|
||||||
|
when (keyCode) {
|
||||||
|
KeyEvent.KEYCODE_DPAD_LEFT -> {
|
||||||
|
val newPosition = _exoPlayer.currentPosition - 10000
|
||||||
|
_exoPlayer.seekTo(max(0, newPosition))
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
KeyEvent.KEYCODE_DPAD_RIGHT -> {
|
||||||
|
val newPosition = _exoPlayer.currentPosition + 10000
|
||||||
|
_exoPlayer.seekTo(newPosition)
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
KeyEvent.KEYCODE_BACK -> {
|
||||||
|
if (_playerControlView.isControllerFullyVisible) {
|
||||||
|
_playerControlView.hideController()
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return super.onKeyDown(keyCode, event)
|
||||||
|
}
|
||||||
|
|
||||||
fun play(playMessage: PlayMessage) {
|
fun play(playMessage: PlayMessage) {
|
||||||
val mediaItemBuilder = MediaItem.Builder()
|
val mediaItemBuilder = MediaItem.Builder()
|
||||||
if (playMessage.container.isNotEmpty()) {
|
if (playMessage.container.isNotEmpty()) {
|
||||||
|
@ -225,6 +303,7 @@ class PlayerActivity : AppCompatActivity() {
|
||||||
_exoPlayer.seekTo(playMessage.time * 1000)
|
_exoPlayer.seekTo(playMessage.time * 1000)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
setStatus(true, null)
|
||||||
_wasPlaying = false
|
_wasPlaying = false
|
||||||
_exoPlayer.playWhenReady = true
|
_exoPlayer.playWhenReady = true
|
||||||
_exoPlayer.prepare()
|
_exoPlayer.prepare()
|
||||||
|
@ -250,5 +329,8 @@ class PlayerActivity : AppCompatActivity() {
|
||||||
companion object {
|
companion object {
|
||||||
var instance: PlayerActivity? = null
|
var instance: PlayerActivity? = null
|
||||||
private const val TAG = "PlayerActivity"
|
private const val TAG = "PlayerActivity"
|
||||||
|
|
||||||
|
private const val SEEK_BACKWARD_MILLIS = 10_000
|
||||||
|
private const val SEEK_FORWARD_MILLIS = 10_000
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -108,7 +108,7 @@ class TcpListenerService : Service() {
|
||||||
_discoveryService = null
|
_discoveryService = null
|
||||||
|
|
||||||
_serverSocket?.close()
|
_serverSocket?.close()
|
||||||
_serverSocket = null;
|
_serverSocket = null
|
||||||
|
|
||||||
_listenThread?.join()
|
_listenThread?.join()
|
||||||
_listenThread = null
|
_listenThread = null
|
||||||
|
|
|
@ -14,6 +14,13 @@
|
||||||
android:layout_height="match_parent"
|
android:layout_height="match_parent"
|
||||||
app:resize_mode="zoom" />
|
app:resize_mode="zoom" />
|
||||||
|
|
||||||
|
<View
|
||||||
|
android:id="@+id/view_demo"
|
||||||
|
android:layout_width="40dp"
|
||||||
|
android:layout_height="40dp"
|
||||||
|
app:layout_constraintTop_toTopOf="parent"
|
||||||
|
app:layout_constraintStart_toStartOf="parent" />
|
||||||
|
|
||||||
<TextView
|
<TextView
|
||||||
android:id="@+id/text_title"
|
android:id="@+id/text_title"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
|
|
|
@ -3,6 +3,7 @@
|
||||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="match_parent"
|
android:layout_height="match_parent"
|
||||||
|
xmlns:tools="http://schemas.android.com/tools"
|
||||||
android:background="@color/black">
|
android:background="@color/black">
|
||||||
|
|
||||||
<com.google.android.exoplayer2.ui.StyledPlayerView
|
<com.google.android.exoplayer2.ui.StyledPlayerView
|
||||||
|
@ -11,4 +12,43 @@
|
||||||
android:layout_height="match_parent"
|
android:layout_height="match_parent"
|
||||||
android:keepScreenOn="true"
|
android:keepScreenOn="true"
|
||||||
app:use_controller="true" />
|
app:use_controller="true" />
|
||||||
|
|
||||||
|
<androidx.constraintlayout.widget.ConstraintLayout
|
||||||
|
android:id="@+id/layout_overlay"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
android:background="#66000000"
|
||||||
|
android:clickable="false">
|
||||||
|
|
||||||
|
<ImageView
|
||||||
|
android:id="@+id/image_spinner"
|
||||||
|
android:layout_width="80dp"
|
||||||
|
android:layout_height="80dp"
|
||||||
|
android:layout_marginStart="8dp"
|
||||||
|
android:alpha="0.5"
|
||||||
|
app:srcCompat="@drawable/ic_loader_animated"
|
||||||
|
android:clickable="false"
|
||||||
|
app:layout_constraintTop_toTopOf="parent"
|
||||||
|
app:layout_constraintBottom_toBottomOf="parent"
|
||||||
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/text_message"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:gravity="center_horizontal"
|
||||||
|
tools:text="This is a test message"
|
||||||
|
android:textColor="@color/white"
|
||||||
|
android:fontFamily="@font/inter_regular"
|
||||||
|
android:textSize="16sp"
|
||||||
|
android:clickable="false"
|
||||||
|
android:layout_marginStart="8dp"
|
||||||
|
android:layout_marginEnd="8dp"
|
||||||
|
android:layout_marginBottom="10dp"
|
||||||
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
|
app:layout_constraintBottom_toBottomOf="parent" />
|
||||||
|
|
||||||
|
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||||
</FrameLayout>
|
</FrameLayout>
|
Loading…
Add table
Add a link
Reference in a new issue