Skip to content

Commit 405be2e

Browse files
author
code3-dev
committed
Add swipe gesture controls for brightness and volume in video player
1 parent a9cfd94 commit 405be2e

File tree

2 files changed

+243
-0
lines changed

2 files changed

+243
-0
lines changed

app/src/main/AndroidManifest.xml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@
55
<!-- Internet and network permissions -->
66
<uses-permission android:name="android.permission.INTERNET" />
77
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
8+
<uses-permission android:name="android.permission.WRITE_SETTINGS" />
9+
<uses-permission android:name="android.permission.MODIFY_AUDIO_SETTINGS" />
810

911
<!-- Declare that the app supports Android TV -->
1012
<uses-feature

app/src/main/java/com/pira/ccloud/VideoPlayerActivity.kt

Lines changed: 241 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,17 @@
11
package com.pira.ccloud
22

3+
import android.app.Activity
34
import android.content.Context
45
import android.content.Intent
56
import android.content.pm.ActivityInfo
67
import android.graphics.Typeface
8+
import android.media.AudioManager
79
import android.net.Uri
810
import android.os.Build
911
import android.os.Bundle
1012
import android.os.Handler
1113
import android.os.Looper
14+
import android.provider.Settings
1215
import android.util.TypedValue
1316
import android.view.GestureDetector
1417
import android.view.MotionEvent
@@ -17,10 +20,12 @@ import android.view.ViewGroup
1720
import android.view.WindowInsets
1821
import android.view.WindowInsetsController
1922
import android.widget.FrameLayout
23+
import kotlin.math.abs
2024
import androidx.activity.ComponentActivity
2125
import androidx.activity.compose.setContent
2226
import androidx.compose.foundation.background
2327
import androidx.compose.foundation.clickable
28+
import androidx.compose.foundation.gestures.detectDragGestures
2429
import androidx.compose.foundation.gestures.detectTapGestures
2530
import androidx.compose.foundation.layout.Box
2631
import androidx.compose.foundation.layout.Column
@@ -34,11 +39,13 @@ import androidx.compose.foundation.layout.width
3439
import androidx.compose.foundation.shape.RoundedCornerShape
3540
import androidx.compose.material.icons.Icons
3641
import androidx.compose.material.icons.automirrored.filled.ArrowBack
42+
import androidx.compose.material.icons.filled.BrightnessMedium
3743
import androidx.compose.material.icons.filled.Forward
3844
import androidx.compose.material.icons.filled.Pause
3945
import androidx.compose.material.icons.filled.PlayArrow
4046
import androidx.compose.material.icons.filled.Replay
4147
import androidx.compose.material.icons.filled.Speed
48+
import androidx.compose.material.icons.filled.VolumeUp
4249
import androidx.compose.material3.DropdownMenu
4350
import androidx.compose.material3.DropdownMenuItem
4451
import androidx.compose.material3.Icon
@@ -109,6 +116,7 @@ fun PlayerView.setSubtitleColors(settings: SubtitleSettings, typeface: Typeface?
109116
class VideoPlayerActivity : ComponentActivity() {
110117
companion object {
111118
const val EXTRA_VIDEO_URL = "video_url"
119+
const val REQUEST_WRITE_SETTINGS = 1001
112120

113121
fun start(context: Context, videoUrl: String) {
114122
val intent = Intent(context, VideoPlayerActivity::class.java).apply {
@@ -133,6 +141,9 @@ class VideoPlayerActivity : ComponentActivity() {
133141
// Keep screen on while in video player
134142
window.addFlags(android.view.WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON)
135143

144+
// Check and request brightness control permission if needed
145+
checkAndRequestBrightnessPermission()
146+
136147
videoUrl = intent.getStringExtra(EXTRA_VIDEO_URL)
137148

138149
if (videoUrl != null) {
@@ -146,6 +157,28 @@ class VideoPlayerActivity : ComponentActivity() {
146157
}
147158
}
148159

160+
private fun checkAndRequestBrightnessPermission() {
161+
// Check if we have permission to write system settings
162+
if (!Settings.System.canWrite(this)) {
163+
// Request permission to write system settings
164+
val intent = Intent(Settings.ACTION_MANAGE_WRITE_SETTINGS)
165+
intent.data = Uri.parse("package:${packageName}")
166+
startActivityForResult(intent, REQUEST_WRITE_SETTINGS)
167+
}
168+
}
169+
170+
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
171+
super.onActivityResult(requestCode, resultCode, data)
172+
if (requestCode == REQUEST_WRITE_SETTINGS) {
173+
// Permission result for writing system settings
174+
if (Settings.System.canWrite(this)) {
175+
// Permission granted
176+
} else {
177+
// Permission denied - we'll work with window-level brightness only
178+
}
179+
}
180+
}
181+
149182
// Handle TV remote control key events
150183
override fun onKeyDown(keyCode: Int, event: android.view.KeyEvent?): Boolean {
151184
exoPlayer?.let { player ->
@@ -256,6 +289,12 @@ fun VideoPlayerScreen(
256289
var playbackSpeed by remember { mutableStateOf(1.0f) }
257290
var showSpeedDropdown by remember { mutableStateOf(false) }
258291

292+
// Brightness and volume control states
293+
var showBrightnessIndicator by remember { mutableStateOf(false) }
294+
var showVolumeIndicator by remember { mutableStateOf(false) }
295+
var brightnessLevel by remember { mutableStateOf(0f) }
296+
var volumeLevel by remember { mutableStateOf(0f) }
297+
259298
// Predefined playback speed options
260299
val speedOptions = remember {
261300
listOf(0.25f, 0.5f, 0.75f, 1.0f, 1.25f, 1.5f, 1.75f, 2.0f, 2.5f, 3.0f, 3.5f)
@@ -522,6 +561,150 @@ fun VideoPlayerScreen(
522561
}
523562
)
524563
}
564+
// Add swipe gesture detection for brightness and volume control
565+
.pointerInput(Unit) {
566+
var initialX = 0f
567+
var initialY = 0f
568+
var isTracking = false
569+
var trackingSide: String? = null // "left" for brightness, "right" for volume
570+
var initialBrightness = 0f
571+
var initialVolume = 0f
572+
573+
detectDragGestures(
574+
onDragStart = { offset ->
575+
initialX = offset.x
576+
initialY = offset.y
577+
isTracking = true
578+
579+
// Determine which side of the screen the gesture started on
580+
val screenWidth = size.width
581+
if (initialX < screenWidth * 0.5f) {
582+
trackingSide = "left" // Left side for brightness
583+
} else {
584+
trackingSide = "right" // Right side for volume
585+
}
586+
587+
// Get initial brightness and volume levels
588+
when (trackingSide) {
589+
"left" -> {
590+
val window = (context as Activity).window
591+
val layoutParams = window.attributes
592+
initialBrightness = layoutParams.screenBrightness
593+
if (initialBrightness < 0) {
594+
// If brightness is set to system default, get the current system brightness
595+
try {
596+
val brightnessMode = Settings.System.getInt(
597+
context.contentResolver,
598+
Settings.System.SCREEN_BRIGHTNESS_MODE
599+
)
600+
if (brightnessMode == Settings.System.SCREEN_BRIGHTNESS_MODE_AUTOMATIC) {
601+
// For automatic brightness, we'll use a default value
602+
initialBrightness = 0.5f // Default to 50%
603+
} else {
604+
val systemBrightness = Settings.System.getInt(
605+
context.contentResolver,
606+
Settings.System.SCREEN_BRIGHTNESS
607+
)
608+
initialBrightness = systemBrightness / 255f
609+
}
610+
} catch (e: Exception) {
611+
initialBrightness = 0.5f // Default to 50%
612+
}
613+
}
614+
brightnessLevel = initialBrightness
615+
showBrightnessIndicator = true
616+
}
617+
"right" -> {
618+
val audioManager = context.getSystemService(Context.AUDIO_SERVICE) as AudioManager
619+
val maxVolume = audioManager.getStreamMaxVolume(AudioManager.STREAM_MUSIC)
620+
val currentVolume = audioManager.getStreamVolume(AudioManager.STREAM_MUSIC)
621+
initialVolume = currentVolume.toFloat()
622+
volumeLevel = initialVolume / maxVolume.toFloat()
623+
showVolumeIndicator = true
624+
}
625+
}
626+
},
627+
onDragEnd = {
628+
isTracking = false
629+
trackingSide = null
630+
631+
// Hide indicators after a delay
632+
CoroutineScope(Dispatchers.Main).launch {
633+
delay(1000)
634+
showBrightnessIndicator = false
635+
showVolumeIndicator = false
636+
}
637+
},
638+
onDragCancel = {
639+
isTracking = false
640+
trackingSide = null
641+
showBrightnessIndicator = false
642+
showVolumeIndicator = false
643+
},
644+
onDrag = { change, dragAmount ->
645+
if (!isTracking) return@detectDragGestures
646+
647+
val dragY = dragAmount.y
648+
649+
// Only process vertical drag gestures
650+
if (abs(dragY) > abs(change.position.x - initialX) * 0.5f) {
651+
when (trackingSide) {
652+
"left" -> {
653+
// Adjust brightness based on vertical drag (up = increase, down = decrease)
654+
val delta = -dragY * 0.01f // Invert Y axis (up is negative)
655+
brightnessLevel = (initialBrightness + delta).coerceIn(0f, 1f)
656+
657+
try {
658+
// Apply brightness change to the current window
659+
val window = (context as Activity).window
660+
val layoutParams = window.attributes
661+
layoutParams.screenBrightness = brightnessLevel
662+
window.attributes = layoutParams
663+
664+
// Also try to change system brightness if we have permission
665+
if (Settings.System.canWrite(context)) {
666+
Settings.System.putInt(
667+
context.contentResolver,
668+
Settings.System.SCREEN_BRIGHTNESS,
669+
(brightnessLevel * 255).toInt()
670+
)
671+
}
672+
673+
// Update UI indicator
674+
showBrightnessIndicator = true
675+
} catch (e: Exception) {
676+
// Ignore permission or other errors
677+
}
678+
}
679+
"right" -> {
680+
// Adjust volume based on vertical drag (up = increase, down = decrease)
681+
val audioManager = context.getSystemService(Context.AUDIO_SERVICE) as AudioManager
682+
val maxVolume = audioManager.getStreamMaxVolume(AudioManager.STREAM_MUSIC)
683+
val delta = -dragY * 0.01f * maxVolume // Invert Y axis (up is negative)
684+
val newVolume = (initialVolume + delta).coerceIn(0f, maxVolume.toFloat())
685+
686+
try {
687+
// Apply volume change
688+
audioManager.setStreamVolume(
689+
AudioManager.STREAM_MUSIC,
690+
newVolume.toInt(),
691+
0 // No flags
692+
)
693+
694+
// Update volume level for UI indicator
695+
volumeLevel = newVolume / maxVolume.toFloat()
696+
697+
// Update UI indicator
698+
showVolumeIndicator = true
699+
} catch (e: Exception) {
700+
// Ignore permission or other errors
701+
}
702+
}
703+
}
704+
}
705+
}
706+
)
707+
}
525708
) {
526709
// Show error message if player failed to initialize
527710
if (playerError != null) {
@@ -653,6 +836,64 @@ fun VideoPlayerScreen(
653836
}
654837
}
655838

839+
// Brightness indicator
840+
if (showBrightnessIndicator) {
841+
Box(
842+
modifier = Modifier
843+
.fillMaxSize()
844+
.padding(32.dp),
845+
contentAlignment = Alignment.CenterStart
846+
) {
847+
Column(
848+
horizontalAlignment = Alignment.CenterHorizontally,
849+
modifier = Modifier
850+
.background(Color.Black.copy(alpha = 0.7f), RoundedCornerShape(8.dp))
851+
.padding(16.dp)
852+
) {
853+
Icon(
854+
imageVector = Icons.Default.BrightnessMedium,
855+
contentDescription = "Brightness",
856+
tint = Color.White,
857+
modifier = Modifier.size(48.dp)
858+
)
859+
Text(
860+
text = "${(brightnessLevel * 100).toInt()}%",
861+
color = Color.White,
862+
modifier = Modifier.padding(top = 8.dp)
863+
)
864+
}
865+
}
866+
}
867+
868+
// Volume indicator
869+
if (showVolumeIndicator) {
870+
Box(
871+
modifier = Modifier
872+
.fillMaxSize()
873+
.padding(32.dp),
874+
contentAlignment = Alignment.CenterEnd
875+
) {
876+
Column(
877+
horizontalAlignment = Alignment.CenterHorizontally,
878+
modifier = Modifier
879+
.background(Color.Black.copy(alpha = 0.7f), RoundedCornerShape(8.dp))
880+
.padding(16.dp)
881+
) {
882+
Icon(
883+
imageVector = Icons.Default.VolumeUp,
884+
contentDescription = "Volume",
885+
tint = Color.White,
886+
modifier = Modifier.size(48.dp)
887+
)
888+
Text(
889+
text = "${(volumeLevel * 100).toInt()}%",
890+
color = Color.White,
891+
modifier = Modifier.padding(top = 8.dp)
892+
)
893+
}
894+
}
895+
}
896+
656897
// Custom controls overlay
657898
if (showControls) {
658899
Column(

0 commit comments

Comments
 (0)