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

Added QR code.

This commit is contained in:
Koen 2023-12-03 15:12:27 +01:00
parent eed50283bb
commit b339f4f487
6 changed files with 336 additions and 37 deletions

View file

@ -84,6 +84,7 @@ dependencies {
implementation "org.jetbrains.kotlinx:kotlinx-serialization-json:1.5.0"
implementation 'com.google.android.exoplayer:exoplayer:2.18.6'
implementation "com.squareup.okhttp3:okhttp:4.11.0"
implementation 'com.journeyapps:zxing-android-embedded:4.3.0'
testImplementation 'junit:junit:4.13.2'
androidTestImplementation 'androidx.test.ext:junit:1.1.5'
androidTestImplementation 'androidx.test.espresso:espresso-core:3.5.1'

View file

@ -24,7 +24,6 @@
<activity
android:name=".MainActivity"
android:configChanges="keyboardHidden|orientation|screenSize"
android:theme="@style/Theme.Main"
android:exported="true">
<intent-filter>

View file

@ -12,7 +12,9 @@ import android.net.Uri
import android.os.Build
import android.os.Bundle
import android.provider.Settings
import android.util.Base64
import android.util.Log
import android.util.TypedValue
import android.view.View
import android.view.WindowManager
import android.widget.*
@ -24,7 +26,11 @@ 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 com.google.zxing.BarcodeFormat
import com.journeyapps.barcodescanner.BarcodeEncoder
import kotlinx.coroutines.*
import kotlinx.serialization.encodeToString
import kotlinx.serialization.json.Json
import okhttp3.OkHttpClient
import java.io.InputStream
import java.io.OutputStream
@ -42,6 +48,9 @@ class MainActivity : AppCompatActivity() {
private lateinit var _videoBackground: StyledPlayerView
private lateinit var _viewDemo: View
private lateinit var _player: ExoPlayer
private lateinit var _imageQr: ImageView
private lateinit var _textScanToConnect: TextView
private var _updateAvailable: Boolean? = null
private var _updating: Boolean = false
private var _demoClickCount = 0
private var _lastDemoToast: Toast? = null
@ -53,6 +62,12 @@ class MainActivity : AppCompatActivity() {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
if (savedInstanceState != null && savedInstanceState.containsKey("updateAvailable")) {
_updateAvailable = savedInstanceState.getBoolean("updateAvailable", false)
} else {
_updateAvailable = null
}
_buttonUpdate = findViewById(R.id.button_update)
_text = findViewById(R.id.text_dialog)
_textIPs = findViewById(R.id.text_ips)
@ -62,6 +77,8 @@ class MainActivity : AppCompatActivity() {
_layoutConnectionInfo = findViewById(R.id.layout_connection_info)
_videoBackground = findViewById(R.id.video_background)
_viewDemo = findViewById(R.id.view_demo)
_imageQr = findViewById(R.id.image_qr)
_textScanToConnect = findViewById(R.id.text_scan_to_connect)
startVideo()
startAnimations()
@ -105,13 +122,28 @@ class MainActivity : AppCompatActivity() {
}
}
_textIPs.text = "IPs\n" + getIPs().joinToString("\n") + "\n\nPort\n46899"
val ips = getIPs()
_textIPs.text = "IPs\n" + ips.joinToString("\n") + "\n\nPort\n46899"
try {
val barcodeEncoder = BarcodeEncoder()
val px = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 100.0f, resources.displayMetrics).toInt()
val json = Json.encodeToString(FCastNetworkConfig(ips, listOf(
FCastService(46899, 0)
)))
val base64 = Base64.encode(json.toByteArray(), Base64.URL_SAFE or Base64.NO_PADDING or Base64.NO_WRAP)
val url = "fcast://r/${base64}"
val bitmap = barcodeEncoder.encodeBitmap(url, BarcodeFormat.QR_CODE, px, px)
_imageQr.setImageBitmap(bitmap)
} catch (e: java.lang.Exception) {
_textScanToConnect.visibility = View.GONE
_imageQr.visibility = View.GONE
}
TcpListenerService.activityCount++
if (checkAndRequestPermissions()) {
Log.i(TAG, "Notification permission already granted")
restartService()
} else {
checkAndRequestPermissions()
if (savedInstanceState == null) {
restartService()
}
@ -138,6 +170,11 @@ class MainActivity : AppCompatActivity() {
TcpListenerService.activityCount--
}
override fun onSaveInstanceState(outState: Bundle) {
super.onSaveInstanceState(outState)
_updateAvailable?.let { outState.putBoolean("updateAvailable", it) }
}
private fun restartService() {
val i = TcpListenerService.instance
if (i != null) {
@ -294,52 +331,59 @@ class MainActivity : AppCompatActivity() {
private suspend fun checkForUpdates() {
Log.i(TAG, "Checking for updates...")
withContext(Dispatchers.IO) {
try {
val latestVersion = downloadVersionCode()
val updateAvailable = _updateAvailable
if (updateAvailable != null) {
setUpdateAvailable(updateAvailable)
} else {
withContext(Dispatchers.IO) {
try {
val latestVersion = downloadVersionCode()
if (latestVersion != null) {
val currentVersion = BuildConfig.VERSION_CODE
Log.i(TAG, "Current version $currentVersion latest version $latestVersion.")
if (latestVersion != null) {
val currentVersion = BuildConfig.VERSION_CODE
Log.i(TAG, "Current version $currentVersion latest version $latestVersion.")
if (latestVersion > currentVersion) {
withContext(Dispatchers.Main) {
try {
(_updateSpinner.drawable as Animatable?)?.stop()
_updateSpinner.visibility = View.INVISIBLE
setText(resources.getText(R.string.there_is_an_update_available_do_you_wish_to_update))
_buttonUpdate.visibility = View.VISIBLE
} catch (e: Throwable) {
Toast.makeText(this@MainActivity, "Failed to show update dialog", Toast.LENGTH_LONG).show()
Log.w(TAG, "Error occurred in update dialog.")
}
setUpdateAvailable(latestVersion > currentVersion)
}
} else {
Log.w(TAG, "Failed to retrieve version from version URL.")
withContext(Dispatchers.Main) {
_updateSpinner.visibility = View.INVISIBLE
_buttonUpdate.visibility = View.INVISIBLE
//setText(getString(R.string.no_updates_available))
setText(null)
//Toast.makeText(this@MainActivity, "Already on latest version", Toast.LENGTH_LONG).show()
Toast.makeText(this@MainActivity, "Failed to retrieve version", Toast.LENGTH_LONG).show()
}
}
} else {
Log.w(TAG, "Failed to retrieve version from version URL.")
} catch (e: Throwable) {
Log.w(TAG, "Failed to check for updates.", e)
withContext(Dispatchers.Main) {
Toast.makeText(this@MainActivity, "Failed to retrieve version", Toast.LENGTH_LONG).show()
Toast.makeText(this@MainActivity, "Failed to check for updates", Toast.LENGTH_LONG).show()
}
}
} catch (e: Throwable) {
Log.w(TAG, "Failed to check for updates.", e)
withContext(Dispatchers.Main) {
Toast.makeText(this@MainActivity, "Failed to check for updates", Toast.LENGTH_LONG).show()
}
}
}
}
private fun setUpdateAvailable(updateAvailable: Boolean) {
if (updateAvailable) {
try {
(_updateSpinner.drawable as Animatable?)?.stop()
_updateSpinner.visibility = View.INVISIBLE
setText(resources.getText(R.string.there_is_an_update_available_do_you_wish_to_update))
_buttonUpdate.visibility = View.VISIBLE
} catch (e: Throwable) {
Toast.makeText(this@MainActivity, "Failed to show update dialog", Toast.LENGTH_LONG).show()
Log.w(TAG, "Error occurred in update dialog.")
}
} else {
_updateSpinner.visibility = View.INVISIBLE
_buttonUpdate.visibility = View.INVISIBLE
setText(null)
}
_updateAvailable = updateAvailable
}
private fun downloadVersionCode(): Int? {
val client = OkHttpClient()
val request = okhttp3.Request.Builder()

View file

@ -0,0 +1,15 @@
package com.futo.fcast.receiver
import kotlinx.serialization.Serializable
@Serializable
data class FCastNetworkConfig(
val ips: List<String>,
val services: List<FCastService>
)
@Serializable
data class FCastService(
val port: Int,
val type: Int
)

View file

@ -0,0 +1,215 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="#000000">
<com.google.android.exoplayer2.ui.StyledPlayerView
android:id="@+id/video_background"
app:use_controller="false"
android:layout_width="match_parent"
android:layout_height="match_parent"
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
android:id="@+id/text_title"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="FCast"
android:textColor="@color/white"
android:gravity="center"
android:fontFamily="@font/inter_bold"
android:textSize="40sp"
android:includeFontPadding="false"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintBottom_toTopOf="@id/layout_connection_info"/>
<androidx.constraintlayout.widget.ConstraintLayout
android:id="@+id/layout_connection_info"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="vertical"
android:background="@drawable/background_manual_connect"
android:paddingStart="25dp"
android:paddingEnd="25dp"
android:paddingTop="25dp"
android:paddingBottom="25dp"
app:layout_constraintTop_toBottomOf="@id/text_title"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintBottom_toTopOf="@id/text_dialog">
<TextView
android:id="@+id/text_waiting_for_connection"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:textColor="@color/white"
android:gravity="center"
android:fontFamily="@font/inter_regular"
android:textSize="14sp"
android:text="Waiting for a connection"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintBottom_toTopOf="@id/text_manual_connection_information" />
<ImageView
android:id="@+id/image_spinner"
android:layout_width="30dp"
android:layout_height="30dp"
android:layout_marginStart="8dp"
app:srcCompat="@drawable/ic_loader_animated"
app:layout_constraintTop_toTopOf="@id/text_waiting_for_connection"
app:layout_constraintBottom_toBottomOf="@id/text_waiting_for_connection"
app:layout_constraintLeft_toRightOf="@id/text_waiting_for_connection" />
<TextView
android:id="@+id/text_manual_connection_information"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:textColor="#919191"
android:gravity="center"
android:fontFamily="@font/inter_light"
android:textSize="12sp"
android:text="Manual connection information"
android:layout_marginTop="20dp"
app:layout_constraintTop_toBottomOf="@id/text_waiting_for_connection"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toStartOf="@id/text_scan_to_connect"
app:layout_constraintBottom_toTopOf="parent" />
<TextView
android:id="@+id/text_ips"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:textColor="@color/white"
android:gravity="center"
android:fontFamily="@font/inter_regular"
android:textSize="14sp"
tools:text="IPs\n192.168.1.3\n\nPort\n46899"
android:layout_marginTop="20dp"
app:layout_constraintTop_toBottomOf="@id/text_manual_connection_information"
app:layout_constraintStart_toStartOf="@id/text_manual_connection_information"
app:layout_constraintEnd_toEndOf="@id/text_manual_connection_information"
app:layout_constraintBottom_toTopOf="@id/text_automatic_discovery" />
<ImageView
android:id="@+id/image_qr"
android:layout_width="100dp"
android:layout_height="100dp"
app:layout_constraintTop_toTopOf="@id/text_ips"
app:layout_constraintStart_toStartOf="@id/text_scan_to_connect"
app:layout_constraintEnd_toEndOf="@id/text_scan_to_connect"
app:layout_constraintBottom_toBottomOf="@id/text_ips" />
<TextView
android:id="@+id/text_scan_to_connect"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:textColor="#919191"
android:gravity="center"
android:fontFamily="@font/inter_regular"
android:textSize="12sp"
android:text="Scan to connect"
android:layout_marginStart="50dp"
app:layout_constraintTop_toTopOf="@id/text_manual_connection_information"
app:layout_constraintBottom_toBottomOf="@id/text_manual_connection_information"
app:layout_constraintStart_toEndOf="@id/text_manual_connection_information"
app:layout_constraintEnd_toEndOf="parent" />
<TextView
android:id="@+id/text_automatic_discovery"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:textColor="#919191"
android:gravity="center"
android:fontFamily="@font/inter_regular"
android:textSize="12sp"
android:text="Automatic discovery is available via mDNS"
android:layout_marginTop="20dp"
app:layout_constraintTop_toBottomOf="@id/text_ips"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintBottom_toBottomOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>
<TextView
android:id="@+id/text_dialog"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/there_is_an_update_available_do_you_wish_to_update"
android:textSize="14sp"
android:maxLines="2"
android:ellipsize="end"
android:textColor="@color/white"
android:fontFamily="@font/inter_regular"
android:paddingStart="30dp"
android:gravity="center"
app:layout_constraintTop_toBottomOf="@id/layout_connection_info"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintBottom_toTopOf="@id/button_update" />
<LinearLayout
android:id="@+id/button_update"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="@drawable/background_button_primary"
android:layout_marginEnd="2dp"
android:clickable="true"
app:layout_constraintTop_toBottomOf="@id/text_dialog"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintBottom_toBottomOf="parent">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/update"
android:textSize="14sp"
android:textColor="@color/white"
android:fontFamily="@font/inter_regular"
android:paddingTop="10dp"
android:paddingBottom="10dp"
android:paddingStart="28dp"
android:paddingEnd="28dp"/>
</LinearLayout>
<FrameLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="8dp"
app:layout_constraintStart_toEndOf="@id/text_dialog"
app:layout_constraintTop_toTopOf="@id/text_dialog"
app:layout_constraintBottom_toBottomOf="@id/text_dialog">
<ImageView
android:id="@+id/update_spinner"
android:layout_width="40dp"
android:layout_height="40dp"
app:srcCompat="@drawable/ic_update_animated" />
<TextView
android:id="@+id/text_progress"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text=""
android:layout_gravity="center"
android:textSize="10sp"
android:textColor="@color/white"
android:fontFamily="@font/inter_regular" />
</FrameLayout>
</androidx.constraintlayout.widget.ConstraintLayout>

View file

@ -118,7 +118,32 @@
app:layout_constraintTop_toBottomOf="@id/text_ips"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintBottom_toBottomOf="parent" />
app:layout_constraintBottom_toTopOf="@id/image_qr"/>
<ImageView
android:id="@+id/image_qr"
android:layout_width="100dp"
android:layout_height="100dp"
android:layout_marginTop="20dp"
app:layout_constraintTop_toBottomOf="@id/text_automatic_discovery"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintBottom_toTopOf="@id/text_scan_to_connect" />
<TextView
android:id="@+id/text_scan_to_connect"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:textColor="#919191"
android:gravity="center"
android:fontFamily="@font/inter_regular"
android:textSize="12sp"
android:text="Scan to connect"
android:layout_marginTop="8dp"
app:layout_constraintTop_toBottomOf="@id/image_qr"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintBottom_toBottomOf="parent"/>
</androidx.constraintlayout.widget.ConstraintLayout>
<TextView