diff --git a/receivers/android/app/build.gradle b/receivers/android/app/build.gradle index 91bd0d3..e32d7c5 100644 --- a/receivers/android/app/build.gradle +++ b/receivers/android/app/build.gradle @@ -85,10 +85,13 @@ dependencies { implementation 'androidx.appcompat:appcompat:1.6.1' implementation 'com.google.android.material:material:1.11.0' 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.journeyapps:zxing-android-embedded:4.3.0' 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' androidTestImplementation 'androidx.test.ext:junit:1.1.5' androidTestImplementation 'androidx.test.espresso:espresso-core:3.5.1' diff --git a/receivers/android/app/src/main/java/com/futo/fcast/receiver/BootReceiver.kt b/receivers/android/app/src/main/java/com/futo/fcast/receiver/BootReceiver.kt index 1a5a801..b77471f 100644 --- a/receivers/android/app/src/main/java/com/futo/fcast/receiver/BootReceiver.kt +++ b/receivers/android/app/src/main/java/com/futo/fcast/receiver/BootReceiver.kt @@ -31,6 +31,7 @@ class BootReceiver : BroadcastReceiver() { } } + @Suppress("DEPRECATION") private fun createNotificationBuilder(context: Context): NotificationCompat.Builder { return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { NotificationCompat.Builder(context, CHANNEL_ID) diff --git a/receivers/android/app/src/main/java/com/futo/fcast/receiver/CustomStyledPlayerView.kt b/receivers/android/app/src/main/java/com/futo/fcast/receiver/CustomStyledPlayerView.kt index 06e9f61..ea4be8b 100644 --- a/receivers/android/app/src/main/java/com/futo/fcast/receiver/CustomStyledPlayerView.kt +++ b/receivers/android/app/src/main/java/com/futo/fcast/receiver/CustomStyledPlayerView.kt @@ -2,7 +2,6 @@ package com.futo.fcast.receiver import android.content.Context import android.util.AttributeSet -import android.view.KeyEvent -import com.google.android.exoplayer2.ui.StyledPlayerView +import androidx.media3.ui.PlayerView -class CustomStyledPlayerView(context: Context, attrs: AttributeSet? = null) : StyledPlayerView(context, attrs) { } \ No newline at end of file +class CustomPlayerView(context: Context, attrs: AttributeSet? = null) : PlayerView(context, attrs) { } \ No newline at end of file diff --git a/receivers/android/app/src/main/java/com/futo/fcast/receiver/InstallReceiver.kt b/receivers/android/app/src/main/java/com/futo/fcast/receiver/InstallReceiver.kt index 3e3759c..24e8a8e 100644 --- a/receivers/android/app/src/main/java/com/futo/fcast/receiver/InstallReceiver.kt +++ b/receivers/android/app/src/main/java/com/futo/fcast/receiver/InstallReceiver.kt @@ -4,6 +4,7 @@ import android.content.BroadcastReceiver import android.content.Context import android.content.Intent import android.content.pm.PackageInstaller +import android.os.Build import android.util.Log class InstallReceiver : BroadcastReceiver() { @@ -13,7 +14,13 @@ class InstallReceiver : BroadcastReceiver() { when (status) { PackageInstaller.STATUS_PENDING_USER_ACTION -> { - val activityIntent = intent.getParcelableExtra(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) { Log.w(TAG, "Received STATUS_PENDING_USER_ACTION and activity intent is null.") return diff --git a/receivers/android/app/src/main/java/com/futo/fcast/receiver/MainActivity.kt b/receivers/android/app/src/main/java/com/futo/fcast/receiver/MainActivity.kt index 7a9bb96..2339f06 100644 --- a/receivers/android/app/src/main/java/com/futo/fcast/receiver/MainActivity.kt +++ b/receivers/android/app/src/main/java/com/futo/fcast/receiver/MainActivity.kt @@ -2,6 +2,7 @@ package com.futo.fcast.receiver import WebSocketListenerService import android.Manifest +import android.app.Activity import android.app.AlertDialog import android.app.PendingIntent import android.content.Context @@ -19,14 +20,17 @@ import android.util.TypedValue import android.view.View import android.view.WindowManager import android.widget.* +import androidx.activity.result.ActivityResultLauncher +import androidx.activity.result.contract.ActivityResultContracts import androidx.appcompat.app.AppCompatActivity import androidx.constraintlayout.widget.ConstraintLayout import androidx.core.app.ActivityCompat import androidx.core.content.ContextCompat -import com.google.android.exoplayer2.ExoPlayer -import com.google.android.exoplayer2.MediaItem -import com.google.android.exoplayer2.Player -import com.google.android.exoplayer2.ui.StyledPlayerView +import androidx.lifecycle.lifecycleScope +import androidx.media3.common.MediaItem +import androidx.media3.common.Player +import androidx.media3.exoplayer.ExoPlayer +import androidx.media3.ui.PlayerView import com.google.zxing.BarcodeFormat import com.journeyapps.barcodescanner.BarcodeEncoder import kotlinx.coroutines.* @@ -46,27 +50,38 @@ class MainActivity : AppCompatActivity() { private lateinit var _updateSpinner: ImageView private lateinit var _imageSpinner: ImageView private lateinit var _layoutConnectionInfo: ConstraintLayout - private lateinit var _videoBackground: StyledPlayerView + private lateinit var _videoBackground: PlayerView private lateinit var _viewDemo: View private lateinit var _player: ExoPlayer private lateinit var _imageQr: ImageView private lateinit var _textScanToConnect: TextView + private lateinit var _systemAlertWindowPermissionLauncher: ActivityResultLauncher private var _updateAvailable: Boolean? = null private var _updating: Boolean = false private var _demoClickCount = 0 private var _lastDemoToast: Toast? = null private val _preferenceFileKey get() = "$packageName.PREFERENCE_FILE_KEY" - private val _scope: CoroutineScope = CoroutineScope(Dispatchers.Main) - override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_main) - if (savedInstanceState != null && savedInstanceState.containsKey("updateAvailable")) { - _updateAvailable = savedInstanceState.getBoolean("updateAvailable", false) + _systemAlertWindowPermissionLauncher = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { _ -> + 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 { - _updateAvailable = null + null } _buttonUpdate = findViewById(R.id.button_update) @@ -118,13 +133,13 @@ class MainActivity : AppCompatActivity() { _updateSpinner.visibility = View.VISIBLE (_updateSpinner.drawable as Animatable?)?.start() - _scope.launch(Dispatchers.IO) { + lifecycleScope.launch(Dispatchers.IO) { checkForUpdates() } } 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 { val barcodeEncoder = BarcodeEncoder() @@ -168,7 +183,6 @@ class MainActivity : AppCompatActivity() { override fun onDestroy() { super.onDestroy() InstallReceiver.onReceiveResult = null - _scope.cancel() _player.release() NetworkService.activityCount-- } @@ -179,11 +193,7 @@ class MainActivity : AppCompatActivity() { } private fun restartService() { - val i = NetworkService.instance - if (i != null) { - i.stopSelf() - } - + NetworkService.instance?.stopSelf() startService(Intent(this, NetworkService::class.java)) } @@ -245,7 +255,7 @@ class MainActivity : AppCompatActivity() { .setPositiveButton(R.string.permission_dialog_positive_button) { _, _ -> try { val intent = Intent(Settings.ACTION_MANAGE_OVERLAY_PERMISSION, Uri.parse("package:$packageName")) - startActivityForResult(intent, REQUEST_CODE) + _systemAlertWindowPermissionLauncher.launch(intent) } catch (e: Throwable) { Log.e("OverlayPermission", "Error requesting overlay permission", e) 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, grantResults: IntArray) { super.onRequestPermissionsResult(requestCode, permissions, grantResults) @@ -410,7 +403,7 @@ class MainActivity : AppCompatActivity() { setText(resources.getText(R.string.downloading_update)) (_updateSpinner.drawable as Animatable?)?.start() - _scope.launch(Dispatchers.IO) { + lifecycleScope.launch(Dispatchers.IO) { var inputStream: InputStream? = null try { val client = OkHttpClient() @@ -458,8 +451,7 @@ class MainActivity : AppCompatActivity() { if (lastProgressText != progressText) { lastProgressText = progressText - //TODO: Use proper scope - GlobalScope.launch(Dispatchers.Main) { + lifecycleScope.launch(Dispatchers.Main) { _textProgress.text = progressText } } @@ -504,7 +496,7 @@ class MainActivity : AppCompatActivity() { (_updateSpinner.drawable as Animatable?)?.stop() - if (result == null || result.isBlank()) { + if (result.isNullOrBlank()) { _updateSpinner.setImageResource(R.drawable.ic_update_success) setText(resources.getText(R.string.success)) } else { diff --git a/receivers/android/app/src/main/java/com/futo/fcast/receiver/NetworkService.kt b/receivers/android/app/src/main/java/com/futo/fcast/receiver/NetworkService.kt index c8f69c3..e4237a9 100644 --- a/receivers/android/app/src/main/java/com/futo/fcast/receiver/NetworkService.kt +++ b/receivers/android/app/src/main/java/com/futo/fcast/receiver/NetworkService.kt @@ -11,6 +11,8 @@ import android.util.Log import android.widget.Toast import androidx.core.app.NotificationCompat import kotlinx.coroutines.* +import kotlinx.serialization.encodeToString +import kotlinx.serialization.json.Json class NetworkService : Service() { private var _discoveryService: DiscoveryService? = null @@ -208,11 +210,7 @@ class NetworkService : Service() { if (PlayerActivity.instance == null) { val i = Intent(this@NetworkService, PlayerActivity::class.java) i.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) - i.putExtra("container", playMessage.container) - i.putExtra("url", playMessage.url) - i.putExtra("content", playMessage.content) - i.putExtra("time", playMessage.time) - i.putExtra("speed", playMessage.speed) + i.putExtra("message", Json.encodeToString(playMessage)) if (activityCount > 0) { startActivity(i) diff --git a/receivers/android/app/src/main/java/com/futo/fcast/receiver/PlayerActivity.kt b/receivers/android/app/src/main/java/com/futo/fcast/receiver/PlayerActivity.kt index afec445..ce6c259 100644 --- a/receivers/android/app/src/main/java/com/futo/fcast/receiver/PlayerActivity.kt +++ b/receivers/android/app/src/main/java/com/futo/fcast/receiver/PlayerActivity.kt @@ -16,25 +16,28 @@ import android.view.WindowInsets import android.view.WindowManager import android.widget.ImageView import android.widget.TextView +import androidx.annotation.OptIn import androidx.appcompat.app.AppCompatActivity import androidx.constraintlayout.widget.ConstraintLayout -import com.google.android.exoplayer2.ExoPlayer -import com.google.android.exoplayer2.MediaItem -import com.google.android.exoplayer2.PlaybackException -import com.google.android.exoplayer2.PlaybackParameters -import com.google.android.exoplayer2.Player -import com.google.android.exoplayer2.source.DefaultMediaSourceFactory -import com.google.android.exoplayer2.source.dash.DashMediaSource -import com.google.android.exoplayer2.source.hls.HlsMediaSource -import com.google.android.exoplayer2.trackselection.DefaultTrackSelector -import com.google.android.exoplayer2.ui.StyledPlayerView -import com.google.android.exoplayer2.upstream.DefaultDataSource -import com.google.android.exoplayer2.upstream.DefaultHttpDataSource -import com.google.android.exoplayer2.upstream.HttpDataSource +import androidx.media3.common.MediaItem +import androidx.media3.common.PlaybackException +import androidx.media3.common.PlaybackParameters +import androidx.media3.common.Player +import androidx.media3.common.util.UnstableApi +import androidx.media3.datasource.DefaultDataSource +import androidx.media3.datasource.DefaultHttpDataSource +import androidx.media3.datasource.HttpDataSource +import androidx.media3.exoplayer.ExoPlayer +import androidx.media3.exoplayer.dash.DashMediaSource +import androidx.media3.exoplayer.hls.HlsMediaSource +import androidx.media3.exoplayer.source.DefaultMediaSourceFactory +import androidx.media3.exoplayer.trackselection.DefaultTrackSelector +import androidx.media3.ui.PlayerView import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.cancel import kotlinx.coroutines.launch +import kotlinx.serialization.json.Json import java.io.File import java.io.FileOutputStream import kotlin.math.abs @@ -42,7 +45,7 @@ import kotlin.math.max class PlayerActivity : AppCompatActivity() { - private lateinit var _playerControlView: StyledPlayerView + private lateinit var _playerControlView: PlayerView private lateinit var _imageSpinner: ImageView private lateinit var _textMessage: TextView private lateinit var _layoutOverlay: ConstraintLayout @@ -165,6 +168,7 @@ class PlayerActivity : AppCompatActivity() { } } + @OptIn(UnstableApi::class) override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) Log.i(TAG, "onCreate") @@ -203,13 +207,15 @@ class PlayerActivity : AppCompatActivity() { .build() _connectivityManager.registerNetworkCallback(netReq, _connectivityEvents) - val container = intent.getStringExtra("container") ?: "" - val url = intent.getStringExtra("url") - val content = intent.getStringExtra("content") - val time = intent.getDoubleExtra("time", 0.0) - val speed = intent.getDoubleExtra("speed", 1.0) - - play(PlayMessage(container, url, content, time, speed)) + val playMessage = intent.getStringExtra("message")?.let { + try { + Json.decodeFromString(it) + } catch (e: Throwable) { + Log.i(TAG, "Failed to deserialize play message.", e) + null + } + } + playMessage?.let { play(it) } instance = this NetworkService.activityCount++ @@ -290,6 +296,7 @@ class PlayerActivity : AppCompatActivity() { NetworkService.activityCount-- } + @OptIn(UnstableApi::class) override fun dispatchKeyEvent(event: KeyEvent): Boolean { if (_playerControlView.isControllerFullyVisible) { if (event.keyCode == KeyEvent.KEYCODE_BACK) { @@ -312,6 +319,7 @@ class PlayerActivity : AppCompatActivity() { return super.dispatchKeyEvent(event) } + @OptIn(UnstableApi::class) fun play(playMessage: PlayMessage) { val mediaItemBuilder = MediaItem.Builder() if (playMessage.container.isNotEmpty()) { diff --git a/receivers/android/app/src/main/res/layout-land/activity_main.xml b/receivers/android/app/src/main/res/layout-land/activity_main.xml index 5789868..e7f6106 100644 --- a/receivers/android/app/src/main/res/layout-land/activity_main.xml +++ b/receivers/android/app/src/main/res/layout-land/activity_main.xml @@ -7,7 +7,7 @@ android:layout_height="match_parent" android:background="#000000"> - - - This app requires the System Alert Window permission to display content on top of other apps. Please grant the permission. Allow Cancel + Waiting for media \ No newline at end of file