Added intent filter for video/image files

Signed-off-by: Balazs Toldi <balazs@toldi.eu>
This commit is contained in:
Balazs Toldi 2022-01-03 23:30:55 +01:00
parent 5933cd2b8a
commit 8865f2c5b4
Signed by: Bazsalanszky
GPG key ID: 933820884952BE27
10 changed files with 351 additions and 85 deletions

View file

@ -42,6 +42,15 @@
<entry key="../../../../../layout/compose-model-1641156419059.xml" value="0.5" />
<entry key="../../../../../layout/compose-model-1641157410658.xml" value="1.0" />
<entry key="../../../../../layout/compose-model-1641157605359.xml" value="0.5" />
<entry key="../../../../../layout/compose-model-1641158311201.xml" value="1.0" />
<entry key="../../../../../layout/compose-model-1641198484109.xml" value="0.30743243243243246" />
<entry key="../../../../../layout/compose-model-1641204990382.xml" value="0.1" />
<entry key="../../../../../layout/compose-model-1641204995500.xml" value="0.1" />
<entry key="../../../../../layout/compose-model-1641204997625.xml" value="0.44814814814814813" />
<entry key="../../../../../layout/compose-model-1641219750398.xml" value="0.25" />
<entry key="../../../../../layout/compose-model-1641222972102.xml" value="0.46296296296296297" />
<entry key="../../../../../layout/compose-model-1641224248444.xml" value="0.46296296296296297" />
<entry key="../../../../../layout/compose-model-1641226030069.xml" value="0.46296296296296297" />
</map>
</option>
</component>

View file

