1
0
Fork 0
mirror of https://gitlab.com/futo-org/fcast.git synced 2025-06-24 21:25:23 +00:00

Fixed headers and removed deprecations.

This commit is contained in:
Koen 2024-01-04 13:20:24 +01:00
parent 28886045af
commit 2cd9db028d
11 changed files with 85 additions and 76 deletions

View file

@ -85,10 +85,13 @@ dependencies {
implementation 'androidx.appcompat:appcompat:1.6.1' implementation 'androidx.appcompat:appcompat:1.6.1'
implementation 'com.google.android.material:material:1.11.0' implementation 'com.google.android.material:material:1.11.0'
implementation "org.jetbrains.kotlinx:kotlinx-serialization-json:1.6.2" implementation "org.jetbrains.kotlinx:kotlinx-serialization-json:1.6.2"
implementation 'com.google.android.exoplayer:exoplayer:2.19.1' implementation 'androidx.media3:media3-exoplayer:1.2.0'
implementation "com.squareup.okhttp3:okhttp:4.11.0" implementation "com.squareup.okhttp3:okhttp:4.11.0"
implementation 'com.journeyapps:zxing-android-embedded:4.3.0' implementation 'com.journeyapps:zxing-android-embedded:4.3.0'
implementation 'org.java-websocket:Java-WebSocket:1.5.4' implementation 'org.java-websocket:Java-WebSocket:1.5.4'
implementation 'androidx.media3:media3-ui:1.2.0'
implementation 'androidx.media3:media3-exoplayer-dash:1.2.0'
implementation 'androidx.media3:media3-exoplayer-hls:1.2.0'
testImplementation 'junit:junit:4.13.2' testImplementation 'junit:junit:4.13.2'
androidTestImplementation 'androidx.test.ext:junit:1.1.5' androidTestImplementation 'androidx.test.ext:junit:1.1.5'
androidTestImplementation 'androidx.test.espresso:espresso-core:3.5.1' androidTestImplementation 'androidx.test.espresso:espresso-core:3.5.1'

View file

@ -31,6 +31,7 @@ class BootReceiver : BroadcastReceiver() {
} }
} }
@Suppress("DEPRECATION")
private fun createNotificationBuilder(context: Context): NotificationCompat.Builder { private fun createNotificationBuilder(context: Context): NotificationCompat.Builder {
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
NotificationCompat.Builder(context, CHANNEL_ID) NotificationCompat.Builder(context, CHANNEL_ID)

View file

@ -2,7 +2,6 @@ package com.futo.fcast.receiver
import android.content.Context import android.content.Context
import android.util.AttributeSet import android.util.AttributeSet
import android.view.KeyEvent import androidx.media3.ui.PlayerView
import com.google.android.exoplayer2.ui.StyledPlayerView
class CustomStyledPlayerView(context: Context, attrs: AttributeSet? = null) : StyledPlayerView(context, attrs) { } class CustomPlayerView(context: Context, attrs: AttributeSet? = null) : PlayerView(context, attrs) { }

View file

@ -4,6 +4,7 @@ import android.content.BroadcastReceiver
import android.content.Context import android.content.Context
import android.content.Intent import android.content.Intent
import android.content.pm.PackageInstaller import android.content.pm.PackageInstaller
import android.os.Build
import android.util.Log import android.util.Log
class InstallReceiver : BroadcastReceiver() { class InstallReceiver : BroadcastReceiver() {
@ -13,7 +14,13 @@ class InstallReceiver : BroadcastReceiver() {
when (status) { when (status) {
PackageInstaller.STATUS_PENDING_USER_ACTION -> { PackageInstaller.STATUS_PENDING_USER_ACTION -> {
val activityIntent = intent.getParcelableExtra<Intent>(Intent.EXTRA_INTENT) val activityIntent: Intent? = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
intent.getParcelableExtra(Intent.EXTRA_INTENT, Intent::class.java)
} else {
@Suppress("DEPRECATION")
intent.getParcelableExtra(Intent.EXTRA_INTENT)
}
if (activityIntent == null) { if (activityIntent == null) {
Log.w(TAG, "Received STATUS_PENDING_USER_ACTION and activity intent is null.") Log.w(TAG, "Received STATUS_PENDING_USER_ACTION and activity intent is null.")
return return

View file

@ -2,6 +2,7 @@ package com.futo.fcast.receiver
import WebSocketListenerService import WebSocketListenerService
import android.Manifest import android.Manifest
import android.app.Activity
import android.app.AlertDialog import android.app.AlertDialog
import android.app.PendingIntent import android.app.PendingIntent
import android.content.Context import android.content.Context
@ -19,14 +20,17 @@ import android.util.TypedValue
import android.view.View import android.view.View
import android.view.WindowManager import android.view.WindowManager
import android.widget.* import android.widget.*
import androidx.activity.result.ActivityResultLauncher
import androidx.activity.result.contract.ActivityResultContracts
import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.app.AppCompatActivity
import androidx.constraintlayout.widget.ConstraintLayout import androidx.constraintlayout.widget.ConstraintLayout
import androidx.core.app.ActivityCompat import androidx.core.app.ActivityCompat
import androidx.core.content.ContextCompat import androidx.core.content.ContextCompat
import com.google.android.exoplayer2.ExoPlayer import androidx.lifecycle.lifecycleScope
import com.google.android.exoplayer2.MediaItem import androidx.media3.common.MediaItem
import com.google.android.exoplayer2.Player import androidx.media3.common.Player
import com.google.android.exoplayer2.ui.StyledPlayerView import androidx.media3.exoplayer.ExoPlayer
import androidx.media3.ui.PlayerView
import com.google.zxing.BarcodeFormat import com.google.zxing.BarcodeFormat
import com.journeyapps.barcodescanner.BarcodeEncoder import com.journeyapps.barcodescanner.BarcodeEncoder
import kotlinx.coroutines.* import kotlinx.coroutines.*
@ -46,27 +50,38 @@ class MainActivity : AppCompatActivity() {
private lateinit var _updateSpinner: ImageView private lateinit var _updateSpinner: ImageView
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: PlayerView
private lateinit var _viewDemo: View private lateinit var _viewDemo: View
private lateinit var _player: ExoPlayer private lateinit var _player: ExoPlayer
private lateinit var _imageQr: ImageView private lateinit var _imageQr: ImageView
private lateinit var _textScanToConnect: TextView private lateinit var _textScanToConnect: TextView
private lateinit var _systemAlertWindowPermissionLauncher: ActivityResultLauncher<Intent>
private var _updateAvailable: Boolean? = null private var _updateAvailable: Boolean? = null
private var _updating: Boolean = false private var _updating: Boolean = false
private var _demoClickCount = 0 private var _demoClickCount = 0
private var _lastDemoToast: Toast? = null private var _lastDemoToast: Toast? = null
private val _preferenceFileKey get() = "$packageName.PREFERENCE_FILE_KEY" private val _preferenceFileKey get() = "$packageName.PREFERENCE_FILE_KEY"
private val _scope: CoroutineScope = CoroutineScope(Dispatchers.Main)
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main) setContentView(R.layout.activity_main)
if (savedInstanceState != null && savedInstanceState.containsKey("updateAvailable")) { _systemAlertWindowPermissionLauncher = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { _ ->
_updateAvailable = savedInstanceState.getBoolean("updateAvailable", false) if (Settings.canDrawOverlays(this)) {
// Permission granted, you can launch the activity from the foreground service
Toast.makeText(this, "Alert window permission granted", Toast.LENGTH_LONG).show()
Log.i(TAG, "Alert window permission granted")
} else {
// Permission denied, notify the user and request again if necessary
Toast.makeText(this, "Permission is required to work in background", Toast.LENGTH_LONG).show()
Log.i(TAG, "Alert window permission denied")
}
}
_updateAvailable = if (savedInstanceState != null && savedInstanceState.containsKey("updateAvailable")) {
savedInstanceState.getBoolean("updateAvailable", false)
} else { } else {
_updateAvailable = null null
} }
_buttonUpdate = findViewById(R.id.button_update) _buttonUpdate = findViewById(R.id.button_update)
@ -118,13 +133,13 @@ class MainActivity : AppCompatActivity() {
_updateSpinner.visibility = View.VISIBLE _updateSpinner.visibility = View.VISIBLE
(_updateSpinner.drawable as Animatable?)?.start() (_updateSpinner.drawable as Animatable?)?.start()
_scope.launch(Dispatchers.IO) { lifecycleScope.launch(Dispatchers.IO) {
checkForUpdates() checkForUpdates()
} }
} }
val ips = getIPs() val ips = getIPs()
_textIPs.text = "IPs\n" + ips.joinToString("\n") + "\n\nPorts\n${TcpListenerService.PORT} (TCP), ${WebSocketListenerService.PORT} (WS)" _textIPs.text = "IPs\n${ips.joinToString("\n")}\n\nPorts\n${TcpListenerService.PORT} (TCP), ${WebSocketListenerService.PORT} (WS)"
try { try {
val barcodeEncoder = BarcodeEncoder() val barcodeEncoder = BarcodeEncoder()
@ -168,7 +183,6 @@ class MainActivity : AppCompatActivity() {
override fun onDestroy() { override fun onDestroy() {
super.onDestroy() super.onDestroy()
InstallReceiver.onReceiveResult = null InstallReceiver.onReceiveResult = null
_scope.cancel()
_player.release() _player.release()
NetworkService.activityCount-- NetworkService.activityCount--
} }
@ -179,11 +193,7 @@ class MainActivity : AppCompatActivity() {
} }
private fun restartService() { private fun restartService() {
val i = NetworkService.instance NetworkService.instance?.stopSelf()
if (i != null) {
i.stopSelf()
}
startService(Intent(this, NetworkService::class.java)) startService(Intent(this, NetworkService::class.java))
} }
@ -245,7 +255,7 @@ class MainActivity : AppCompatActivity() {
.setPositiveButton(R.string.permission_dialog_positive_button) { _, _ -> .setPositiveButton(R.string.permission_dialog_positive_button) { _, _ ->
try { try {
val intent = Intent(Settings.ACTION_MANAGE_OVERLAY_PERMISSION, Uri.parse("package:$packageName")) val intent = Intent(Settings.ACTION_MANAGE_OVERLAY_PERMISSION, Uri.parse("package:$packageName"))
startActivityForResult(intent, REQUEST_CODE) _systemAlertWindowPermissionLauncher.launch(intent)
} catch (e: Throwable) { } catch (e: Throwable) {
Log.e("OverlayPermission", "Error requesting overlay permission", e) Log.e("OverlayPermission", "Error requesting overlay permission", e)
Toast.makeText(this, "An error occurred: ${e.message}", Toast.LENGTH_LONG).show() Toast.makeText(this, "An error occurred: ${e.message}", Toast.LENGTH_LONG).show()
@ -271,23 +281,6 @@ class MainActivity : AppCompatActivity() {
} }
} }
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
super.onActivityResult(requestCode, resultCode, data)
if (requestCode == REQUEST_CODE) {
if (Settings.canDrawOverlays(this)) {
// Permission granted, you can launch the activity from the foreground service
Toast.makeText(this, "Alert window permission granted", Toast.LENGTH_LONG).show()
Log.i(TAG, "Alert window permission granted")
} else {
// Permission denied, notify the user and request again if necessary
Toast.makeText(this, "Permission is required to work in background", Toast.LENGTH_LONG).show()
Log.i(TAG, "Alert window permission denied")
}
}
super.onActivityResult(requestCode, resultCode, data)
}
override fun onRequestPermissionsResult(requestCode: Int, permissions: Array<out String>, grantResults: IntArray) { override fun onRequestPermissionsResult(requestCode: Int, permissions: Array<out String>, grantResults: IntArray) {
super.onRequestPermissionsResult(requestCode, permissions, grantResults) super.onRequestPermissionsResult(requestCode, permissions, grantResults)
@ -410,7 +403,7 @@ class MainActivity : AppCompatActivity() {
setText(resources.getText(R.string.downloading_update)) setText(resources.getText(R.string.downloading_update))
(_updateSpinner.drawable as Animatable?)?.start() (_updateSpinner.drawable as Animatable?)?.start()
_scope.launch(Dispatchers.IO) { lifecycleScope.launch(Dispatchers.IO) {
var inputStream: InputStream? = null var inputStream: InputStream? = null
try { try {
val client = OkHttpClient() val client = OkHttpClient()
@ -458,8 +451,7 @@ class MainActivity : AppCompatActivity() {
if (lastProgressText != progressText) { if (lastProgressText != progressText) {
lastProgressText = progressText lastProgressText = progressText
//TODO: Use proper scope lifecycleScope.launch(Dispatchers.Main) {
GlobalScope.launch(Dispatchers.Main) {
_textProgress.text = progressText _textProgress.text = progressText
} }
} }
@ -504,7 +496,7 @@ class MainActivity : AppCompatActivity() {
(_updateSpinner.drawable as Animatable?)?.stop() (_updateSpinner.drawable as Animatable?)?.stop()
if (result == null || result.isBlank()) { if (result.isNullOrBlank()) {
_updateSpinner.setImageResource(R.drawable.ic_update_success) _updateSpinner.setImageResource(R.drawable.ic_update_success)
setText(resources.getText(R.string.success)) setText(resources.getText(R.string.success))
} else { } else {

View file

@ -11,6 +11,8 @@ import android.util.Log
import android.widget.Toast import android.widget.Toast
import androidx.core.app.NotificationCompat import androidx.core.app.NotificationCompat
import kotlinx.coroutines.* import kotlinx.coroutines.*
import kotlinx.serialization.encodeToString
import kotlinx.serialization.json.Json
class NetworkService : Service() { class NetworkService : Service() {
private var _discoveryService: DiscoveryService? = null private var _discoveryService: DiscoveryService? = null
@ -208,11 +210,7 @@ class NetworkService : Service() {
if (PlayerActivity.instance == null) { if (PlayerActivity.instance == null) {
val i = Intent(this@NetworkService, PlayerActivity::class.java) val i = Intent(this@NetworkService, PlayerActivity::class.java)
i.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) i.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
i.putExtra("container", playMessage.container) i.putExtra("message", Json.encodeToString(playMessage))
i.putExtra("url", playMessage.url)
i.putExtra("content", playMessage.content)
i.putExtra("time", playMessage.time)
i.putExtra("speed", playMessage.speed)
if (activityCount > 0) { if (activityCount > 0) {
startActivity(i) startActivity(i)

View file

@ -16,25 +16,28 @@ import android.view.WindowInsets
import android.view.WindowManager import android.view.WindowManager
import android.widget.ImageView import android.widget.ImageView
import android.widget.TextView import android.widget.TextView
import androidx.annotation.OptIn
import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.app.AppCompatActivity
import androidx.constraintlayout.widget.ConstraintLayout import androidx.constraintlayout.widget.ConstraintLayout
import com.google.android.exoplayer2.ExoPlayer import androidx.media3.common.MediaItem
import com.google.android.exoplayer2.MediaItem import androidx.media3.common.PlaybackException
import com.google.android.exoplayer2.PlaybackException import androidx.media3.common.PlaybackParameters
import com.google.android.exoplayer2.PlaybackParameters import androidx.media3.common.Player
import com.google.android.exoplayer2.Player import androidx.media3.common.util.UnstableApi
import com.google.android.exoplayer2.source.DefaultMediaSourceFactory import androidx.media3.datasource.DefaultDataSource
import com.google.android.exoplayer2.source.dash.DashMediaSource import androidx.media3.datasource.DefaultHttpDataSource
import com.google.android.exoplayer2.source.hls.HlsMediaSource import androidx.media3.datasource.HttpDataSource
import com.google.android.exoplayer2.trackselection.DefaultTrackSelector import androidx.media3.exoplayer.ExoPlayer
import com.google.android.exoplayer2.ui.StyledPlayerView import androidx.media3.exoplayer.dash.DashMediaSource
import com.google.android.exoplayer2.upstream.DefaultDataSource import androidx.media3.exoplayer.hls.HlsMediaSource
import com.google.android.exoplayer2.upstream.DefaultHttpDataSource import androidx.media3.exoplayer.source.DefaultMediaSourceFactory
import com.google.android.exoplayer2.upstream.HttpDataSource import androidx.media3.exoplayer.trackselection.DefaultTrackSelector
import androidx.media3.ui.PlayerView
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.cancel import kotlinx.coroutines.cancel
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.serialization.json.Json
import java.io.File import java.io.File
import java.io.FileOutputStream import java.io.FileOutputStream
import kotlin.math.abs import kotlin.math.abs
@ -42,7 +45,7 @@ import kotlin.math.max
class PlayerActivity : AppCompatActivity() { class PlayerActivity : AppCompatActivity() {
private lateinit var _playerControlView: StyledPlayerView private lateinit var _playerControlView: PlayerView
private lateinit var _imageSpinner: ImageView private lateinit var _imageSpinner: ImageView
private lateinit var _textMessage: TextView private lateinit var _textMessage: TextView
private lateinit var _layoutOverlay: ConstraintLayout private lateinit var _layoutOverlay: ConstraintLayout
@ -165,6 +168,7 @@ class PlayerActivity : AppCompatActivity() {
} }
} }
@OptIn(UnstableApi::class)
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
Log.i(TAG, "onCreate") Log.i(TAG, "onCreate")
@ -203,13 +207,15 @@ class PlayerActivity : AppCompatActivity() {
.build() .build()
_connectivityManager.registerNetworkCallback(netReq, _connectivityEvents) _connectivityManager.registerNetworkCallback(netReq, _connectivityEvents)
val container = intent.getStringExtra("container") ?: "" val playMessage = intent.getStringExtra("message")?.let {
val url = intent.getStringExtra("url") try {
val content = intent.getStringExtra("content") Json.decodeFromString<PlayMessage>(it)
val time = intent.getDoubleExtra("time", 0.0) } catch (e: Throwable) {
val speed = intent.getDoubleExtra("speed", 1.0) Log.i(TAG, "Failed to deserialize play message.", e)
null
play(PlayMessage(container, url, content, time, speed)) }
}
playMessage?.let { play(it) }
instance = this instance = this
NetworkService.activityCount++ NetworkService.activityCount++
@ -290,6 +296,7 @@ class PlayerActivity : AppCompatActivity() {
NetworkService.activityCount-- NetworkService.activityCount--
} }
@OptIn(UnstableApi::class)
override fun dispatchKeyEvent(event: KeyEvent): Boolean { override fun dispatchKeyEvent(event: KeyEvent): Boolean {
if (_playerControlView.isControllerFullyVisible) { if (_playerControlView.isControllerFullyVisible) {
if (event.keyCode == KeyEvent.KEYCODE_BACK) { if (event.keyCode == KeyEvent.KEYCODE_BACK) {
@ -312,6 +319,7 @@ class PlayerActivity : AppCompatActivity() {
return super.dispatchKeyEvent(event) return super.dispatchKeyEvent(event)
} }
@OptIn(UnstableApi::class)
fun play(playMessage: PlayMessage) { fun play(playMessage: PlayMessage) {
val mediaItemBuilder = MediaItem.Builder() val mediaItemBuilder = MediaItem.Builder()
if (playMessage.container.isNotEmpty()) { if (playMessage.container.isNotEmpty()) {

View file

@ -7,7 +7,7 @@
android:layout_height="match_parent" android:layout_height="match_parent"
android:background="#000000"> android:background="#000000">
<com.google.android.exoplayer2.ui.StyledPlayerView <androidx.media3.ui.PlayerView
android:id="@+id/video_background" android:id="@+id/video_background"
app:use_controller="false" app:use_controller="false"
android:layout_width="match_parent" android:layout_width="match_parent"
@ -59,7 +59,7 @@
android:gravity="center" android:gravity="center"
android:fontFamily="@font/inter_regular" android:fontFamily="@font/inter_regular"
android:textSize="14sp" android:textSize="14sp"
android:text="Waiting for a connection" android:text="@string/waiting_for_media"
app:layout_constraintTop_toTopOf="parent" app:layout_constraintTop_toTopOf="parent"
app:layout_constraintStart_toStartOf="parent" app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent" app:layout_constraintEnd_toEndOf="parent"

View file

@ -7,7 +7,7 @@
android:layout_height="match_parent" android:layout_height="match_parent"
android:background="#000000"> android:background="#000000">
<com.google.android.exoplayer2.ui.StyledPlayerView <androidx.media3.ui.PlayerView
android:id="@+id/video_background" android:id="@+id/video_background"
app:use_controller="false" app:use_controller="false"
android:layout_width="match_parent" android:layout_width="match_parent"
@ -59,7 +59,7 @@
android:gravity="center" android:gravity="center"
android:fontFamily="@font/inter_regular" android:fontFamily="@font/inter_regular"
android:textSize="14sp" android:textSize="14sp"
android:text="Waiting for a connection" android:text="@string/waiting_for_media"
app:layout_constraintTop_toTopOf="parent" app:layout_constraintTop_toTopOf="parent"
app:layout_constraintStart_toStartOf="parent" app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent" app:layout_constraintEnd_toEndOf="parent"

View file

@ -6,7 +6,7 @@
xmlns:tools="http://schemas.android.com/tools" xmlns:tools="http://schemas.android.com/tools"
android:background="@color/black"> android:background="@color/black">
<com.futo.fcast.receiver.CustomStyledPlayerView <com.futo.fcast.receiver.CustomPlayerView
android:id="@+id/player_control_view" android:id="@+id/player_control_view"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent" android:layout_height="match_parent"

View file

@ -21,4 +21,5 @@
<string name="permission_dialog_message">This app requires the System Alert Window permission to display content on top of other apps. Please grant the permission.</string> <string name="permission_dialog_message">This app requires the System Alert Window permission to display content on top of other apps. Please grant the permission.</string>
<string name="permission_dialog_positive_button">Allow</string> <string name="permission_dialog_positive_button">Allow</string>
<string name="permission_dialog_negative_button">Cancel</string> <string name="permission_dialog_negative_button">Cancel</string>
<string name="waiting_for_media">Waiting for media</string>
</resources> </resources>