Compare commits
10 commits
cf4374d34b
...
8865f2c5b4
Author | SHA1 | Date | |
---|---|---|---|
8865f2c5b4 | |||
5933cd2b8a | |||
55ca983cf7 | |||
f681b7afc9 | |||
a55f2bcd44 | |||
5672eab2bd | |||
0943680271 | |||
4c88992e5f | |||
bebde9bc6a | |||
0432c0be90 |
37
.idea/deploymentTargetDropDown.xml
Normal file
|
@ -0,0 +1,37 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project version="4">
|
||||||
|
<component name="deploymentTargetDropDown">
|
||||||
|
<targetSelectedWithDropDown>
|
||||||
|
<Target>
|
||||||
|
<type value="QUICK_BOOT_TARGET" />
|
||||||
|
<deviceKey>
|
||||||
|
<Key>
|
||||||
|
<type value="VIRTUAL_DEVICE_PATH" />
|
||||||
|
<value value="$USER_HOME$/.android/avd/Pixel_3a_XL_API_30.avd" />
|
||||||
|
</Key>
|
||||||
|
</deviceKey>
|
||||||
|
</Target>
|
||||||
|
</targetSelectedWithDropDown>
|
||||||
|
<timeTargetWasSelectedWithDropDown value="2021-12-05T17:39:35.648801Z" />
|
||||||
|
<targetsSelectedWithDialog>
|
||||||
|
<Target>
|
||||||
|
<type value="QUICK_BOOT_TARGET" />
|
||||||
|
<deviceKey>
|
||||||
|
<Key>
|
||||||
|
<type value="VIRTUAL_DEVICE_PATH" />
|
||||||
|
<value value="$USER_HOME$/.android/avd/Pixel_3a_XL_API_30.avd" />
|
||||||
|
</Key>
|
||||||
|
</deviceKey>
|
||||||
|
</Target>
|
||||||
|
<Target>
|
||||||
|
<type value="QUICK_BOOT_TARGET" />
|
||||||
|
<deviceKey>
|
||||||
|
<Key>
|
||||||
|
<type value="VIRTUAL_DEVICE_PATH" />
|
||||||
|
<value value="$USER_HOME$/.android/avd/Android_TV_720p_API_30.avd" />
|
||||||
|
</Key>
|
||||||
|
</deviceKey>
|
||||||
|
</Target>
|
||||||
|
</targetsSelectedWithDialog>
|
||||||
|
</component>
|
||||||
|
</project>
|
|
@ -28,6 +28,29 @@
|
||||||
<entry key="../../../../../layout/compose-model-1637656156044.xml" value="0.25" />
|
<entry key="../../../../../layout/compose-model-1637656156044.xml" value="0.25" />
|
||||||
<entry key="../../../../../layout/compose-model-1637673325505.xml" value="0.11570945945945946" />
|
<entry key="../../../../../layout/compose-model-1637673325505.xml" value="0.11570945945945946" />
|
||||||
<entry key="../../../../../layout/compose-model-1637674242617.xml" value="0.49537037037037035" />
|
<entry key="../../../../../layout/compose-model-1637674242617.xml" value="0.49537037037037035" />
|
||||||
|
<entry key="../../../../../layout/compose-model-1638180237267.xml" value="0.3095439189189189" />
|
||||||
|
<entry key="../../../../../layout/compose-model-1638183069943.xml" value="0.33" />
|
||||||
|
<entry key="../../../../../layout/compose-model-1638183903572.xml" value="0.48703703703703705" />
|
||||||
|
<entry key="../../../../../layout/compose-model-1638742625644.xml" value="0.48055555555555557" />
|
||||||
|
<entry key="../../../../../layout/compose-model-1638742825833.xml" value="0.1" />
|
||||||
|
<entry key="../../../../../layout/compose-model-1639485191192.xml" value="0.1" />
|
||||||
|
<entry key="../../../../../layout/compose-model-1641150442256.xml" value="0.4564814814814815" />
|
||||||
|
<entry key="../../../../../layout/compose-model-1641153447766.xml" value="0.33" />
|
||||||
|
<entry key="../../../../../layout/compose-model-1641153474417.xml" value="0.4537037037037037" />
|
||||||
|
<entry key="../../../../../layout/compose-model-1641154461708.xml" value="0.30743243243243246" />
|
||||||
|
<entry key="../../../../../layout/compose-model-1641155483959.xml" value="0.25" />
|
||||||
|
<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>
|
</map>
|
||||||
</option>
|
</option>
|
||||||
</component>
|
</component>
|
||||||
|
|
|
@ -4,6 +4,8 @@ plugins {
|
||||||
}
|
}
|
||||||
|
|
||||||
android {
|
android {
|
||||||
|
|
||||||
|
|
||||||
compileSdk 31
|
compileSdk 31
|
||||||
|
|
||||||
defaultConfig {
|
defaultConfig {
|
||||||
|
@ -17,6 +19,11 @@ android {
|
||||||
vectorDrawables {
|
vectorDrawables {
|
||||||
useSupportLibrary true
|
useSupportLibrary true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
ndk {
|
||||||
|
abiFilters 'x86', 'x86_64', 'armeabi-v7a', 'arm64-v8a'
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
buildTypes {
|
buildTypes {
|
||||||
|
@ -29,6 +36,16 @@ android {
|
||||||
sourceCompatibility JavaVersion.VERSION_1_8
|
sourceCompatibility JavaVersion.VERSION_1_8
|
||||||
targetCompatibility JavaVersion.VERSION_1_8
|
targetCompatibility JavaVersion.VERSION_1_8
|
||||||
}
|
}
|
||||||
|
|
||||||
|
splits {
|
||||||
|
abi {
|
||||||
|
enable true
|
||||||
|
reset()
|
||||||
|
include 'x86', 'x86_64', 'armeabi-v7a', 'arm64-v8a'
|
||||||
|
universalApk true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
kotlinOptions {
|
kotlinOptions {
|
||||||
jvmTarget = '1.8'
|
jvmTarget = '1.8'
|
||||||
useIR = true
|
useIR = true
|
||||||
|
@ -41,6 +58,10 @@ android {
|
||||||
kotlinCompilerVersion '1.5.21'
|
kotlinCompilerVersion '1.5.21'
|
||||||
}
|
}
|
||||||
packagingOptions {
|
packagingOptions {
|
||||||
|
exclude 'META-INF/*'
|
||||||
|
// Due to https://github.com/Kotlin/kotlinx.coroutines/issues/2023
|
||||||
|
exclude 'META-INF/licenses/*'
|
||||||
|
exclude '**/attach_hotspot_windows.dll'
|
||||||
resources {
|
resources {
|
||||||
excludes += '/META-INF/{AL2.0,LGPL2.1}'
|
excludes += '/META-INF/{AL2.0,LGPL2.1}'
|
||||||
}
|
}
|
||||||
|
@ -62,9 +83,15 @@ dependencies {
|
||||||
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.5.2'
|
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.5.2'
|
||||||
implementation 'com.fasterxml.jackson.core:jackson-databind:2.10.0'
|
implementation 'com.fasterxml.jackson.core:jackson-databind:2.10.0'
|
||||||
implementation 'su.litvak.chromecast:api-v2:0.11.3'
|
implementation 'su.litvak.chromecast:api-v2:0.11.3'
|
||||||
implementation 'com.github.yausername.youtubedl-android:library:0.12.+'
|
implementation("io.coil-kt:coil-compose:1.4.0")
|
||||||
implementation 'com.github.yausername.youtubedl-android:ffmpeg:0.12.+'
|
implementation "com.google.accompanist:accompanist-swiperefresh:0.22.0-rc"
|
||||||
|
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 "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.+'
|
testImplementation 'junit:junit:4.+'
|
||||||
androidTestImplementation 'androidx.test.ext:junit:1.1.3'
|
androidTestImplementation 'androidx.test.ext:junit:1.1.3'
|
||||||
androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0'
|
androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0'
|
||||||
|
|
|
@ -3,8 +3,9 @@
|
||||||
package="eu.toldi.balazs.caster">
|
package="eu.toldi.balazs.caster">
|
||||||
|
|
||||||
<uses-permission android:name="android.permission.INTERNET" />
|
<uses-permission android:name="android.permission.INTERNET" />
|
||||||
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
|
<uses-permission android:name="android.permission.ACCESS_WIFI_STATE" />
|
||||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE"/>
|
<uses-permission android:name="android.permission.CHANGE_WIFI_MULTICAST_STATE" />
|
||||||
|
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
|
||||||
|
|
||||||
<application
|
<application
|
||||||
android:name=".App"
|
android:name=".App"
|
||||||
|
@ -13,6 +14,8 @@
|
||||||
android:label="@string/app_name"
|
android:label="@string/app_name"
|
||||||
android:roundIcon="@drawable/ic_caster_logo"
|
android:roundIcon="@drawable/ic_caster_logo"
|
||||||
android:supportsRtl="true"
|
android:supportsRtl="true"
|
||||||
|
android:extractNativeLibs="true"
|
||||||
|
android:requestLegacyExternalStorage="true"
|
||||||
android:theme="@style/Theme.Caster">
|
android:theme="@style/Theme.Caster">
|
||||||
<activity
|
<activity
|
||||||
android:name=".ShareRecieverActivity"
|
android:name=".ShareRecieverActivity"
|
||||||
|
@ -24,6 +27,16 @@
|
||||||
<category android:name="android.intent.category.DEFAULT" />
|
<category android:name="android.intent.category.DEFAULT" />
|
||||||
<data android:mimeType="text/plain" />
|
<data android:mimeType="text/plain" />
|
||||||
</intent-filter>
|
</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>
|
||||||
<activity
|
<activity
|
||||||
android:name=".ChromecastManagerActivity"
|
android:name=".ChromecastManagerActivity"
|
||||||
|
|
|
@ -11,6 +11,8 @@ import kotlinx.coroutines.withContext
|
||||||
import su.litvak.chromecast.api.v2.Application
|
import su.litvak.chromecast.api.v2.Application
|
||||||
import su.litvak.chromecast.api.v2.ChromeCast
|
import su.litvak.chromecast.api.v2.ChromeCast
|
||||||
import su.litvak.chromecast.api.v2.MediaStatus
|
import su.litvak.chromecast.api.v2.MediaStatus
|
||||||
|
import java.net.InetAddress
|
||||||
|
import java.net.NetworkInterface
|
||||||
|
|
||||||
object ChromeCastHelper {
|
object ChromeCastHelper {
|
||||||
|
|
||||||
|
@ -25,6 +27,7 @@ object ChromeCastHelper {
|
||||||
const val APP_BUBBLEUPNP = "3927FA74"
|
const val APP_BUBBLEUPNP = "3927FA74"
|
||||||
const val APP_BBCSOUNDS = "03977A48"
|
const val APP_BBCSOUNDS = "03977A48"
|
||||||
const val APP_BBCIPLAYER = "5E81F6DB"
|
const val APP_BBCIPLAYER = "5E81F6DB"
|
||||||
|
const val APP_YOUTUBE_MUSIC = "2DB7CC49"
|
||||||
|
|
||||||
lateinit var chromeCast: ChromeCast
|
lateinit var chromeCast: ChromeCast
|
||||||
|
|
||||||
|
@ -32,6 +35,7 @@ object ChromeCastHelper {
|
||||||
return when (id) {
|
return when (id) {
|
||||||
APP_MEDIA_RECEIVER -> "Media Player"
|
APP_MEDIA_RECEIVER -> "Media Player"
|
||||||
APP_YOUTUBE -> "Youtube"
|
APP_YOUTUBE -> "Youtube"
|
||||||
|
APP_YOUTUBE_MUSIC -> "Youtube Music"
|
||||||
APP_PLEX -> "Plex"
|
APP_PLEX -> "Plex"
|
||||||
APP_BACKDROP -> "Idle"
|
APP_BACKDROP -> "Idle"
|
||||||
APP_DASHCAST -> "Dashcast"
|
APP_DASHCAST -> "Dashcast"
|
||||||
|
@ -66,7 +70,11 @@ object ChromeCastHelper {
|
||||||
if (mediaStatus != null) {
|
if (mediaStatus != null) {
|
||||||
try {
|
try {
|
||||||
withContext(IO) {
|
withContext(IO) {
|
||||||
chromeCast.seek(d * mediaStatus.media.duration)
|
try {
|
||||||
|
chromeCast.seek(d * mediaStatus.media.duration)
|
||||||
|
}catch (e:Exception) {
|
||||||
|
Log.e(null,e.stackTraceToString())
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Log.e(null, e.stackTraceToString())
|
Log.e(null, e.stackTraceToString())
|
||||||
|
@ -96,7 +104,13 @@ object ChromeCastHelper {
|
||||||
|
|
||||||
suspend fun setVolume(f: Float) {
|
suspend fun setVolume(f: Float) {
|
||||||
withContext(IO) {
|
withContext(IO) {
|
||||||
chromeCast.setVolume(f)
|
if(f >= 0.0f && f <= 1.0f) {
|
||||||
|
try {
|
||||||
|
chromeCast.setVolume(f)
|
||||||
|
}catch (e:Exception) {
|
||||||
|
Log.e(null,e.stackTraceToString())
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -120,30 +134,43 @@ object ChromeCastHelper {
|
||||||
exitStatus = false
|
exitStatus = false
|
||||||
callBack.invoke()
|
callBack.invoke()
|
||||||
} else {
|
} else {
|
||||||
|
try {
|
||||||
|
val streamInfo = _streamInfo as VideoInfo
|
||||||
|
val status = chromeCast.status
|
||||||
|
if (chromeCast.isAppAvailable(ChromeCastHelper.APP_MEDIA_RECEIVER) && !status.isAppRunning(
|
||||||
|
ChromeCastHelper.APP_MEDIA_RECEIVER
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
val app: Application =
|
||||||
|
chromeCast.launchApp(ChromeCastHelper.APP_MEDIA_RECEIVER)
|
||||||
|
}
|
||||||
|
while (!chromeCast.status.isAppRunning(ChromeCastHelper.APP_MEDIA_RECEIVER)) {
|
||||||
|
delay(100)
|
||||||
|
}
|
||||||
|
|
||||||
val streamInfo = _streamInfo as VideoInfo
|
chromeCast.load(
|
||||||
val status = chromeCast.status
|
streamInfo.title,
|
||||||
if (chromeCast.isAppAvailable(ChromeCastHelper.APP_MEDIA_RECEIVER) && !status.isAppRunning(
|
streamInfo.thumbnail,
|
||||||
ChromeCastHelper.APP_MEDIA_RECEIVER
|
streamInfo.url,
|
||||||
|
null
|
||||||
)
|
)
|
||||||
) {
|
} catch (e: Exception) {
|
||||||
val app: Application = chromeCast.launchApp(ChromeCastHelper.APP_MEDIA_RECEIVER)
|
Log.e(null, e.stackTraceToString())
|
||||||
|
} finally {
|
||||||
|
callBack.invoke()
|
||||||
}
|
}
|
||||||
while (!chromeCast.status.isAppRunning(ChromeCastHelper.APP_MEDIA_RECEIVER)) {
|
|
||||||
delay(100)
|
|
||||||
}
|
|
||||||
|
|
||||||
chromeCast.load(
|
|
||||||
streamInfo.title,
|
|
||||||
streamInfo.thumbnail,
|
|
||||||
streamInfo.url,
|
|
||||||
null
|
|
||||||
)
|
|
||||||
|
|
||||||
callBack.invoke()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return exitStatus
|
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
|
||||||
|
}
|
||||||
}
|
}
|
|
@ -1,13 +1,21 @@
|
||||||
package eu.toldi.balazs.caster
|
package eu.toldi.balazs.caster
|
||||||
|
|
||||||
|
import android.annotation.SuppressLint
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
|
import android.database.Cursor
|
||||||
|
import android.net.Uri
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
|
import android.provider.OpenableColumns
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
import android.view.KeyEvent
|
import android.view.KeyEvent
|
||||||
|
import android.webkit.MimeTypeMap
|
||||||
import androidx.activity.ComponentActivity
|
import androidx.activity.ComponentActivity
|
||||||
|
import androidx.activity.compose.rememberLauncherForActivityResult
|
||||||
import androidx.activity.compose.setContent
|
import androidx.activity.compose.setContent
|
||||||
|
import androidx.activity.result.contract.ActivityResultContracts
|
||||||
import androidx.annotation.RequiresApi
|
import androidx.annotation.RequiresApi
|
||||||
|
import androidx.compose.foundation.Image
|
||||||
import androidx.compose.foundation.layout.*
|
import androidx.compose.foundation.layout.*
|
||||||
import androidx.compose.material.*
|
import androidx.compose.material.*
|
||||||
import androidx.compose.material.icons.Icons
|
import androidx.compose.material.icons.Icons
|
||||||
|
@ -15,34 +23,31 @@ import androidx.compose.material.icons.filled.*
|
||||||
import androidx.compose.runtime.*
|
import androidx.compose.runtime.*
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.res.stringResource
|
||||||
import androidx.compose.ui.tooling.preview.Preview
|
import androidx.compose.ui.tooling.preview.Preview
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.core.content.ContextCompat
|
import androidx.core.content.ContextCompat
|
||||||
import androidx.lifecycle.ViewModel
|
|
||||||
import androidx.lifecycle.ViewModelProvider
|
import androidx.lifecycle.ViewModelProvider
|
||||||
import androidx.lifecycle.lifecycleScope
|
import coil.compose.rememberImagePainter
|
||||||
import eu.toldi.balazs.caster.ui.theme.CasterTheme
|
import com.yausername.ffmpeg.FFmpeg
|
||||||
import kotlinx.coroutines.Dispatchers.IO
|
|
||||||
import kotlinx.coroutines.GlobalScope
|
|
||||||
import kotlinx.coroutines.delay
|
|
||||||
import kotlinx.coroutines.launch
|
|
||||||
import su.litvak.chromecast.api.v2.Application
|
|
||||||
import su.litvak.chromecast.api.v2.ChromeCast
|
|
||||||
import com.yausername.youtubedl_android.YoutubeDL
|
import com.yausername.youtubedl_android.YoutubeDL
|
||||||
|
|
||||||
import com.yausername.youtubedl_android.YoutubeDLRequest
|
|
||||||
import com.yausername.youtubedl_android.YoutubeDLException
|
import com.yausername.youtubedl_android.YoutubeDLException
|
||||||
import com.yausername.youtubedl_android.mapper.VideoInfo
|
import eu.toldi.balazs.caster.helpers.FileCacheHelper
|
||||||
import eu.toldi.balazs.caster.model.ChromecastManageViewmodel
|
import eu.toldi.balazs.caster.model.ChromecastManageViewmodel
|
||||||
import eu.toldi.balazs.caster.services.ChromecastManagerService
|
import eu.toldi.balazs.caster.services.ChromecastManagerService
|
||||||
import kotlinx.coroutines.withContext
|
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.Media
|
||||||
import su.litvak.chromecast.api.v2.MediaStatus
|
import su.litvak.chromecast.api.v2.MediaStatus
|
||||||
|
import java.io.BufferedInputStream
|
||||||
|
import java.io.BufferedOutputStream
|
||||||
|
import java.io.FileOutputStream
|
||||||
|
|
||||||
|
|
||||||
class ChromecastManagerActivity : ComponentActivity() {
|
class ChromecastManagerActivity : ComponentActivity() {
|
||||||
companion object {
|
companion object {
|
||||||
var chromeCast_: ChromeCast? = null
|
var chromeCast_: ChromeCast? = null
|
||||||
|
const val METADATA_THUMB = "thumb"
|
||||||
}
|
}
|
||||||
|
|
||||||
private lateinit var chromeCast: ChromeCast
|
private lateinit var chromeCast: ChromeCast
|
||||||
|
@ -56,10 +61,11 @@ class ChromecastManagerActivity : ComponentActivity() {
|
||||||
ChromeCastHelper.chromeCast = chromeCast
|
ChromeCastHelper.chromeCast = chromeCast
|
||||||
try {
|
try {
|
||||||
YoutubeDL.getInstance().init(application)
|
YoutubeDL.getInstance().init(application)
|
||||||
|
FFmpeg.getInstance().init(application)
|
||||||
} catch (e: YoutubeDLException) {
|
} catch (e: YoutubeDLException) {
|
||||||
Log.e("Caster", "failed to initialize youtubedl-android", e)
|
Log.e("Caster", "failed to initialize youtubedl-android", e)
|
||||||
}
|
}
|
||||||
val serviceIntent = Intent(this, ChromecastManagerService::class.java).also {
|
Intent(this, ChromecastManagerService::class.java).also {
|
||||||
it.action = ChromecastManagerService.ACTION_INIT
|
it.action = ChromecastManagerService.ACTION_INIT
|
||||||
it.putExtra("CHROMECAST_ADDRESS", chromeCast.address)
|
it.putExtra("CHROMECAST_ADDRESS", chromeCast.address)
|
||||||
it.putExtra("CHROMECAST_NAME", chromeCast.title)
|
it.putExtra("CHROMECAST_NAME", chromeCast.title)
|
||||||
|
@ -72,7 +78,7 @@ class ChromecastManagerActivity : ComponentActivity() {
|
||||||
// A surface container using the 'background' color from the theme
|
// A surface container using the 'background' color from the theme
|
||||||
Surface(color = MaterialTheme.colors.background) {
|
Surface(color = MaterialTheme.colors.background) {
|
||||||
Column {
|
Column {
|
||||||
viewModel.fetchMediaStatus()
|
//viewModel.fetchMediaStatus()
|
||||||
MenuBar()
|
MenuBar()
|
||||||
Column(
|
Column(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
|
@ -88,7 +94,7 @@ class ChromecastManagerActivity : ComponentActivity() {
|
||||||
onValueChange = {
|
onValueChange = {
|
||||||
text = it
|
text = it
|
||||||
},
|
},
|
||||||
label = { Text("Cast URL") },
|
label = { Text(stringResource(id = R.string.cast_url)) },
|
||||||
modifier = Modifier.padding(vertical = 4.dp)
|
modifier = Modifier.padding(vertical = 4.dp)
|
||||||
)
|
)
|
||||||
var castEnabled by remember { mutableStateOf(true) }
|
var castEnabled by remember { mutableStateOf(true) }
|
||||||
|
@ -102,44 +108,13 @@ class ChromecastManagerActivity : ComponentActivity() {
|
||||||
},
|
},
|
||||||
modifier = Modifier.padding(vertical = 4.dp)
|
modifier = Modifier.padding(vertical = 4.dp)
|
||||||
) {
|
) {
|
||||||
Text(text = "Cast")
|
Text(text = stringResource(id = R.string.cast))
|
||||||
}
|
}
|
||||||
if (castEnabled.not()) {
|
if (castEnabled.not()) {
|
||||||
CircularProgressIndicator()
|
CircularProgressIndicator()
|
||||||
}
|
}
|
||||||
playBackControl()
|
PlayBackControl()
|
||||||
val mediaStatus = viewModel.mediaStatus.value
|
mediaStatus()
|
||||||
|
|
||||||
if (mediaStatus != null) {
|
|
||||||
val nowPlaying = mediaStatus.media.metadata[Media.METADATA_TITLE]
|
|
||||||
Text(text = "Now playing: $nowPlaying")
|
|
||||||
var sliderPosition by remember { mutableStateOf((mediaStatus.currentTime / mediaStatus.media.duration).toFloat()) }
|
|
||||||
|
|
||||||
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()
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
Slider(value = sliderPosition, onValueChange = {
|
|
||||||
sliderPosition = it
|
|
||||||
}, onValueChangeFinished = {
|
|
||||||
viewModel.seek(sliderPosition.toDouble())
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -148,8 +123,90 @@ class ChromecastManagerActivity : ComponentActivity() {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun mediaStatus() {
|
||||||
|
val mediaStatus = viewModel.mediaStatus.value
|
||||||
|
|
||||||
|
if (mediaStatus != null) {
|
||||||
|
val nowPlaying =
|
||||||
|
if (mediaStatus.media.metadata != null) mediaStatus.media.metadata[Media.METADATA_TITLE] else ""
|
||||||
|
|
||||||
|
|
||||||
|
Text(text = stringResource(id = R.string.now_playing) + ": $nowPlaying")
|
||||||
|
var sliderPosition by remember { mutableStateOf(0.0f) }
|
||||||
|
var sliderMoving by remember { mutableStateOf(false) }
|
||||||
|
if (mediaStatus.media.duration != null) {
|
||||||
|
|
||||||
|
|
||||||
|
if (!sliderMoving)
|
||||||
|
sliderPosition =
|
||||||
|
(mediaStatus.currentTime / mediaStatus.media.duration).toFloat()
|
||||||
|
}
|
||||||
|
if (mediaStatus.media.metadata[Media.METADATA_IMAGES] != null) {
|
||||||
|
val images =
|
||||||
|
mediaStatus.media.metadata[Media.METADATA_IMAGES] as ArrayList<LinkedHashMap<String, String>>
|
||||||
|
if (images.size >= 1 && images[0].containsKey("url")) {
|
||||||
|
Log.e("Caster", images[0]["url"].toString())
|
||||||
|
Image(
|
||||||
|
painter = rememberImagePainter(images[0]["url"]),
|
||||||
|
contentDescription = null,
|
||||||
|
modifier = Modifier.fillMaxWidth()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
} else if (mediaStatus.media.metadata[METADATA_THUMB] != null) {
|
||||||
|
val image =
|
||||||
|
mediaStatus.media.metadata[METADATA_THUMB] as String
|
||||||
|
Log.e("Caster", image)
|
||||||
|
Image(
|
||||||
|
painter = rememberImagePainter(image),
|
||||||
|
contentDescription = null,
|
||||||
|
modifier = Modifier.fillMaxWidth()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
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()
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
Slider(value = sliderPosition, onValueChange = {
|
||||||
|
sliderPosition = it
|
||||||
|
sliderMoving = true
|
||||||
|
}, onValueChangeFinished = {
|
||||||
|
viewModel.seek(sliderPosition.toDouble())
|
||||||
|
sliderMoving = false
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun MenuBar() {
|
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(
|
TopAppBar(
|
||||||
title = {
|
title = {
|
||||||
Text("Caster")
|
Text("Caster")
|
||||||
|
@ -158,39 +215,42 @@ class ChromecastManagerActivity : ComponentActivity() {
|
||||||
IconButton(onClick = { finish() }) {
|
IconButton(onClick = { finish() }) {
|
||||||
Icon(
|
Icon(
|
||||||
Icons.Filled.ArrowBack,
|
Icons.Filled.ArrowBack,
|
||||||
contentDescription = "Back"
|
contentDescription = stringResource(id = R.string.back)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
actions = {
|
actions = {
|
||||||
Row {
|
Row {
|
||||||
|
IconButton(onClick = {
|
||||||
|
pickPictureLauncher.launch(arrayOf("image/*", "video/*"))
|
||||||
|
}) {
|
||||||
|
Icon(
|
||||||
|
Icons.Filled.Folder,
|
||||||
|
contentDescription = stringResource(id = R.string.back)
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun playBackControl() {
|
fun PlayBackControl() {
|
||||||
val mediaStatus = viewModel.mediaStatus.value
|
val mediaStatus = if (this::viewModel.isInitialized)
|
||||||
|
viewModel.mediaStatus.value
|
||||||
|
else null
|
||||||
Row {
|
Row {
|
||||||
|
|
||||||
IconButton(onClick = {
|
IconButton(onClick = {
|
||||||
if (mediaStatus != null) {
|
if (mediaStatus != null) {
|
||||||
viewModel.seek(chromeCast.mediaStatus.currentTime - 10)
|
viewModel.seek(mediaStatus.currentTime - 10)
|
||||||
}
|
}
|
||||||
}, enabled = mediaStatus != null) {
|
}, enabled = mediaStatus != null) {
|
||||||
Icon(
|
Icon(
|
||||||
Icons.Filled.FastRewind,
|
Icons.Filled.FastRewind,
|
||||||
contentDescription = "FastRewind"
|
contentDescription = stringResource(id = R.string.rewind)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
/*
|
|
||||||
var playBackState by remember { mutableStateOf(MediaStatus.PlayerState.IDLE) }
|
|
||||||
lifecycleScope.launch(IO) {
|
|
||||||
if(chromeCast.mediaStatus != null)
|
|
||||||
playBackState = chromeCast.mediaStatus.playerState
|
|
||||||
}*/
|
|
||||||
IconButton(onClick = {
|
IconButton(onClick = {
|
||||||
|
|
||||||
if (mediaStatus != null) {
|
if (mediaStatus != null) {
|
||||||
|
@ -200,22 +260,21 @@ class ChromecastManagerActivity : ComponentActivity() {
|
||||||
if (mediaStatus.playerState == MediaStatus.PlayerState.PAUSED) {
|
if (mediaStatus.playerState == MediaStatus.PlayerState.PAUSED) {
|
||||||
viewModel.play()
|
viewModel.play()
|
||||||
}
|
}
|
||||||
viewModel.fetchMediaStatus()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}, enabled = mediaStatus != null) {
|
}, enabled = mediaStatus != null) {
|
||||||
when {
|
when {
|
||||||
mediaStatus == null || mediaStatus.playerState == MediaStatus.PlayerState.PAUSED -> Icon(
|
mediaStatus == null || mediaStatus.playerState == MediaStatus.PlayerState.PAUSED -> Icon(
|
||||||
Icons.Filled.PlayArrow,
|
Icons.Filled.PlayArrow,
|
||||||
contentDescription = "Resume"
|
contentDescription = stringResource(id = R.string.resume)
|
||||||
)
|
)
|
||||||
mediaStatus.playerState == MediaStatus.PlayerState.PLAYING -> Icon(
|
mediaStatus.playerState == MediaStatus.PlayerState.PLAYING -> Icon(
|
||||||
Icons.Filled.Pause,
|
Icons.Filled.Pause,
|
||||||
contentDescription = "Pause"
|
contentDescription = stringResource(id = R.string.pause)
|
||||||
)
|
)
|
||||||
else -> Icon(
|
else -> Icon(
|
||||||
Icons.Filled.PlayArrow,
|
Icons.Filled.PlayArrow,
|
||||||
contentDescription = "Resume"
|
contentDescription = stringResource(id = R.string.resume)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -226,18 +285,18 @@ class ChromecastManagerActivity : ComponentActivity() {
|
||||||
}, enabled = mediaStatus != null) {
|
}, enabled = mediaStatus != null) {
|
||||||
Icon(
|
Icon(
|
||||||
Icons.Filled.Stop,
|
Icons.Filled.Stop,
|
||||||
contentDescription = "Stop"
|
contentDescription = stringResource(id = R.string.stop)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
IconButton(onClick = {
|
IconButton(onClick = {
|
||||||
if (chromeCast.mediaStatus != null) {
|
if (mediaStatus != null) {
|
||||||
viewModel.seek(chromeCast.mediaStatus.currentTime + 10)
|
viewModel.seek(mediaStatus.currentTime + 10)
|
||||||
}
|
}
|
||||||
}, enabled = mediaStatus != null) {
|
}, enabled = mediaStatus != null) {
|
||||||
Icon(
|
Icon(
|
||||||
Icons.Filled.FastForward,
|
Icons.Filled.FastForward,
|
||||||
contentDescription = "FastForward"
|
contentDescription = stringResource(id = R.string.fastforward)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -270,18 +329,18 @@ class ChromecastManagerActivity : ComponentActivity() {
|
||||||
onValueChange = {
|
onValueChange = {
|
||||||
text = it
|
text = it
|
||||||
},
|
},
|
||||||
label = { Text("Cast URL") },
|
label = { Text(stringResource(id = R.string.cast_url)) },
|
||||||
modifier = Modifier.padding(vertical = 4.dp)
|
modifier = Modifier.padding(vertical = 4.dp)
|
||||||
)
|
)
|
||||||
Button(
|
Button(
|
||||||
onClick = {},
|
onClick = {},
|
||||||
modifier = Modifier.padding(vertical = 4.dp)
|
modifier = Modifier.padding(vertical = 4.dp)
|
||||||
) {
|
) {
|
||||||
Text(text = "Cast")
|
Text(text = stringResource(id = R.string.cast))
|
||||||
}
|
}
|
||||||
// val viewModel = ViewModelProvider(this).get(ChromecastManageViewmodel::class.java)
|
// val viewModel = ViewModelProvider(this).get(ChromecastManageViewmodel::class.java)
|
||||||
//playBackControl(viewModel)
|
PlayBackControl()
|
||||||
Text(text = "Now playing: Some video")
|
Text(text = stringResource(id = R.string.now_playing) + ": Some video")
|
||||||
var sliderPosition by remember { mutableStateOf(0f) }
|
var sliderPosition by remember { mutableStateOf(0f) }
|
||||||
Slider(value = sliderPosition, onValueChange = { sliderPosition = it })
|
Slider(value = sliderPosition, onValueChange = { sliderPosition = it })
|
||||||
}
|
}
|
||||||
|
@ -291,13 +350,13 @@ class ChromecastManagerActivity : ComponentActivity() {
|
||||||
|
|
||||||
override fun onKeyDown(keyCode: Int, event: KeyEvent?): Boolean {
|
override fun onKeyDown(keyCode: Int, event: KeyEvent?): Boolean {
|
||||||
if (keyCode == KeyEvent.KEYCODE_VOLUME_UP) {
|
if (keyCode == KeyEvent.KEYCODE_VOLUME_UP) {
|
||||||
viewModel.setVolume(chromeCast.status.volume.level + 0.05f)
|
viewModel.increaseVolume()
|
||||||
|
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
if (keyCode == KeyEvent.KEYCODE_VOLUME_DOWN) {
|
if (keyCode == KeyEvent.KEYCODE_VOLUME_DOWN) {
|
||||||
|
viewModel.decreaseVolume()
|
||||||
|
|
||||||
viewModel.setVolume(chromeCast.status.volume.level - 0.05f)
|
|
||||||
|
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
return super.onKeyDown(keyCode, event)
|
return super.onKeyDown(keyCode, event)
|
||||||
|
|
|
@ -2,36 +2,64 @@ package eu.toldi.balazs.caster
|
||||||
|
|
||||||
import android.annotation.SuppressLint
|
import android.annotation.SuppressLint
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
|
import android.net.wifi.WifiManager
|
||||||
|
import android.net.wifi.WifiManager.MulticastLock
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
import androidx.activity.ComponentActivity
|
import androidx.activity.ComponentActivity
|
||||||
import androidx.activity.compose.setContent
|
import androidx.activity.compose.setContent
|
||||||
import androidx.activity.viewModels
|
import androidx.compose.foundation.Image
|
||||||
|
import androidx.compose.foundation.clickable
|
||||||
import androidx.compose.foundation.layout.*
|
import androidx.compose.foundation.layout.*
|
||||||
import androidx.compose.foundation.lazy.LazyColumn
|
import androidx.compose.foundation.lazy.LazyColumn
|
||||||
import androidx.compose.material.*
|
import androidx.compose.material.*
|
||||||
import androidx.compose.material.icons.Icons
|
import androidx.compose.material.icons.Icons
|
||||||
import androidx.compose.material.icons.filled.*
|
import androidx.compose.material.icons.filled.Add
|
||||||
|
import androidx.compose.material.icons.filled.Refresh
|
||||||
import androidx.compose.runtime.*
|
import androidx.compose.runtime.*
|
||||||
import androidx.compose.runtime.livedata.observeAsState
|
import androidx.compose.runtime.livedata.observeAsState
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.res.painterResource
|
||||||
|
import androidx.compose.ui.res.stringResource
|
||||||
import androidx.compose.ui.tooling.preview.Preview
|
import androidx.compose.ui.tooling.preview.Preview
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.lifecycle.ViewModelProvider
|
import androidx.lifecycle.ViewModelProvider
|
||||||
import androidx.lifecycle.lifecycleScope
|
import com.google.accompanist.swiperefresh.SwipeRefresh
|
||||||
|
import com.google.accompanist.swiperefresh.rememberSwipeRefreshState
|
||||||
import eu.toldi.balazs.caster.model.ChromeCastViewModel
|
import eu.toldi.balazs.caster.model.ChromeCastViewModel
|
||||||
import eu.toldi.balazs.caster.ui.theme.CasterTheme
|
import eu.toldi.balazs.caster.ui.theme.CasterTheme
|
||||||
import kotlinx.coroutines.*
|
|
||||||
import kotlinx.coroutines.Dispatchers.IO
|
|
||||||
import su.litvak.chromecast.api.v2.Application
|
|
||||||
import su.litvak.chromecast.api.v2.ChromeCast
|
import su.litvak.chromecast.api.v2.ChromeCast
|
||||||
import su.litvak.chromecast.api.v2.ChromeCasts
|
|
||||||
import java.net.Inet4Address
|
|
||||||
import java.net.InetAddress
|
|
||||||
import java.net.NetworkInterface
|
|
||||||
|
|
||||||
class MainActivity : ComponentActivity() {
|
|
||||||
|
open class MainActivity : ComponentActivity() {
|
||||||
|
|
||||||
|
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
|
||||||
|
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()
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
@SuppressLint("CoroutineCreationDuringComposition")
|
@SuppressLint("CoroutineCreationDuringComposition")
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
|
@ -39,37 +67,76 @@ class MainActivity : ComponentActivity() {
|
||||||
|
|
||||||
setContent {
|
setContent {
|
||||||
CasterTheme {
|
CasterTheme {
|
||||||
val viewModel = ViewModelProvider(this).get(ChromeCastViewModel::class.java)
|
|
||||||
viewModel.startScanning()
|
|
||||||
|
|
||||||
|
viewModel = ViewModelProvider(this).get(ChromeCastViewModel::class.java)
|
||||||
val chromeCastState = viewModel.chromeCasts.observeAsState(initial = emptyList())
|
val chromeCastState = viewModel.chromeCasts.observeAsState(initial = emptyList())
|
||||||
val chromeCasts = chromeCastState.value
|
val chromeCasts = chromeCastState.value
|
||||||
Log.e(null,chromeCasts.toString())
|
Log.e(null, chromeCasts.toString())
|
||||||
// A surface container using the 'background' color from the theme
|
// A surface container using the 'background' color from the theme
|
||||||
Surface(color = MaterialTheme.colors.background) {
|
Surface(color = MaterialTheme.colors.background) {
|
||||||
Column {
|
Column(modifier = Modifier
|
||||||
MenuBar {
|
.fillMaxWidth()
|
||||||
viewModel.refresh()
|
.fillMaxHeight()) {
|
||||||
|
var isAddChromecastDialogOpen by remember {
|
||||||
|
mutableStateOf(false)
|
||||||
}
|
}
|
||||||
LazyColumn(modifier = Modifier
|
MenuBar(
|
||||||
.padding(all = 4.dp)
|
refresh = {
|
||||||
.fillMaxWidth(),
|
viewModel.refresh()
|
||||||
horizontalAlignment = Alignment.CenterHorizontally) {
|
},
|
||||||
item {
|
add = {
|
||||||
if (chromeCasts.isNotEmpty())
|
isAddChromecastDialogOpen = true
|
||||||
Text(text = "Available chromecasts:")
|
})
|
||||||
else {
|
|
||||||
Column(
|
|
||||||
modifier = Modifier.fillMaxSize() ,
|
if (isAddChromecastDialogOpen) {
|
||||||
horizontalAlignment = Alignment.CenterHorizontally
|
showAddChromecastDialog(dismiss = {
|
||||||
){
|
isAddChromecastDialogOpen = false
|
||||||
Text("Looking for Chromecasts on your network...")
|
}) {
|
||||||
CircularProgressIndicator()
|
if (it.matches(Regex("^(?:[0-9]{1,3}\\.){3}[0-9]{1,3}\$")))
|
||||||
|
viewModel.addChromecast(ChromeCast(it).also { chromeCast ->
|
||||||
|
chromeCast.name = "Chromecast@$it"
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
var isRefreshing by remember {
|
||||||
|
mutableStateOf(false)
|
||||||
|
}
|
||||||
|
|
||||||
|
SwipeRefresh(
|
||||||
|
state = rememberSwipeRefreshState(isRefreshing),
|
||||||
|
onRefresh = {
|
||||||
|
isRefreshing = true
|
||||||
|
viewModel.refresh()
|
||||||
|
isRefreshing = false
|
||||||
|
},
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.fillMaxHeight()
|
||||||
|
) {
|
||||||
|
LazyColumn(
|
||||||
|
modifier = Modifier
|
||||||
|
.padding(all = 4.dp)
|
||||||
|
.fillMaxWidth()
|
||||||
|
.fillMaxHeight(),
|
||||||
|
horizontalAlignment = Alignment.CenterHorizontally
|
||||||
|
) {
|
||||||
|
item {
|
||||||
|
if (chromeCasts.isNotEmpty())
|
||||||
|
Text(text = stringResource(id = R.string.available_chromecasts))
|
||||||
|
else {
|
||||||
|
Column(
|
||||||
|
modifier = Modifier.fillMaxSize(),
|
||||||
|
horizontalAlignment = Alignment.CenterHorizontally
|
||||||
|
) {
|
||||||
|
Text(stringResource(id = R.string.looking_for_devices))
|
||||||
|
CircularProgressIndicator()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
items(chromeCasts.size) { index ->
|
||||||
items(chromeCasts.size) { index ->
|
showChromeCastButton(chromeCast = chromeCasts[index])
|
||||||
showChromeCastButton(chromeCast = chromeCasts[index])
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -78,22 +145,106 @@ class MainActivity : ComponentActivity() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun showAddChromecastDialog(dismiss: () -> Unit, add: (String) -> Unit) {
|
||||||
|
var ipaddress by remember {
|
||||||
|
mutableStateOf("")
|
||||||
|
}
|
||||||
|
AlertDialog(onDismissRequest = dismiss,
|
||||||
|
title = {
|
||||||
|
Text(text = stringResource(id = R.string.add_chromecast))
|
||||||
|
},
|
||||||
|
text = {
|
||||||
|
OutlinedTextField(
|
||||||
|
value = ipaddress,
|
||||||
|
onValueChange = {
|
||||||
|
ipaddress = it
|
||||||
|
},
|
||||||
|
label = { Text(stringResource(id = R.string.ip_address)) },
|
||||||
|
modifier = Modifier.padding(vertical = 4.dp)
|
||||||
|
)
|
||||||
|
}, buttons = {
|
||||||
|
Row(
|
||||||
|
modifier = Modifier.padding(all = 8.dp),
|
||||||
|
horizontalArrangement = Arrangement.Center
|
||||||
|
) {
|
||||||
|
Button(
|
||||||
|
onClick = {
|
||||||
|
add(ipaddress)
|
||||||
|
dismiss()
|
||||||
|
},
|
||||||
|
modifier = Modifier.padding(all = 8.dp)
|
||||||
|
) {
|
||||||
|
Text(text = stringResource(id = R.string.add_chromecast))
|
||||||
|
}
|
||||||
|
Button(onClick = dismiss, modifier = Modifier.padding(all = 8.dp)) {
|
||||||
|
Text(stringResource(id = R.string.cancel))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun showChromeCastButton(chromeCast: ChromeCast) {
|
fun showChromeCastButton(chromeCast: ChromeCast,buttonCallBack: () -> Unit = {
|
||||||
Button(onClick = {
|
ChromecastManagerActivity.chromeCast_ = chromeCast
|
||||||
ChromecastManagerActivity.chromeCast_ = chromeCast
|
val intent = Intent(applicationContext, ChromecastManagerActivity::class.java)
|
||||||
val intent = Intent(this, ChromecastManagerActivity::class.java)
|
startActivity(intent)
|
||||||
startActivity(intent)
|
}) {
|
||||||
},
|
Column(
|
||||||
modifier = Modifier.padding(5.dp)
|
Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(all = 5.dp)
|
||||||
) {
|
) {
|
||||||
Text(text = chromeCast.model)
|
Card(modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.clickable {
|
||||||
|
buttonCallBack()
|
||||||
|
}) {
|
||||||
|
Row {
|
||||||
|
val image_id = when (chromeCast.model) {
|
||||||
|
"Chromecast Ultra" -> R.drawable.chromecastultra
|
||||||
|
else -> R.drawable.chromecastv1
|
||||||
|
}
|
||||||
|
Column(
|
||||||
|
modifier = Modifier
|
||||||
|
.height(80.dp)
|
||||||
|
.width(80.dp)
|
||||||
|
.padding(10.dp)
|
||||||
|
) {
|
||||||
|
Image(
|
||||||
|
painter = painterResource(id = image_id),
|
||||||
|
contentDescription = chromeCast.model,
|
||||||
|
Modifier
|
||||||
|
.fillMaxHeight()
|
||||||
|
.fillMaxWidth()
|
||||||
|
|
||||||
|
)
|
||||||
|
}
|
||||||
|
Column(modifier = Modifier.fillMaxWidth().height(80.dp),
|
||||||
|
verticalArrangement = Arrangement.Center) {
|
||||||
|
Text(
|
||||||
|
text = "Name: " + when {
|
||||||
|
chromeCast.title != null -> chromeCast.title
|
||||||
|
chromeCast.name != null -> chromeCast.name
|
||||||
|
else -> chromeCast.address
|
||||||
|
}
|
||||||
|
)
|
||||||
|
Text(
|
||||||
|
text = "Model: " + when {
|
||||||
|
chromeCast.model != null -> chromeCast.model
|
||||||
|
else -> "Unknown"
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun MenuBar(refresh: () -> Unit) {
|
fun MenuBar(refresh: () -> Unit, add: () -> Unit) {
|
||||||
TopAppBar(
|
TopAppBar(
|
||||||
title = {
|
title = {
|
||||||
Text("Caster")
|
Text("Caster")
|
||||||
|
@ -103,16 +254,14 @@ class MainActivity : ComponentActivity() {
|
||||||
IconButton(onClick = refresh) {
|
IconButton(onClick = refresh) {
|
||||||
Icon(
|
Icon(
|
||||||
Icons.Filled.Refresh,
|
Icons.Filled.Refresh,
|
||||||
contentDescription = "Refresh"
|
contentDescription = stringResource(id = R.string.refresh)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
IconButton(onClick = {
|
IconButton(onClick = add) {
|
||||||
|
|
||||||
}) {
|
|
||||||
Icon(
|
Icon(
|
||||||
Icons.Filled.Add,
|
Icons.Filled.Add,
|
||||||
contentDescription = "Add"
|
contentDescription = stringResource(id = R.string.add_chromecast)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -123,9 +272,26 @@ class MainActivity : ComponentActivity() {
|
||||||
|
|
||||||
@Preview(showBackground = true)
|
@Preview(showBackground = true)
|
||||||
@Composable
|
@Composable
|
||||||
fun DefaultPreview() {
|
open fun DefaultPreview() {
|
||||||
CasterTheme {
|
CasterTheme {
|
||||||
MenuBar({})
|
Column {
|
||||||
|
MenuBar({}, {})
|
||||||
|
Column(
|
||||||
|
modifier = Modifier
|
||||||
|
.padding(all = 4.dp)
|
||||||
|
.fillMaxWidth(),
|
||||||
|
horizontalAlignment = Alignment.CenterHorizontally
|
||||||
|
) {
|
||||||
|
showChromeCastButton(
|
||||||
|
chromeCast = ChromeCast("127.0.0.1").apply {
|
||||||
|
name = "Chromecast@127.0.0.1"
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -2,18 +2,18 @@ package eu.toldi.balazs.caster
|
||||||
|
|
||||||
import android.annotation.SuppressLint
|
import android.annotation.SuppressLint
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
|
import android.net.Uri
|
||||||
|
import android.net.wifi.WifiManager
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
|
import android.os.Parcelable
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
import androidx.activity.ComponentActivity
|
import androidx.activity.ComponentActivity
|
||||||
import androidx.activity.compose.setContent
|
import androidx.activity.compose.setContent
|
||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.*
|
||||||
import androidx.compose.foundation.layout.Row
|
|
||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
|
||||||
import androidx.compose.foundation.layout.padding
|
|
||||||
import androidx.compose.foundation.lazy.LazyColumn
|
import androidx.compose.foundation.lazy.LazyColumn
|
||||||
import androidx.compose.material.*
|
import androidx.compose.material.*
|
||||||
import androidx.compose.material.icons.Icons
|
import androidx.compose.material.icons.Icons
|
||||||
import androidx.compose.material.icons.filled.*
|
import androidx.compose.material.icons.filled.Refresh
|
||||||
import androidx.compose.runtime.*
|
import androidx.compose.runtime.*
|
||||||
import androidx.compose.runtime.livedata.observeAsState
|
import androidx.compose.runtime.livedata.observeAsState
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
|
@ -21,30 +21,47 @@ import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
import androidx.compose.ui.tooling.preview.Preview
|
import androidx.compose.ui.tooling.preview.Preview
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
|
import androidx.core.content.ContextCompat
|
||||||
import androidx.lifecycle.ViewModelProvider
|
import androidx.lifecycle.ViewModelProvider
|
||||||
import androidx.lifecycle.lifecycleScope
|
import androidx.lifecycle.lifecycleScope
|
||||||
import com.yausername.youtubedl_android.YoutubeDL
|
import com.yausername.youtubedl_android.YoutubeDL
|
||||||
import com.yausername.youtubedl_android.YoutubeDLException
|
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.model.ChromeCastViewModel
|
||||||
|
import eu.toldi.balazs.caster.services.ChromecastManagerService
|
||||||
import eu.toldi.balazs.caster.ui.theme.CasterTheme
|
import eu.toldi.balazs.caster.ui.theme.CasterTheme
|
||||||
import kotlinx.coroutines.*
|
import kotlinx.coroutines.launch
|
||||||
import kotlinx.coroutines.Dispatchers.IO
|
|
||||||
import su.litvak.chromecast.api.v2.Application
|
|
||||||
import su.litvak.chromecast.api.v2.ChromeCast
|
import su.litvak.chromecast.api.v2.ChromeCast
|
||||||
import su.litvak.chromecast.api.v2.ChromeCasts
|
|
||||||
import java.net.Inet4Address
|
|
||||||
import java.net.InetAddress
|
|
||||||
import java.net.NetworkInterface
|
|
||||||
|
|
||||||
class ShareRecieverActivity : ComponentActivity() {
|
class ShareRecieverActivity : MainActivity() {
|
||||||
|
|
||||||
@SuppressLint("CoroutineCreationDuringComposition")
|
@SuppressLint("CoroutineCreationDuringComposition")
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
if (intent?.action != Intent.ACTION_SEND || intent.type != "text/plain") {
|
if (intent?.action != Intent.ACTION_SEND ) {
|
||||||
finish()
|
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 {
|
try {
|
||||||
YoutubeDL.getInstance().init(application)
|
YoutubeDL.getInstance().init(application)
|
||||||
} catch (e: YoutubeDLException) {
|
} catch (e: YoutubeDLException) {
|
||||||
|
@ -52,14 +69,15 @@ class ShareRecieverActivity : ComponentActivity() {
|
||||||
}
|
}
|
||||||
setContent {
|
setContent {
|
||||||
CasterTheme {
|
CasterTheme {
|
||||||
val viewModel = ViewModelProvider(this).get(ChromeCastViewModel::class.java)
|
if (!isViewModelInitialised())
|
||||||
|
viewModel = ViewModelProvider(this).get(ChromeCastViewModel::class.java)
|
||||||
viewModel.startScanning()
|
viewModel.startScanning()
|
||||||
val chromeCastState = viewModel.chromeCasts.observeAsState(emptyList())
|
val chromeCastState = viewModel.chromeCasts.observeAsState(emptyList())
|
||||||
val chromeCasts = chromeCastState.value
|
val chromeCasts = chromeCastState.value
|
||||||
// A surface container using the 'background' color from the theme
|
// A surface container using the 'background' color from the theme
|
||||||
Surface(color = MaterialTheme.colors.background) {
|
Surface(color = MaterialTheme.colors.background) {
|
||||||
Column {
|
Column {
|
||||||
MenuBar{
|
MenuBar {
|
||||||
viewModel.refresh()
|
viewModel.refresh()
|
||||||
}
|
}
|
||||||
var enabled by remember {
|
var enabled by remember {
|
||||||
|
@ -73,7 +91,16 @@ class ShareRecieverActivity : ComponentActivity() {
|
||||||
) {
|
) {
|
||||||
item {
|
item {
|
||||||
if (chromeCasts.isNotEmpty())
|
if (chromeCasts.isNotEmpty())
|
||||||
Text(text = "Available chromecasts:")
|
Text(text = stringResource(id = R.string.available_chromecasts))
|
||||||
|
else {
|
||||||
|
Column(
|
||||||
|
modifier = Modifier.fillMaxSize(),
|
||||||
|
horizontalAlignment = Alignment.CenterHorizontally
|
||||||
|
) {
|
||||||
|
Text(stringResource(id = R.string.looking_for_devices))
|
||||||
|
CircularProgressIndicator()
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
items(chromeCasts.size) { index ->
|
items(chromeCasts.size) { index ->
|
||||||
showChromeCastButton(
|
showChromeCastButton(
|
||||||
|
@ -83,8 +110,8 @@ class ShareRecieverActivity : ComponentActivity() {
|
||||||
link = link
|
link = link
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
item{
|
item {
|
||||||
if(enabled.not()){
|
if (enabled.not()) {
|
||||||
CircularProgressIndicator()
|
CircularProgressIndicator()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -103,21 +130,23 @@ class ShareRecieverActivity : ComponentActivity() {
|
||||||
chromeCast: ChromeCast,
|
chromeCast: ChromeCast,
|
||||||
link: String
|
link: String
|
||||||
) {
|
) {
|
||||||
Button(
|
showChromeCastButton(chromeCast = chromeCast, buttonCallBack = {
|
||||||
onClick = {
|
if (enabled) {
|
||||||
onEnableChanged(false)
|
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 {
|
lifecycleScope.launch {
|
||||||
ChromeCastHelper.chromeCast = chromeCast
|
ChromeCastHelper.chromeCast = chromeCast
|
||||||
ChromeCastHelper.castLink(link) {
|
ChromeCastHelper.castLink(link) {
|
||||||
finish()
|
//finish()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
}
|
||||||
modifier = Modifier.padding(5.dp),
|
})
|
||||||
enabled = enabled
|
|
||||||
) {
|
|
||||||
Text(text = chromeCast.model)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
|
@ -131,16 +160,7 @@ class ShareRecieverActivity : ComponentActivity() {
|
||||||
IconButton(onClick = refresh) {
|
IconButton(onClick = refresh) {
|
||||||
Icon(
|
Icon(
|
||||||
Icons.Filled.Refresh,
|
Icons.Filled.Refresh,
|
||||||
contentDescription = "Refresh"
|
contentDescription = stringResource(id = R.string.refresh)
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
IconButton(onClick = {
|
|
||||||
|
|
||||||
}) {
|
|
||||||
Icon(
|
|
||||||
Icons.Filled.Add,
|
|
||||||
contentDescription = "Add"
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -151,7 +171,7 @@ class ShareRecieverActivity : ComponentActivity() {
|
||||||
|
|
||||||
@Preview(showBackground = true)
|
@Preview(showBackground = true)
|
||||||
@Composable
|
@Composable
|
||||||
fun DefaultPreview() {
|
override fun DefaultPreview() {
|
||||||
CasterTheme {
|
CasterTheme {
|
||||||
MenuBar {}
|
MenuBar {}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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()
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,41 +1,46 @@
|
||||||
package eu.toldi.balazs.caster.model
|
package eu.toldi.balazs.caster.model
|
||||||
|
|
||||||
|
import android.net.wifi.WifiManager
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
import androidx.compose.runtime.MutableState
|
|
||||||
import androidx.compose.runtime.getValue
|
|
||||||
import androidx.compose.runtime.setValue
|
|
||||||
import androidx.compose.runtime.mutableStateOf
|
|
||||||
import androidx.compose.runtime.remember
|
|
||||||
import androidx.lifecycle.LiveData
|
import androidx.lifecycle.LiveData
|
||||||
import androidx.lifecycle.MutableLiveData
|
import androidx.lifecycle.MutableLiveData
|
||||||
import androidx.lifecycle.ViewModel
|
import androidx.lifecycle.ViewModel
|
||||||
import androidx.lifecycle.viewModelScope
|
import androidx.lifecycle.viewModelScope
|
||||||
import kotlinx.coroutines.Dispatchers.IO
|
import kotlinx.coroutines.Dispatchers.IO
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import su.litvak.chromecast.api.v2.*
|
import su.litvak.chromecast.api.v2.ChromeCast
|
||||||
|
import su.litvak.chromecast.api.v2.ChromeCasts
|
||||||
|
import su.litvak.chromecast.api.v2.ChromeCastsListener
|
||||||
|
import java.lang.String
|
||||||
import java.net.InetAddress
|
import java.net.InetAddress
|
||||||
import java.net.NetworkInterface
|
import java.net.NetworkInterface
|
||||||
import javax.jmdns.ServiceEvent
|
import java.net.UnknownHostException
|
||||||
import javax.jmdns.ServiceListener
|
|
||||||
|
|
||||||
class ChromeCastViewModel : ViewModel(),ChromeCastsListener {
|
|
||||||
private val _chromecasts : MutableLiveData<List<ChromeCast>> = MutableLiveData<List<ChromeCast>>(listOf())
|
class ChromeCastViewModel : ViewModel(), ChromeCastsListener {
|
||||||
val chromeCasts : LiveData<List<ChromeCast>>
|
private val _chromecasts: MutableLiveData<List<ChromeCast>> =
|
||||||
|
MutableLiveData<List<ChromeCast>>(listOf())
|
||||||
|
val chromeCasts: LiveData<List<ChromeCast>>
|
||||||
get() = _chromecasts
|
get() = _chromecasts
|
||||||
|
|
||||||
init {
|
init {
|
||||||
|
|
||||||
ChromeCasts.registerListener(this)
|
ChromeCasts.registerListener(this)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun addChromecast(chromeCast: ChromeCast) {
|
||||||
|
_chromecasts.postValue(_chromecasts.value!!.plus(chromeCast))
|
||||||
|
}
|
||||||
|
|
||||||
override fun newChromeCastDiscovered(chromeCast: ChromeCast?) {
|
override fun newChromeCastDiscovered(chromeCast: ChromeCast?) {
|
||||||
if(chromeCast != null) {
|
if (chromeCast != null) {
|
||||||
Log.i(null,"Found ${chromeCast.title}")
|
Log.i(null, "Found ${chromeCast.title}")
|
||||||
_chromecasts.postValue(_chromecasts.value!!.plus(chromeCast))
|
_chromecasts.postValue(_chromecasts.value!!.plus(chromeCast))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun chromeCastRemoved(chromeCast: ChromeCast?) {
|
override fun chromeCastRemoved(chromeCast: ChromeCast?) {
|
||||||
if(chromeCast != null) {
|
if (chromeCast != null) {
|
||||||
Log.i(null,"Lost ${chromeCast.title}")
|
Log.i(null,"Lost ${chromeCast.title}")
|
||||||
_chromecasts.postValue(_chromecasts.value!!.minus(chromeCast))
|
_chromecasts.postValue(_chromecasts.value!!.minus(chromeCast))
|
||||||
}
|
}
|
||||||
|
@ -50,7 +55,13 @@ class ChromeCastViewModel : ViewModel(),ChromeCastsListener {
|
||||||
|
|
||||||
fun startScanning() {
|
fun startScanning() {
|
||||||
viewModelScope.launch(IO) {
|
viewModelScope.launch(IO) {
|
||||||
ChromeCasts.startDiscovery(getIPv4Address())
|
val address = getIPv4Address()
|
||||||
|
if (address.toString().startsWith('/')) {
|
||||||
|
Log.e("Caster", address.toString().drop(1))
|
||||||
|
ChromeCasts.startDiscovery(InetAddress.getByName(address.toString().drop(1)))
|
||||||
|
} else {
|
||||||
|
ChromeCasts.startDiscovery(address)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
fun getIPv4Address(): InetAddress? {
|
fun getIPv4Address(): InetAddress? {
|
||||||
|
@ -64,4 +75,24 @@ class ChromeCastViewModel : ViewModel(),ChromeCastsListener {
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun getDeviceIpAddress(wifi: WifiManager): InetAddress? {
|
||||||
|
var result: InetAddress? = null
|
||||||
|
try {
|
||||||
|
// default to Android localhost
|
||||||
|
result = InetAddress.getByName("10.0.0.2")
|
||||||
|
|
||||||
|
// figure out our wifi address, otherwise bail
|
||||||
|
val wifiinfo = wifi.connectionInfo
|
||||||
|
val intaddr = wifiinfo.ipAddress
|
||||||
|
val byteaddr = byteArrayOf(
|
||||||
|
(intaddr and 0xff).toByte(), (intaddr shr 8 and 0xff).toByte(),
|
||||||
|
(intaddr shr 16 and 0xff).toByte(), (intaddr shr 24 and 0xff).toByte()
|
||||||
|
)
|
||||||
|
result = InetAddress.getByAddress(byteaddr)
|
||||||
|
} catch (ex: UnknownHostException) {
|
||||||
|
Log.w("Caster", String.format("getDeviceIpAddress Error: %s", ex.message))
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
|
@ -1,19 +1,21 @@
|
||||||
package eu.toldi.balazs.caster.model
|
package eu.toldi.balazs.caster.model
|
||||||
|
|
||||||
|
import android.net.Uri
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
import androidx.compose.runtime.State
|
import androidx.compose.runtime.State
|
||||||
import androidx.compose.runtime.mutableStateOf
|
import androidx.compose.runtime.mutableStateOf
|
||||||
import androidx.lifecycle.ViewModel
|
import androidx.lifecycle.ViewModel
|
||||||
import androidx.lifecycle.viewModelScope
|
import androidx.lifecycle.viewModelScope
|
||||||
import eu.toldi.balazs.caster.ChromeCastHelper
|
import eu.toldi.balazs.caster.ChromeCastHelper
|
||||||
|
import eu.toldi.balazs.caster.services.ChromecastManagerService
|
||||||
import kotlinx.coroutines.Dispatchers.IO
|
import kotlinx.coroutines.Dispatchers.IO
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import su.litvak.chromecast.api.v2.ChromeCast
|
import su.litvak.chromecast.api.v2.ChromeCast
|
||||||
import su.litvak.chromecast.api.v2.ChromeCastSpontaneousEvent
|
|
||||||
import su.litvak.chromecast.api.v2.ChromeCastSpontaneousEventListener
|
|
||||||
import su.litvak.chromecast.api.v2.MediaStatus
|
import su.litvak.chromecast.api.v2.MediaStatus
|
||||||
|
import java.net.InetAddress
|
||||||
|
import java.net.NetworkInterface
|
||||||
|
|
||||||
class ChromecastManageViewmodel : ViewModel(), ChromeCastSpontaneousEventListener {
|
class ChromecastManageViewmodel : ViewModel() {
|
||||||
|
|
||||||
private val _mediaState = mutableStateOf<MediaStatus?>(null)
|
private val _mediaState = mutableStateOf<MediaStatus?>(null)
|
||||||
val mediaStatus: State<MediaStatus?>
|
val mediaStatus: State<MediaStatus?>
|
||||||
|
@ -23,13 +25,14 @@ class ChromecastManageViewmodel : ViewModel(), ChromeCastSpontaneousEventListene
|
||||||
|
|
||||||
init {
|
init {
|
||||||
viewModelScope.launch(IO) {
|
viewModelScope.launch(IO) {
|
||||||
|
fetchMediaStatus()
|
||||||
while (true) {
|
while (true) {
|
||||||
fetchMediaStatus()
|
fetchMediaStatus()
|
||||||
Thread.sleep(1000)
|
Thread.sleep(3000)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
/*
|
||||||
override fun spontaneousEventReceived(event: ChromeCastSpontaneousEvent?) {
|
override fun spontaneousEventReceived(event: ChromeCastSpontaneousEvent?) {
|
||||||
Log.e(null, event?.type.toString())
|
Log.e(null, event?.type.toString())
|
||||||
if (event != null) {
|
if (event != null) {
|
||||||
|
@ -37,11 +40,15 @@ class ChromecastManageViewmodel : ViewModel(), ChromeCastSpontaneousEventListene
|
||||||
_mediaState.value = event.data as MediaStatus
|
_mediaState.value = event.data as MediaStatus
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}*/
|
||||||
|
|
||||||
fun fetchMediaStatus() {
|
fun fetchMediaStatus() {
|
||||||
viewModelScope.launch(IO) {
|
viewModelScope.launch(IO) {
|
||||||
_mediaState.value = ChromeCastHelper.fetchMediaStatus()
|
try {
|
||||||
|
_mediaState.value = ChromeCastHelper.fetchMediaStatus()
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Log.e(null, e.stackTraceToString())
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -57,6 +64,13 @@ class ChromecastManageViewmodel : ViewModel(), ChromeCastSpontaneousEventListene
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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() {
|
fun stopApp() {
|
||||||
viewModelScope.launch(IO) {
|
viewModelScope.launch(IO) {
|
||||||
ChromeCastHelper.stopApp()
|
ChromeCastHelper.stopApp()
|
||||||
|
@ -80,4 +94,27 @@ class ChromecastManageViewmodel : ViewModel(), ChromeCastSpontaneousEventListene
|
||||||
ChromeCastHelper.setVolume(f)
|
ChromeCastHelper.setVolume(f)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun increaseVolume(){
|
||||||
|
viewModelScope.launch(IO) {
|
||||||
|
setVolume(chromeCast.status.volume.level + 0.05f)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun decreaseVolume(){
|
||||||
|
viewModelScope.launch(IO) {
|
||||||
|
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
|
||||||
|
}
|
||||||
}
|
}
|
|
@ -6,9 +6,6 @@ import android.app.PendingIntent
|
||||||
import android.app.Service
|
import android.app.Service
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.media.MediaPlayer
|
import android.media.MediaPlayer
|
||||||
import android.media.session.MediaController
|
|
||||||
import android.media.session.MediaSession
|
|
||||||
import android.media.session.PlaybackState
|
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
import android.os.IBinder
|
import android.os.IBinder
|
||||||
import android.support.v4.media.session.MediaControllerCompat
|
import android.support.v4.media.session.MediaControllerCompat
|
||||||
|
@ -19,13 +16,21 @@ import androidx.core.app.NotificationCompat
|
||||||
import eu.toldi.balazs.caster.App.Companion.CHANNEL_ID
|
import eu.toldi.balazs.caster.App.Companion.CHANNEL_ID
|
||||||
import eu.toldi.balazs.caster.ChromeCastHelper
|
import eu.toldi.balazs.caster.ChromeCastHelper
|
||||||
import eu.toldi.balazs.caster.R
|
import eu.toldi.balazs.caster.R
|
||||||
|
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.*
|
||||||
import kotlinx.coroutines.Dispatchers.IO
|
import kotlinx.coroutines.Dispatchers.IO
|
||||||
|
|
||||||
import su.litvak.chromecast.api.v2.ChromeCast
|
import su.litvak.chromecast.api.v2.ChromeCast
|
||||||
import su.litvak.chromecast.api.v2.Media
|
import su.litvak.chromecast.api.v2.Media
|
||||||
|
|
||||||
import su.litvak.chromecast.api.v2.MediaStatus
|
import su.litvak.chromecast.api.v2.MediaStatus
|
||||||
|
import java.io.File
|
||||||
|
|
||||||
|
|
||||||
class ChromecastManagerService : Service() {
|
class ChromecastManagerService : Service() {
|
||||||
|
@ -38,6 +43,9 @@ class ChromecastManagerService : Service() {
|
||||||
const val ACTION_NEXT = "action_next"
|
const val ACTION_NEXT = "action_next"
|
||||||
const val ACTION_PREVIOUS = "action_previous"
|
const val ACTION_PREVIOUS = "action_previous"
|
||||||
const val ACTION_STOP = "action_stop"
|
const val ACTION_STOP = "action_stop"
|
||||||
|
const val ACTION_SETFILE = "action_file"
|
||||||
|
|
||||||
|
const val PORT = 3080
|
||||||
}
|
}
|
||||||
|
|
||||||
private val mMediaPlayer = MediaPlayer()
|
private val mMediaPlayer = MediaPlayer()
|
||||||
|
@ -45,10 +53,33 @@ class ChromecastManagerService : Service() {
|
||||||
private lateinit var mController: MediaControllerCompat
|
private lateinit var mController: MediaControllerCompat
|
||||||
private lateinit var pendingIntent : PendingIntent
|
private lateinit var pendingIntent : PendingIntent
|
||||||
private var mediaStatus: MediaStatus? = null
|
private var mediaStatus: MediaStatus? = null
|
||||||
|
private var file : File? = null
|
||||||
|
|
||||||
override fun onBind(p0: Intent?): IBinder? = null
|
override fun onBind(p0: Intent?): IBinder? = null
|
||||||
private lateinit var chromeCast: ChromeCast
|
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)
|
@RequiresApi(Build.VERSION_CODES.M)
|
||||||
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
|
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
|
||||||
|
@ -74,7 +105,8 @@ class ChromecastManagerService : Service() {
|
||||||
chromeCast = ChromeCast(address)
|
chromeCast = ChromeCast(address)
|
||||||
chromeCast.name = name
|
chromeCast.name = name
|
||||||
val notificationIntent = Intent(this, ChromecastManagerService::class.java)
|
val notificationIntent = Intent(this, ChromecastManagerService::class.java)
|
||||||
pendingIntent = PendingIntent.getActivity(this, 0, notificationIntent, 0)
|
pendingIntent =
|
||||||
|
PendingIntent.getActivity(this, 0, notificationIntent, PendingIntent.FLAG_MUTABLE)
|
||||||
|
|
||||||
val notification: Notification = NotificationCompat.Builder(this, CHANNEL_ID)
|
val notification: Notification = NotificationCompat.Builder(this, CHANNEL_ID)
|
||||||
.setContentTitle("Caster for ${chromeCast.name}@${address}")
|
.setContentTitle("Caster for ${chromeCast.name}@${address}")
|
||||||
|
@ -82,8 +114,11 @@ class ChromecastManagerService : Service() {
|
||||||
.setContentIntent(pendingIntent).build()
|
.setContentIntent(pendingIntent).build()
|
||||||
|
|
||||||
startForeground(1, notification)
|
startForeground(1, notification)
|
||||||
|
CoroutineScope(IO).launch {
|
||||||
|
server.start(wait = true)
|
||||||
|
}
|
||||||
|
CoroutineScope(IO).launch {
|
||||||
|
|
||||||
GlobalScope.launch(IO) {
|
|
||||||
while (true) {
|
while (true) {
|
||||||
try {
|
try {
|
||||||
ChromeCastHelper.chromeCast = chromeCast
|
ChromeCastHelper.chromeCast = chromeCast
|
||||||
|
@ -100,7 +135,7 @@ class ChromecastManagerService : Service() {
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Log.e(null,e.stackTraceToString())
|
Log.e(null,e.stackTraceToString())
|
||||||
}
|
}
|
||||||
Thread.sleep(1000)
|
Thread.sleep(10500)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -117,6 +152,11 @@ class ChromecastManagerService : Service() {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
override fun onDestroy() {
|
||||||
|
server.stop(1_000, 2_000)
|
||||||
|
super.onDestroy()
|
||||||
|
}
|
||||||
|
|
||||||
private suspend fun buildNotification() {
|
private suspend fun buildNotification() {
|
||||||
var status = chromeCast.status
|
var status = chromeCast.status
|
||||||
Log.d(null, status.applications.toString())
|
Log.d(null, status.applications.toString())
|
||||||
|
|
BIN
app/src/main/res/drawable-hdpi/chromecastultra.png
Normal file
After Width: | Height: | Size: 18 KiB |
BIN
app/src/main/res/drawable-hdpi/chromecastv1.png
Normal file
After Width: | Height: | Size: 5.9 KiB |
BIN
app/src/main/res/drawable-ldpi/chromecastultra.png
Normal file
After Width: | Height: | Size: 7.7 KiB |
BIN
app/src/main/res/drawable-ldpi/chromecastv1.png
Normal file
After Width: | Height: | Size: 2.6 KiB |
BIN
app/src/main/res/drawable-mdpi/chromecastultra.png
Normal file
After Width: | Height: | Size: 11 KiB |
BIN
app/src/main/res/drawable-mdpi/chromecastv1.png
Normal file
After Width: | Height: | Size: 3.6 KiB |
BIN
app/src/main/res/drawable-xhdpi/chromecastultra.png
Normal file
After Width: | Height: | Size: 27 KiB |
BIN
app/src/main/res/drawable-xhdpi/chromecastv1.png
Normal file
After Width: | Height: | Size: 8.4 KiB |
BIN
app/src/main/res/drawable-xxhdpi/chromecastultra.png
Normal file
After Width: | Height: | Size: 48 KiB |
BIN
app/src/main/res/drawable-xxhdpi/chromecastv1.png
Normal file
After Width: | Height: | Size: 14 KiB |
BIN
app/src/main/res/drawable-xxxhdpi/chromecastultra.png
Normal file
After Width: | Height: | Size: 72 KiB |
BIN
app/src/main/res/drawable-xxxhdpi/chromecastv1.png
Normal file
After Width: | Height: | Size: 21 KiB |
|
@ -2,4 +2,21 @@
|
||||||
<string name="app_name">Caster</string>
|
<string name="app_name">Caster</string>
|
||||||
<string name="title_activity_chromecast_manager">ChromecastManagerActivity</string>
|
<string name="title_activity_chromecast_manager">ChromecastManagerActivity</string>
|
||||||
<string name="title_activity_share_reciever">Choose Chromecast</string>
|
<string name="title_activity_share_reciever">Choose Chromecast</string>
|
||||||
|
<string name="refresh">Refresh</string>
|
||||||
|
<string name="add_chromecast">Add Chromecast</string>
|
||||||
|
<string name="cancel">Cancel</string>
|
||||||
|
<string name="ip_address">IP address of the Chromecast</string>
|
||||||
|
<string name="available_chromecasts">Available Chromecasts:</string>
|
||||||
|
<string name="looking_for_devices">Looking for Chromecasts on your network...</string>
|
||||||
|
<string name="cast">Cast</string>
|
||||||
|
<string name="stop">Stop</string>
|
||||||
|
<string name="pause">Pause</string>
|
||||||
|
<string name="resume">Resume</string>
|
||||||
|
<string name="play">Play</string>
|
||||||
|
<string name="fastforward">Fast Forward</string>
|
||||||
|
<string name="rewind">Rewind</string>
|
||||||
|
<string name="back">Back</string>
|
||||||
|
<string name="now_playing">Now playing</string>
|
||||||
|
<string name="cast_url">Cast URL</string>
|
||||||
|
|
||||||
</resources>
|
</resources>
|
|
@ -8,7 +8,7 @@ buildscript {
|
||||||
mavenCentral()
|
mavenCentral()
|
||||||
}
|
}
|
||||||
dependencies {
|
dependencies {
|
||||||
classpath "com.android.tools.build:gradle:7.0.2"
|
classpath 'com.android.tools.build:gradle:7.0.3'
|
||||||
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:1.5.21"
|
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:1.5.21"
|
||||||
|
|
||||||
// NOTE: Do not place your application dependencies here; they belong
|
// NOTE: Do not place your application dependencies here; they belong
|
||||||
|
|