@ -4,6 +4,8 @@ plugins {
}
android {
compileSdk 31
defaultConfig {
@ -56,6 +58,10 @@ android {
kotlinCompilerVersion '1.5.21'
}
packagingOptions {
exclude 'META-INF/*'
// Due to https://github.com/Kotlin/kotlinx.coroutines/issues/2023
exclude 'META-INF/licenses/*'
exclude '**/attach_hotspot_windows.dll'
resources {
excludes += '/META-INF/{AL2.0,LGPL2.1}'
}
@ -82,6 +88,10 @@ dependencies {
implementation 'com.github.yausername.youtubedl-android:library:0.13.+'
implementation 'com.github.yausername.youtubedl-android:ffmpeg:0.13.+'
implementation "androidx.media:media:1.4.3"
implementation 'io.ktor:ktor-server-core:1.6.2'
implementation 'io.ktor:ktor-server-jetty:1.6.2'
implementation 'io.ktor:ktor-websockets:1.6.2'
testImplementation 'junit:junit:4.+'
androidTestImplementation 'androidx.test.ext:junit:1.1.3'
androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0'

View file

@ -27,6 +27,16 @@
<category android:name="android.intent.category.DEFAULT" />
<data android:mimeType="text/plain" />
</intent-filter>
<intent-filter>
<action android:name="android.intent.action.SEND" />
<category android:name="android.intent.category.DEFAULT" />
<data android:mimeType="image/*" />
</intent-filter>
<intent-filter>
<action android:name="android.intent.action.SEND" />
<category android:name="android.intent.category.DEFAULT" />
<data android:mimeType="video/*" />
</intent-filter>
</activity>
<activity
android:name=".ChromecastManagerActivity"

View file

@ -11,6 +11,8 @@ import kotlinx.coroutines.withContext
import su.litvak.chromecast.api.v2.Application
import su.litvak.chromecast.api.v2.ChromeCast
import su.litvak.chromecast.api.v2.MediaStatus
import java.net.InetAddress
import java.net.NetworkInterface
object ChromeCastHelper {
@ -161,5 +163,14 @@ object ChromeCastHelper {
}
return exitStatus
}
fun getIPv4Address(): InetAddress? {
NetworkInterface.getNetworkInterfaces().toList().forEach { interf ->
interf.inetAddresses.toList().forEach { inetAddress ->
if (!inetAddress.isLoopbackAddress && inetAddress.hostAddress.indexOf(':') < 0) {
return inetAddress
}
}
}
return null
}
}

View file

@ -1,12 +1,19 @@
package eu.toldi.balazs.caster
import android.annotation.SuppressLint
import android.content.Intent
import android.database.Cursor
import android.net.Uri
import android.os.Build
import android.os.Bundle
import android.provider.OpenableColumns
import android.util.Log
import android.view.KeyEvent
import android.webkit.MimeTypeMap
import androidx.activity.ComponentActivity
import androidx.activity.compose.rememberLauncherForActivityResult
import androidx.activity.compose.setContent
import androidx.activity.result.contract.ActivityResultContracts
import androidx.annotation.RequiresApi
import androidx.compose.foundation.Image
import androidx.compose.foundation.layout.*
@ -25,12 +32,16 @@ import coil.compose.rememberImagePainter
import com.yausername.ffmpeg.FFmpeg
import com.yausername.youtubedl_android.YoutubeDL
import com.yausername.youtubedl_android.YoutubeDLException
import eu.toldi.balazs.caster.helpers.FileCacheHelper
import eu.toldi.balazs.caster.model.ChromecastManageViewmodel
import eu.toldi.balazs.caster.services.ChromecastManagerService
import eu.toldi.balazs.caster.ui.theme.CasterTheme
import su.litvak.chromecast.api.v2.ChromeCast
import su.litvak.chromecast.api.v2.Media
import su.litvak.chromecast.api.v2.MediaStatus
import java.io.BufferedInputStream
import java.io.BufferedOutputStream
import java.io.FileOutputStream
class ChromecastManagerActivity : ComponentActivity() {
@ -152,37 +163,50 @@ class ChromecastManagerActivity : ComponentActivity() {
modifier = Modifier.fillMaxWidth()
)
}
Row(
Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceBetween
) {
Text(
text = String.format(
"%02d:%02d",
((mediaStatus.currentTime % 3600) / 60).toInt(),
(mediaStatus.currentTime % 60).toInt()
if(mediaStatus.media.contentType.startsWith("video/")) {
Row(
Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceBetween
) {
Text(
text = String.format(
"%02d:%02d",
((mediaStatus.currentTime % 3600) / 60).toInt(),
(mediaStatus.currentTime % 60).toInt()
)
)
)
Text(
text = String.format(
"%02d:%02d",
((mediaStatus.media.duration % 3600) / 60).toInt(),
(mediaStatus.media.duration % 60).toInt()
Text(
text = String.format(
"%02d:%02d",
((mediaStatus.media.duration % 3600) / 60).toInt(),
(mediaStatus.media.duration % 60).toInt()
)
)
)
}
Slider(value = sliderPosition, onValueChange = {
sliderPosition = it
sliderMoving = true
}, onValueChangeFinished = {
viewModel.seek(sliderPosition.toDouble())
sliderMoving = false
})
}
Slider(value = sliderPosition, onValueChange = {
sliderPosition = it
sliderMoving = true
}, onValueChangeFinished = {
viewModel.seek(sliderPosition.toDouble())
sliderMoving = false
})
}
}
@Composable
fun MenuBar() {
val pickPictureLauncher = rememberLauncherForActivityResult(
ActivityResultContracts.OpenDocument()
) { fileUri ->
if (fileUri != null) {
val cacheHelper = FileCacheHelper(applicationContext)
cacheHelper.cacheThis(listOf(fileUri))
viewModel.castFromCache(cacheHelper.tryFileName(fileUri))
}
}
TopAppBar(
title = {
Text("Caster")
@ -197,7 +221,14 @@ class ChromecastManagerActivity : ComponentActivity() {
},
actions = {
Row {
IconButton(onClick = {
pickPictureLauncher.launch(arrayOf("image/*", "video/*"))
}) {
Icon(
Icons.Filled.Folder,
contentDescription = stringResource(id = R.string.back)
)
}
}
}
)

View file

@ -32,11 +32,13 @@ import eu.toldi.balazs.caster.ui.theme.CasterTheme
import su.litvak.chromecast.api.v2.ChromeCast
class MainActivity : ComponentActivity() {
open class MainActivity : ComponentActivity() {
lateinit var viewModel: ChromeCastViewModel
private var multicastLock: MulticastLock? = null
protected lateinit var viewModel: ChromeCastViewModel
protected var multicastLock: MulticastLock? = null
protected fun isViewModelInitialised() = ::viewModel.isInitialized
override fun onStart() {
super.onStart()
val wifi = applicationContext.getSystemService(WIFI_SERVICE) as WifiManager
@ -184,7 +186,11 @@ class MainActivity : ComponentActivity() {
@Composable
fun showChromeCastButton(chromeCast: ChromeCast) {
fun showChromeCastButton(chromeCast: ChromeCast,buttonCallBack: () -> Unit = {
ChromecastManagerActivity.chromeCast_ = chromeCast
val intent = Intent(applicationContext, ChromecastManagerActivity::class.java)
startActivity(intent)
}) {
Column(
Modifier
.fillMaxWidth()
@ -193,11 +199,9 @@ class MainActivity : ComponentActivity() {
Card(modifier = Modifier
.fillMaxWidth()
.clickable {
ChromecastManagerActivity.chromeCast_ = chromeCast
val intent = Intent(applicationContext, ChromecastManagerActivity::class.java)
startActivity(intent)
buttonCallBack()
}) {
Row() {
Row {
val image_id = when (chromeCast.model) {
"Chromecast Ultra" -> R.drawable.chromecastultra
else -> R.drawable.chromecastv1
@ -217,7 +221,8 @@ class MainActivity : ComponentActivity() {
)
}
Column() {
Column(modifier = Modifier.fillMaxWidth().height(80.dp),
verticalArrangement = Arrangement.Center) {
Text(
text = "Name: " + when {
chromeCast.title != null -> chromeCast.title
@ -267,7 +272,7 @@ class MainActivity : ComponentActivity() {
@Preview(showBackground = true)
@Composable
fun DefaultPreview() {
open fun DefaultPreview() {
CasterTheme {
Column {
MenuBar({}, {})

View file

@ -2,8 +2,10 @@ package eu.toldi.balazs.caster
import android.annotation.SuppressLint
import android.content.Intent
import android.net.Uri
import android.net.wifi.WifiManager
import android.os.Bundle
import android.os.Parcelable
import android.util.Log
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
@ -19,49 +21,47 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import androidx.core.content.ContextCompat
import androidx.lifecycle.ViewModelProvider
import androidx.lifecycle.lifecycleScope
import com.yausername.youtubedl_android.YoutubeDL
import com.yausername.youtubedl_android.YoutubeDLException
import eu.toldi.balazs.caster.helpers.FileCacheHelper
import eu.toldi.balazs.caster.model.ChromeCastViewModel
import eu.toldi.balazs.caster.services.ChromecastManagerService
import eu.toldi.balazs.caster.ui.theme.CasterTheme
import kotlinx.coroutines.launch
import su.litvak.chromecast.api.v2.ChromeCast
class ShareRecieverActivity : ComponentActivity() {
lateinit var viewModel: ChromeCastViewModel
private var multicastLock: WifiManager.MulticastLock? = null
override fun onStart() {
super.onStart()
val wifi = applicationContext.getSystemService(WIFI_SERVICE) as WifiManager
// get the device ip address
multicastLock = wifi.createMulticastLock(javaClass.name)
multicastLock!!.setReferenceCounted(true)
multicastLock!!.acquire()
if (!this::viewModel.isInitialized)
viewModel = ViewModelProvider(this).get(ChromeCastViewModel::class.java)
viewModel.startScanning()
}
override fun onStop() {
super.onStop()
if (multicastLock != null) {
Log.i("Caster", "Releasing Mutlicast Lock...")
multicastLock!!.release()
multicastLock = null
}
viewModel.startScanning()
}
class ShareRecieverActivity : MainActivity() {
@SuppressLint("CoroutineCreationDuringComposition")
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
if (intent?.action != Intent.ACTION_SEND || intent.type != "text/plain") {
if (intent?.action != Intent.ACTION_SEND ) {
finish()
}
val link = intent.getStringExtra(Intent.EXTRA_TEXT) as String
Log.d(null, intent.type.toString())
val link = when {
intent.type == "text/plain" -> intent.getStringExtra(Intent.EXTRA_TEXT) as String
intent.type?.startsWith("video/") == true ||
intent.type?.startsWith("image/") == true -> {
val uri = (intent.getParcelableExtra<Parcelable>(Intent.EXTRA_STREAM) as? Uri)
if(uri is Uri) {
val fileCache = FileCacheHelper(applicationContext)
fileCache.cacheThis(listOf(uri))
"http://" + ChromeCastHelper.getIPv4Address()?.hostAddress + ":" + ChromecastManagerService.PORT + "/assets/" + fileCache.tryFileName(
uri
)
}else ""
}
else -> {
""
}
}
if (link == "") finish()
try {
YoutubeDL.getInstance().init(application)
} catch (e: YoutubeDLException) {
@ -69,7 +69,7 @@ class ShareRecieverActivity : ComponentActivity() {
}
setContent {
CasterTheme {
if (!this::viewModel.isInitialized)
if (!isViewModelInitialised())
viewModel = ViewModelProvider(this).get(ChromeCastViewModel::class.java)
viewModel.startScanning()
val chromeCastState = viewModel.chromeCasts.observeAsState(emptyList())
@ -110,8 +110,8 @@ class ShareRecieverActivity : ComponentActivity() {
link = link
)
}
item{
if(enabled.not()){
item {
if (enabled.not()) {
CircularProgressIndicator()
}
}
@ -130,25 +130,23 @@ class ShareRecieverActivity : ComponentActivity() {
chromeCast: ChromeCast,
link: String
) {
Button(
onClick = {
showChromeCastButton(chromeCast = chromeCast, buttonCallBack = {
if (enabled) {
onEnableChanged(false)
Intent(this, ChromecastManagerService::class.java).also {
it.action = ChromecastManagerService.ACTION_INIT
it.putExtra("CHROMECAST_ADDRESS", chromeCast.address)
it.putExtra("CHROMECAST_NAME", chromeCast.title)
ContextCompat.startForegroundService(this, it)
}
lifecycleScope.launch {
ChromeCastHelper.chromeCast = chromeCast
ChromeCastHelper.castLink(link) {
finish()
//finish()
}
}
},
modifier = Modifier.padding(5.dp),
enabled = enabled
) {
when {
chromeCast.title != null -> Text(text = chromeCast.title)
chromeCast.name != null -> Text(text = chromeCast.name)
else -> Text(text = chromeCast.address)
}
}
})
}
@Composable
@ -173,7 +171,7 @@ class ShareRecieverActivity : ComponentActivity() {
@Preview(showBackground = true)
@Composable
fun DefaultPreview() {
override fun DefaultPreview() {
CasterTheme {
MenuBar {}
}

View file

@ -0,0 +1,129 @@
package eu.toldi.balazs.caster.helpers
import android.annotation.SuppressLint
import android.content.Context
import android.database.Cursor
import android.net.Uri
import android.provider.OpenableColumns
import android.util.Log
import android.webkit.MimeTypeMap
import java.io.BufferedInputStream
import java.io.BufferedOutputStream
import java.io.File
import java.io.FileOutputStream
import java.util.concurrent.Executors
class FileCacheHelper(
private val mContext: Context
) {
// content resolver
private val contentResolver = mContext.contentResolver
// to get the type of file
private val mimeTypeMap = MimeTypeMap.getSingleton()
private val mCacheLocation = mContext.cacheDir
fun cacheThis(uris: List<Uri>) {
executor.submit {
uris.forEach { uri -> copyFromSource(uri) }
}
}
fun tryFileName(uri: Uri): String{
val fileExtension: String = getFileExtension(uri) ?: kotlin.run {
throw RuntimeException("Extension is null for $uri")
}
return queryName(uri) ?: getFileName(fileExtension)
}
/**
* Copies the actual data from provided content provider.
*/
private fun copyFromSource(uri: Uri) {
val fileExtension: String = getFileExtension(uri) ?: kotlin.run {
throw RuntimeException("Extension is null for $uri")
}
val fileName = queryName(uri) ?: getFileName(fileExtension)
val inputStream = contentResolver.openInputStream(uri) ?: kotlin.run {
throw RuntimeException("Cannot open for reading $uri")
}
val bufferedInputStream = BufferedInputStream(inputStream)
// the file which will be the new cached file
val outputFile = File(mCacheLocation, fileName)
val bufferedOutputStream = BufferedOutputStream(FileOutputStream(outputFile))
// this will hold the content for each iteration
val buffer = ByteArray(DEFAULT_BUFFER_SIZE)
var readBytes = 0 // will be -1 if reached the end of file
while (true) {
readBytes = bufferedInputStream.read(buffer)
// check if the read was failure
if (readBytes == -1) {
bufferedOutputStream.flush()
break
}
bufferedOutputStream.write(buffer)
bufferedOutputStream.flush()
}
Log.i("FileCache",outputFile.absoluteFile.toString())
// close everything
inputStream.close()
bufferedInputStream.close()
bufferedOutputStream.close()
}
private fun getFileExtension(uri: Uri): String? {
return mimeTypeMap.getExtensionFromMimeType(contentResolver.getType(uri))
}
/**
* Tries to get actual name of the file being copied.
* This might be required in some of the cases where you might want to know the file name too.
*
* @param uri
*
*/
@SuppressLint("Recycle")
private fun queryName(uri: Uri): String? {
val returnCursor: Cursor = contentResolver.query(uri, null, null, null, null) ?: return null
val nameIndex: Int = returnCursor.getColumnIndex(OpenableColumns.DISPLAY_NAME)
returnCursor.moveToFirst()
val name: String = returnCursor.getString(nameIndex)
returnCursor.close()
return name
}
private fun getFileName(fileExtension: String): String {
return "${System.currentTimeMillis().toString()}.$fileExtension"
}
/**
* Remove everything that we have cached.
* You might want to invoke this method before quiting the application.
*/
fun removeAll() {
mContext.cacheDir.deleteRecursively()
}
companion object {
// base buffer size
private const val BASE_BUFFER_SIZE = 1024
// if you want to modify size use binary multiplier 2, 4, 6, 8
private const val DEFAULT_BUFFER_SIZE = BASE_BUFFER_SIZE * 4
private val executor = Executors.newSingleThreadExecutor()
}
}

View file

@ -1,15 +1,19 @@
package eu.toldi.balazs.caster.model
import android.net.Uri
import android.util.Log
import androidx.compose.runtime.State
import androidx.compose.runtime.mutableStateOf
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import eu.toldi.balazs.caster.ChromeCastHelper
import eu.toldi.balazs.caster.services.ChromecastManagerService
import kotlinx.coroutines.Dispatchers.IO
import kotlinx.coroutines.launch
import su.litvak.chromecast.api.v2.ChromeCast
import su.litvak.chromecast.api.v2.MediaStatus
import java.net.InetAddress
import java.net.NetworkInterface
class ChromecastManageViewmodel : ViewModel() {
@ -60,6 +64,13 @@ class ChromecastManageViewmodel : ViewModel() {
}
}
fun castFromCache(fileName: String,callBack: () -> Unit = {}){
//Log.i("Caster","http://"+getIPv4Address()?.hostAddress+":"+ChromecastManagerService.PORT+"/assets/"+fileName)
viewModelScope.launch(IO) {
ChromeCastHelper.castLink("http://"+getIPv4Address()?.hostAddress+":"+ChromecastManagerService.PORT+"/assets/"+fileName,callBack)
}
}
fun stopApp() {
viewModelScope.launch(IO) {
ChromeCastHelper.stopApp()
@ -95,4 +106,15 @@ class ChromecastManageViewmodel : ViewModel() {
setVolume(chromeCast.status.volume.level - 0.05f)
}
}
fun getIPv4Address(): InetAddress? {
NetworkInterface.getNetworkInterfaces().toList().forEach { interf ->
interf.inetAddresses.toList().forEach { inetAddress ->
if (!inetAddress.isLoopbackAddress && inetAddress.hostAddress.indexOf(':') < 0) {
return inetAddress
}
}
}
return null
}
}

View file

@ -16,14 +16,21 @@ import androidx.core.app.NotificationCompat
import eu.toldi.balazs.caster.App.Companion.CHANNEL_ID
import eu.toldi.balazs.caster.ChromeCastHelper
import eu.toldi.balazs.caster.R
import kotlinx.coroutines.Dispatchers
import io.ktor.application.*
import io.ktor.features.*
import io.ktor.http.*
import io.ktor.http.content.*
import io.ktor.response.*
import io.ktor.routing.*
import io.ktor.server.engine.*
import io.ktor.server.jetty.*
import io.ktor.websocket.*
import kotlinx.coroutines.*
import kotlinx.coroutines.Dispatchers.IO
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import su.litvak.chromecast.api.v2.ChromeCast
import su.litvak.chromecast.api.v2.Media
import su.litvak.chromecast.api.v2.MediaStatus
import java.io.File
class ChromecastManagerService : Service() {
@ -36,6 +43,9 @@ class ChromecastManagerService : Service() {
const val ACTION_NEXT = "action_next"
const val ACTION_PREVIOUS = "action_previous"
const val ACTION_STOP = "action_stop"
const val ACTION_SETFILE = "action_file"
const val PORT = 3080
}
private val mMediaPlayer = MediaPlayer()
@ -43,10 +53,33 @@ class ChromecastManagerService : Service() {
private lateinit var mController: MediaControllerCompat
private lateinit var pendingIntent : PendingIntent
private var mediaStatus: MediaStatus? = null
private var file : File? = null
override fun onBind(p0: Intent?): IBinder? = null
private lateinit var chromeCast: ChromeCast
private val server by lazy {
embeddedServer(Jetty, PORT, watchPaths = emptyList()) {
install(WebSockets)
install(CallLogging)
routing {
get("/") {
if(file == null) {
call.respondText(
text = "Hello!! You are here in ${Build.MODEL}",
contentType = ContentType.Text.Plain
)
}else{
call.respondFile(file!!)
}
}
static("assets") {
staticRootFolder = applicationContext.cacheDir
files(".")
}
}
}
}
@RequiresApi(Build.VERSION_CODES.M)
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
@ -81,8 +114,11 @@ class ChromecastManagerService : Service() {
.setContentIntent(pendingIntent).build()
startForeground(1, notification)
CoroutineScope(IO).launch {
server.start(wait = true)
}
CoroutineScope(IO).launch {
GlobalScope.launch(IO) {
while (true) {
try {
ChromeCastHelper.chromeCast = chromeCast
@ -116,6 +152,11 @@ class ChromecastManagerService : Service() {
}
override fun onDestroy() {
server.stop(1_000, 2_000)
super.onDestroy()
}
private suspend fun buildNotification() {
var status = chromeCast.status
Log.d(null, status.applications.toString())