UI가독성 변경 및 VFD 제어 추가

FakeView incoming call 동작 되도록 처리
This commit is contained in:
ritoseo 2025-09-25 01:46:32 +09:00
parent 267ebbfc44
commit 74803af21b
10 changed files with 888 additions and 77 deletions

View File

@ -9,8 +9,8 @@ android {
applicationId = 'kr.co.rito.ritosip'
minSdkVersion 29
targetSdkVersion 35
versionCode = 104
versionName = '1.0.4'
versionCode = 106
versionName = '1.0.6'
externalNativeBuild {
cmake {
cFlags '-DHAVE_INTTYPES_H -lstdc++'
@ -86,5 +86,11 @@ dependencies {
implementation "androidx.activity:activity-ktx:1.9.3"
implementation "androidx.fragment:fragment-ktx:1.8.5"
implementation 'androidx.media:media:1.7.0'
def camerax_version = "1.3.3" // 1.3.x
implementation "androidx.camera:camera-core:$camerax_version"
implementation "androidx.camera:camera-camera2:$camerax_version"
implementation "androidx.camera:camera-lifecycle:$camerax_version"
implementation "androidx.camera:camera-view:$camerax_version"
}

View File

@ -25,6 +25,10 @@
android:name="android.permission.WRITE_EXTERNAL_STORAGE"
android:maxSdkVersion="32"
tools:ignore="ScopedStorage" />
<uses-permission
android:name="android.permission.MANAGE_EXTERNAL_STORAGE"
android:maxSdkVersion="32"
tools:ignore="ScopedStorage" />
<uses-permission android:name="android.permission.READ_CONTACTS" />
<uses-permission android:name="android.permission.WRITE_CONTACTS" />
<uses-permission android:name="android.permission.CALL_PHONE" />

View File

@ -45,6 +45,7 @@ module_app mwi.so
avcodec_h264enc h264_mediacodec
video_fps 30
mic_volume 20
aux_volume 80
evdev_device /dev/input/event0
opus_samplerate 48000
opus_stereo no

View File

@ -54,8 +54,10 @@ import androidx.media.AudioFocusRequestCompat
import androidx.media.AudioManagerCompat
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import org.json.JSONArray
import org.json.JSONObject
import java.io.File
@ -187,6 +189,7 @@ class BaresipService: Service() {
Intent.FLAG_ACTIVITY_SINGLE_TOP or Intent.FLAG_ACTIVITY_CLEAR_TOP or
Intent.FLAG_ACTIVITY_NEW_TASK
newIntent.putExtra("action", "network available")
//newIntent.putExtra("address", netInfo.get("ip"))
newIntent.putExtra("address", deviceIpAddress)
@ -205,6 +208,8 @@ class BaresipService: Service() {
allNetworks.remove(network)
if (isServiceRunning)
updateNetwork()
Utils.renderVfdString("NO,NETWORK")
}
override fun onCapabilitiesChanged(network: Network, caps: NetworkCapabilities) {
@ -564,19 +569,38 @@ class BaresipService: Service() {
if(param != null) {
val json = JSONObject(param)
val device_name = json.getString("device_name")
val mic_volume = json.getString("mic_volume")
val display_type = json.getString("display_type")
Config.replaceVariable("device_name", device_name)
BaresipService.deviceName = device_name
val mic_volume_int = mic_volume.toInt()
if(mic_volume_int >= 0 && mic_volume_int <= 24) {
Utils.setMicVolumeByMix(mic_volume_int)
try {
val mic_volume = json.getString("mic_volume")
val mic_volume_int = mic_volume.toInt()
if (mic_volume_int >= 0 && mic_volume_int <= 24) {
if (mic_volume_int != BaresipService.micVolume) {
Utils.setMicVolumeByMix(mic_volume_int)
}
}
} catch(e : Exception) {
}
try {
val aux_volume = json.getString("aux_volume")
val aux_volume_int = aux_volume.toInt()
if(aux_volume_int >= 0 && aux_volume_int <= 127) {
if(aux_volume_int != BaresipService.auxVolume) {
Utils.setAuxVolumeByMix(aux_volume_int)
}
}
} catch(e : Exception) {
}
Config.replaceVariable("far_view_display_id", display_type)
BaresipService.farViewDisplayId = display_type.toInt()
try {
BaresipService.farViewDisplayId = display_type.toInt()
} catch(e:Exception) {
}
Config.save()
sendActivityAction("update info")
@ -759,7 +783,7 @@ class BaresipService: Service() {
}
val scope = CoroutineScope(Dispatchers.Default)
var captureCount : Long = 0
//var captureCount : Long = 0
var running = true
val job = scope.launch {
@ -811,7 +835,10 @@ class BaresipService: Service() {
val bufSize : Int = farBuf.capacity() / 2
var width : Int = 1280
var height : Int = 720
if(bufSize == 1280 * 720) {
if(bufSize == 720 * 480) {
width = 720
height = 480
} else if(bufSize == 1280 * 720) {
width = 1280
height = 720
} else if(bufSize == 1920 * 1080) {
@ -989,23 +1016,49 @@ class BaresipService: Service() {
//call.setVideoSource(!BaresipService.cameraFront)
val prevCamera2 = Utils.propertyGet("sys.ritosip.camera2.status")
Utils.propertySet("sys.ritosip.camera2.status", camera2)
if(camera1 == "plugin" && prevCamera1 != "plugin") {
sendActivityAction("camera connected");
} else if(camera1 != "plugin" && prevCamera1 == "plugin") {
sendActivityAction("camera removed");
}
var cameraCount = 0
if(camera1 == "plugin") {
cameraCount++
}
if(camera2 == "plugin") {
cameraCount++
}
BaresipService.connectedCameraCount = cameraCount
if(camera1 == "plugin" && camera2 != "plugin") {
BaresipService.sourceSelect = 1
} else if(camera1 != "plugin" && camera2 == "plugin") {
BaresipService.sourceSelect = 2
} else if(prevCamera2 != "plugin" && camera2 == "plugin") {
BaresipService.sourceSelect = 2
}
if(camera1 != "plugin" && camera2 != "plugin" && cameraState != 0) {
if(uas.size > 0) {
val ua = uas[0]
val call = ua.currentCall()
if(call != null) {
delay(200)
call.setVideoFake()
cameraState = 0
println("setVideoFake 실행")
}
}
} else if(camera1 == "plugin" && camera2 != "plugin" && (cameraState == 0 || cameraState == 2)) {
} else if(camera1 == "plugin" && (camera2 != "plugin" || BaresipService.sourceSelect == 1) && (cameraState == 0 || cameraState == 2)) {
if(uas.size > 0) {
val ua = uas[0]
val call = ua.currentCall()
if(call != null) {
Log.d(TAG, "RITO camera1 detect!")
Log.d(TAG, "RITO camera1 detect!, sourceSelect = ${BaresipService.sourceSelect}, cameraState = ${cameraState}")
//val process = Runtime.getRuntime().exec("ritosysc shell-order=killall android.hardware.camera.provider@2.4-service android.hardware.camera.provider@2.4-external-service cameraserver android.hardware.tv.input@1.0-service")
if(cameraState == 0) {
val process = Runtime.getRuntime()
@ -1016,24 +1069,31 @@ class BaresipService: Service() {
}
BaresipService.cameraFront = true;
call.setVideoSource(true)
if(cameraState == 2) {
showCustomToast(applicationContext, "입력소스 HDMI-1로 전환됩니다.", Toast.LENGTH_SHORT)
}
cameraState = 1
//delay(3000) // 1초마다 실행
}
}
} else if(camera2 == "plugin" && cameraState != 2) {
} else if(camera2 == "plugin" && (camera1 != "plugin" || BaresipService.sourceSelect != 1) && cameraState != 2) {
if(uas.size > 0) {
val ua = uas[0]
val call = ua.currentCall()
if(call != null) {
Log.d(TAG, "RITO camera2 detect!")
Log.d(TAG, "RITO camera2 detect!, sourceSelect = ${BaresipService.sourceSelect}, cameraState = ${cameraState}")
//val process = Runtime.getRuntime().exec("ritosysc shell-order=killall android.hardware.camera.provider@2.4-service android.hardware.camera.provider@2.4-external-service cameraserver android.hardware.tv.input@1.0-service")
// val process = Runtime.getRuntime().exec("ritosysc shell-order=killall cameraserver")
// process.waitFor()
// process.destroy()
// delay(1500) // 1초마다 실행
if(cameraState == 1 || camera1 == "plugin") {
showCustomToast(applicationContext, "입력소스 HDMI-2로 전환됩니다.", Toast.LENGTH_SHORT)
}
BaresipService.cameraFront = false;
call.setVideoSource(false)
cameraState = 2
//delay(3000) // 1초마다 실행
}
@ -1063,6 +1123,7 @@ class BaresipService: Service() {
if(call == null) {
cameraState = -1
BaresipService.sourceSelect = 1
}
} else {
cameraState = -1
@ -1152,7 +1213,9 @@ class BaresipService: Service() {
if(cameraState == -1) {
if(camera1 == "plugin") {
//Utils.runShellOrderSuper("killall -9 v4l2-ctl")
Utils.runShellOrderSuper("timeout 2s v4l2-ctl --device=/dev/video20 --stream-mmap=3 --stream-to=/mnt/obb/camera.rgb888 --stream-count=1 && chmod 666 /mnt/obb/camera.rgb888")
if(!useMonitoringSelfView) {
Utils.runShellOrderSuper("timeout 2s v4l2-ctl --device=/dev/video20 --stream-mmap=3 --stream-to=/mnt/obb/camera.rgb888 --stream-count=1 && chmod 666 /mnt/obb/camera.rgb888")
}
try {
val nearBuf = Utils.readFileToByteBuffer("/mnt/obb/camera.rgb888")
@ -1288,7 +1351,11 @@ class BaresipService: Service() {
CallHistoryNew.save()
}
val value = Utils.readNumberFromFile("/sdcard/Documents/sip_total_duration.txt")
var value = Utils.readNumberFromFile("/sdcard/Documents/sip_total_duration.txt")
if(value == null) {
Utils.runShellOrderSuper("appops set kr.co.rito.ritosip MANAGE_EXTERNAL_STORAGE allow")
value = Utils.readNumberFromFile("/sdcard/Documents/sip_total_duration.txt")
}
if(value != null) {
BaresipService.totalDurationSeconds = value
}
@ -1353,7 +1420,7 @@ class BaresipService: Service() {
applyTransportConfiguration()
Utils.setMicVolumeByMix(BaresipService.micVolume)
Utils.setAuxVolumeByMix(BaresipService.auxVolume)
}
"Start Content Observer" -> {
@ -1680,6 +1747,7 @@ class BaresipService: Service() {
}
return
}
// callp holds SIP message pointer
Api.ua_accept(uap, callp)
return
@ -2275,17 +2343,21 @@ class BaresipService: Service() {
}
}
fun showCustomToast(context: Context, message: String) {
val inflater = LayoutInflater.from(context)
val layout: View = inflater.inflate(R.layout.custom_toast, null)
fun showCustomToast(context: Context, message: String, duration: Int = Toast.LENGTH_LONG) {
GlobalScope.launch(Dispatchers.IO) {
withContext(Dispatchers.Main) {
val inflater = LayoutInflater.from(context)
val layout: View = inflater.inflate(R.layout.custom_toast, null)
val textView: TextView = layout.findViewById(R.id.toast_text)
textView.text = message
val textView: TextView = layout.findViewById(R.id.toast_text)
textView.text = message
val toast = Toast(context)
toast.duration = Toast.LENGTH_LONG
toast.view = layout
toast.show()
val toast = Toast(context)
toast.duration = duration
toast.view = layout
toast.show()
}
}
}
private fun getActionText(@StringRes stringRes: Int, @ColorRes colorRes: Int): Spannable {
@ -2686,6 +2758,11 @@ class BaresipService: Service() {
var audioDeviceUsbId = -1
var audioDeviceCodecId = -1
var micVolume = 20
var auxVolume = 80
var sourceSelect = 1
var useMonitoringSelfView = false
var captureCount : Long = 0 // 로컬 프리뷰와 함께 캡쳐를 위해 companion object로 변경
var connectedCameraCount = 0
fun updateAudioSourceDevice(ctx: Context) {
val am = ctx.getSystemService(Context.AUDIO_SERVICE) as AudioManager

View File

@ -219,6 +219,14 @@ object Config {
config = "${config}mic_volume ${BaresipService.micVolume}\n"
}
val auxVolume = previousVariable("aux_volume")
if (auxVolume != "") {
config = "${config}aux_volume $auxVolume\n"
BaresipService.auxVolume = auxVolume.toInt()
} else {
config = "${config}aux_volume ${BaresipService.auxVolume}\n"
}
for (size in defaultSizes)
videoSizes.add(size.toString())
/********************/

View File

@ -11,9 +11,10 @@ import android.content.*
import android.content.Intent.ACTION_CALL
import android.content.Intent.ACTION_DIAL
import android.content.Intent.ACTION_VIEW
import android.content.pm.PackageInfo
import android.content.pm.PackageManager
import android.content.res.Configuration
import android.graphics.Bitmap
import android.graphics.Matrix
import android.graphics.Rect
import android.hardware.display.DisplayManager
import android.media.AudioManager
@ -32,10 +33,12 @@ import androidx.activity.OnBackPressedCallback
import androidx.activity.enableEdgeToEdge
import androidx.activity.result.ActivityResultLauncher
import androidx.activity.result.contract.ActivityResultContracts
import androidx.annotation.OptIn
import androidx.annotation.RequiresApi
import androidx.appcompat.app.AppCompatActivity
import androidx.appcompat.app.AppCompatDelegate
import androidx.appcompat.widget.AppCompatButton
import androidx.camera.view.PreviewView
import androidx.core.app.ActivityCompat
import androidx.core.content.ContextCompat
import androidx.core.graphics.Insets
@ -50,17 +53,20 @@ import com.google.android.material.snackbar.Snackbar
import com.tutpro.baresip.plus.Utils.getAppVersion
import com.tutpro.baresip.plus.Utils.showSnackBar
import com.tutpro.baresip.plus.databinding.ActivityMainBinding
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import org.json.JSONObject
import java.io.File
import java.text.SimpleDateFormat
import java.time.LocalDateTime
import java.time.format.DateTimeFormatter
import java.util.*
import java.util.concurrent.Executors
import kotlin.system.exitProcess
import java.io.FileOutputStream
import androidx.camera.core.*
import androidx.camera.lifecycle.ProcessCameraProvider
import androidx.camera.camera2.interop.Camera2CameraInfo
import androidx.camera.camera2.interop.ExperimentalCamera2Interop
class SecondScreenPresentation(context: Context, display: Display) : Presentation(context, display) {
@ -75,6 +81,68 @@ class SecondScreenPresentation(context: Context, display: Display) : Presentatio
}
}
private class FrameSaver(
private val outputDir: File
) : ImageAnalysis.Analyzer {
private var lastSavedAt = 0L
private val sdf = SimpleDateFormat("yyyyMMdd_HHmmss_SSS", Locale.US)
override fun analyze(image: ImageProxy) {
try {
val now = System.currentTimeMillis()
// 1초 간격
if (now - lastSavedAt >= 1000) {
val bitmap = imageProxyToBitmap(image) ?: return
// 회전 보정
val rotated = rotateBitmap(bitmap, image.imageInfo.rotationDegrees.toFloat())
// 파일 저장 (JPG)
var nearFilePath = "/mnt/obb/near_${BaresipService.captureCount}.jpg"
//val file = File(outputDir, "${sdf.format(now)}.jpg")
val file = File(nearFilePath)
FileOutputStream(file).use { fos ->
rotated.compress(Bitmap.CompressFormat.JPEG, 90, fos)
val destFile = File(nearFilePath)
destFile.setReadable(true, false)
Utils.propertySet("sys.ritosip.near_screen_file", nearFilePath)
}
lastSavedAt = now
}
} catch (t: Throwable) {
t.printStackTrace()
} finally {
image.close() // 반드시 닫아야 다음 프레임이 들어옵니다.
}
}
// RGBA_8888 → Bitmap
private fun imageProxyToBitmap(image: ImageProxy): Bitmap? {
// if (image.format != ImageFormat.UNKNOWN &&
// image.format != ImageFormat.RGBA_8888
// ) return null
val plane = image.planes.firstOrNull() ?: return null
val buffer = plane.buffer
buffer.rewind()
val width = image.width
val height = image.height
val bitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888)
bitmap.copyPixelsFromBuffer(buffer)
return bitmap
}
private fun rotateBitmap(src: Bitmap, degrees: Float): Bitmap {
if (degrees == 0f) return src
val m = Matrix().apply { postRotate(degrees) }
return Bitmap.createBitmap(src, 0, 0, src.width, src.height, m, true).also {
if (it != src) src.recycle()
}
}
}
class MainActivity : AppCompatActivity() {
private lateinit var binding: ActivityMainBinding
@ -147,6 +215,10 @@ class MainActivity : AppCompatActivity() {
private lateinit var networkButton: AppCompatButton
private lateinit var backwardButton: AppCompatButton
private lateinit var previewView: PreviewView
private var imageAnalyzer: ImageAnalysis? = null
private var cameraExecutor = Executors.newSingleThreadExecutor()
private var callHandler: Handler = Handler(Looper.getMainLooper())
private var callRunnable: Runnable? = null
private var downloadsInputUri: Uri? = null
@ -197,11 +269,136 @@ class MainActivity : AppCompatActivity() {
}
lateinit var navUpList : HashMap<View, View>
lateinit var navUpListAlter : HashMap<View, View>
lateinit var navDownList : HashMap<View, View>
lateinit var navDownListAlter : HashMap<View, View>
lateinit var navUpServerList : HashMap<View, View>
lateinit var navDownServerList : HashMap<View, View>
private var cameraProvider: ProcessCameraProvider? = null
@OptIn(ExperimentalCamera2Interop::class)
private fun cameraSelectorById(targetId: String): CameraSelector {
return CameraSelector.Builder()
.addCameraFilter { cameraInfos: List<CameraInfo> ->
cameraInfos.filter { info ->
Camera2CameraInfo.from(info).cameraId == targetId
}
}
.build()
}
private fun stopSelfMoitoringCamera() {
if(BaresipService.useMonitoringSelfView) {
binding.selfViewLayout.visibility = View.INVISIBLE
// val cameraProviderFuture = ProcessCameraProvider.getInstance(this)
// val cameraProvider = cameraProviderFuture.get()
cameraProvider?.unbindAll()
}
}
@SuppressLint("RestrictedApi")
private fun resetSelfMoitoringCamera() {
if(BaresipService.useMonitoringSelfView) {
for(i in 0..1) {
cameraProvider?.unbindAll()
cameraProvider?.shutdown()
cameraExecutor.shutdown()
}
cameraExecutor = Executors.newSingleThreadExecutor()
}
}
// @SuppressLint("RestrictedApi")
// private fun reinitializeCamera(reason: String) {
// // 1) 모든 UseCase 해제
// cameraProvider?.unbindAll()
//
// // 2) provider 완전 종료
// val oldProvider = cameraProvider
// cameraProvider = null
//
// if (oldProvider != null) {
// val shutdownFuture = oldProvider.shutdown() // ListenableFuture<Void>
// shutdownFuture.addListener({
// // 3) 재시작
// startCameraWithId(targetCameraId)
// }, ContextCompat.getMainExecutor(this))
// } else {
// // 바로 시작
// startCameraWithId(targetCameraId)
// }
// }
private fun startSelfMoitoringCamera() {
if(!BaresipService.useMonitoringSelfView)
return;
binding.selfViewLayout.visibility = View.VISIBLE
val cameraProviderFuture = ProcessCameraProvider.getInstance(this)
cameraProviderFuture.addListener({
cameraProvider = cameraProviderFuture.get()
// Preview
val preview = Preview.Builder().build().apply {
setSurfaceProvider(previewView.surfaceProvider)
}
// ImageAnalysis: 프리뷰 프레임을 가져옴
imageAnalyzer = ImageAnalysis.Builder()
// RGBA_8888로 받으면 YUV→RGB 변환 없이 바로 Bitmap 생성 가능
.setOutputImageFormat(ImageAnalysis.OUTPUT_IMAGE_FORMAT_RGBA_8888)
// 최신 프레임만 유지 (버벅임 방지)
.setBackpressureStrategy(ImageAnalysis.STRATEGY_KEEP_ONLY_LATEST)
.build()
.also { ia ->
ia.setAnalyzer(cameraExecutor, FrameSaver(getOutputDir()))
}
val cameraSelector = cameraSelectorById("120")
try {
cameraProvider?.unbindAll()
val camera = cameraProvider?.bindToLifecycle(
this, cameraSelector, preview, imageAnalyzer
)
camera?.cameraInfo?.cameraState?.observe(this) { state ->
when (val type = state.type) {
CameraState.Type.OPEN, CameraState.Type.PENDING_OPEN -> { /* 정상/열리는 중 */ }
CameraState.Type.CLOSING, CameraState.Type.CLOSED -> {
// 닫힘 → 필요 시 재시도 타이머
println("RITO Camera Closing or closed")
}
else -> {}
}
state.error?.let { err ->
when (err.code) {
CameraState.ERROR_CAMERA_IN_USE,
CameraState.ERROR_MAX_CAMERAS_IN_USE,
CameraState.ERROR_CAMERA_DISABLED,
CameraState.ERROR_CAMERA_FATAL_ERROR -> {
// 복구 트리거
println("RITO Camera Error code : ${err.code}")
}
}
}
}
} catch (e: Exception) {
e.printStackTrace()
}
}, ContextCompat.getMainExecutor(this))
}
private fun getOutputDir(): File {
// 앱 전용 Pictures 폴더 (권한 불필요)
val dir = getExternalFilesDir(Environment.DIRECTORY_PICTURES)
return File(dir, "preview_frames").apply { if (!exists()) mkdirs() }
}
override fun dispatchKeyEvent(event: KeyEvent): Boolean {
if(!isKeyboardVisible) {
val currentFocusView = currentFocus
@ -214,10 +411,19 @@ class MainActivity : AppCompatActivity() {
view.requestFocus()
return false
} else {
if(currentFocusView == binding.btnApply) {
binding.radioDhcp.requestFocus()
return false
if(navUpListAlter.contains(currentFocusView)) {
val view = navUpListAlter.get(currentFocusView)!!
if(view.isVisible) {
view.requestFocus()
return false
}
}
// if(currentFocusView == binding.btnApply) {
// binding.radioDhcp.requestFocus()
// return false
// }
}
} else {
if(currentFocusView == binding.aorSpinner) {
@ -250,6 +456,14 @@ class MainActivity : AppCompatActivity() {
if(view.isVisible) {
view.requestFocus()
return false
} else {
if(navDownListAlter.contains(currentFocusView)) {
val view = navDownListAlter.get(currentFocusView)!!
if(view.isVisible) {
view.requestFocus()
return false
}
}
}
}
if(navDownServerList.contains(currentFocusView)) {
@ -281,6 +495,9 @@ class MainActivity : AppCompatActivity() {
if(navUpList.contains(currentFocusView) || navDownList.contains(currentFocusView)) {
found = true
}
// if(!found && navDownListAlter.contains(currentFocusView)) {
// found = true
// }
if(navUpServerList.contains(currentFocusView) || navDownServerList.contains(currentFocusView)) {
found = true
}
@ -369,6 +586,7 @@ class MainActivity : AppCompatActivity() {
private fun updateFieldsVisibility() {
val isStatic = binding.radioStatic.isChecked
val visibility = if (isStatic) View.VISIBLE else View.GONE
binding.layoutStaticIp.visibility = visibility
binding.editIp.visibility = visibility
binding.editNetmask.visibility = visibility
binding.editGateway.visibility = visibility
@ -478,6 +696,8 @@ class MainActivity : AppCompatActivity() {
dialpadButton = binding.dialpadButton
swipeRefresh = binding.swipeRefresh
previewView = binding.previewView
dialogImgBtn1 = binding.dialogButtonImg1
dialogImgBtn1.setOnClickListener {
@ -639,6 +859,10 @@ class MainActivity : AppCompatActivity() {
binding.seekbarMicVolume.progress = BaresipService.micVolume
val currentValue = binding.seekbarMicVolume.progress
binding.textMicVolume.setText(currentValue.toString())
binding.seekbarAuxVolume.progress = BaresipService.auxVolume
val currentValueAux = binding.seekbarAuxVolume.progress
binding.textAuxVolume.setText(currentValueAux.toString())
}
binding.dialogButtonVolumeInCall.setOnClickListener {
@ -649,6 +873,10 @@ class MainActivity : AppCompatActivity() {
binding.seekbarMicVolume.progress = BaresipService.micVolume
val currentValue = binding.seekbarMicVolume.progress
binding.textMicVolume.setText(currentValue.toString())
binding.seekbarAuxVolume.progress = BaresipService.auxVolume
val currentValueAux = binding.seekbarAuxVolume.progress
binding.textAuxVolume.setText(currentValueAux.toString())
}
binding.seekbarMicVolume.setOnSeekBarChangeListener(object : SeekBar.OnSeekBarChangeListener{
@ -656,7 +884,7 @@ class MainActivity : AppCompatActivity() {
// progress: 현재 값
// fromUser: 사용자가 직접 움직였는지 여부
val currentValue = progress
println("현재 값: $currentValue")
println("현재 MIC 값: $currentValue")
binding.textMicVolume.setText(currentValue.toString())
Utils.setMicVolumeByMix(currentValue)
}
@ -671,6 +899,26 @@ class MainActivity : AppCompatActivity() {
}
})
binding.seekbarAuxVolume.setOnSeekBarChangeListener(object : SeekBar.OnSeekBarChangeListener{
override fun onProgressChanged(seekBar: SeekBar?, progress: Int, fromUser: Boolean) {
// progress: 현재 값
// fromUser: 사용자가 직접 움직였는지 여부
val currentValue = progress
println("현재 AUX 값: $currentValue")
binding.textAuxVolume.setText(currentValue.toString())
Utils.setAuxVolumeByMix(currentValue)
}
override fun onStartTrackingTouch(seekBar: SeekBar?) {
// 터치 시작 시 호출
}
override fun onStopTrackingTouch(seekBar: SeekBar?) {
// 터치 끝날 때 호출
}
})
binding.btnDoneVolume.setOnClickListener {
binding.volumeSettingLayout.visibility = View.INVISIBLE
if(binding.settingLayout.visibility == View.VISIBLE) {
@ -731,21 +979,29 @@ class MainActivity : AppCompatActivity() {
imm.showSoftInput(it, InputMethodManager.SHOW_IMPLICIT)
}
navUpList = HashMap<View, View>()
navUpListAlter = HashMap<View, View>()
navDownList = HashMap<View, View>()
navDownListAlter = HashMap<View, View>()
navUpList.put(binding.editIp, binding.radioStatic)
navUpList.put(binding.editNetmask, binding.editIp)
navUpList.put(binding.editGateway, binding.editNetmask)
navUpList.put(binding.editDns, binding.editGateway)
navUpList.put(binding.btnApply, binding.editDns)
navUpList.put(binding.btnCancel, binding.editDns)
navUpListAlter.put(binding.btnCancel, binding.radioStatic)
navUpListAlter.put(binding.btnApply, binding.radioDhcp)
//navUpList.put(binding.btnApply, binding.radioDhcp)
navDownList.put(binding.radioDhcp, binding.editIp)
navDownList.put(binding.radioStatic, binding.editIp)
navDownListAlter.put(binding.radioStatic, binding.btnApply)
navDownListAlter.put(binding.radioDhcp, binding.btnApply)
navDownList.put(binding.editIp, binding.editNetmask)
navDownList.put(binding.editNetmask, binding.editGateway)
navDownList.put(binding.editGateway, binding.editDns)
navDownList.put(binding.editDns, binding.btnApply)
navDownList.put(binding.radioDhcp, binding.btnApply)
//navDownList.put(binding.radioDhcp, binding.btnApply)
// setDpadNavigation(binding.editIp, downView = binding.editNetmask)
// setDpadNavigation(binding.editNetmask, upView = binding.editIp, downView = binding.editGateway)
@ -767,6 +1023,31 @@ class MainActivity : AppCompatActivity() {
showCustomToast(applicationContext, "DHCP 적용되었습니다.")
binding.networkSettingLayout.visibility = View.INVISIBLE
} else {
if(binding.editIp.text.length == 0 ||
binding.editNetmask.text.length == 0 ||
binding.editGateway.text.length == 0 ||
binding.editDns.text.length == 0) {
showCustomToast(applicationContext, "비어있는 항목이 있습니다.")
return@setOnClickListener
}
if(Utils.IpValidator.detectIpType(binding.editIp.text.toString()) == Utils.IpType.NONE) {
showCustomToast(applicationContext, "IP가 유효하지 않습니다.")
return@setOnClickListener
}
if(Utils.IpValidator.detectIpType(binding.editNetmask.text.toString()) == Utils.IpType.NONE) {
showCustomToast(applicationContext, "NETMASK가 유효하지 않습니다.")
return@setOnClickListener
}
if(Utils.IpValidator.detectIpType(binding.editGateway.text.toString()) == Utils.IpType.NONE) {
showCustomToast(applicationContext, "GATEWAY가 유효하지 않습니다.")
return@setOnClickListener
}
if(Utils.IpValidator.detectIpType(binding.editDns.text.toString()) == Utils.IpType.NONE) {
showCustomToast(applicationContext, "DNS가 유효하지 않습니다.")
return@setOnClickListener
}
val params: HashMap<String, String> = HashMap<String, String>()
params.put("IPV4TYPE", "STATIC")
params.put("DEVTYPE", "ETHERNET")
@ -834,6 +1115,22 @@ class MainActivity : AppCompatActivity() {
binding.dialogButtonServer.requestFocus()
}
binding.btnPingTestServer.setOnClickListener { _ ->
Thread {
val rtts = Utils.ping(binding.editSipServer.text.toString(), 1)
if (rtts.isNotEmpty()) {
val avg = rtts.average()
runOnUiThread {
showCustomToast(applicationContext, "PING 성공하였습니다. RTT: ${"%.2f".format(avg)} ms")
}
} else {
runOnUiThread {
showCustomToast(applicationContext, "PING 실패하였습니다.")
}
}
}.start()
}
navUpServerList = HashMap<View, View>()
navDownServerList = HashMap<View, View>()
@ -1528,6 +1825,9 @@ class MainActivity : AppCompatActivity() {
}
}
//Utils.runShellOrder("sendserial /dev/ttyUSB10 115200 term '@123456'")
Utils.renderVfdString(",")
} // OnCreate
fun sendSettingByBroadcast(req : String, param : String) {
@ -2547,12 +2847,15 @@ class MainActivity : AppCompatActivity() {
Utils.alertView(this, getString(R.string.notice),
getString(R.string.no_network))
*/
Utils.renderVfdString("NO,NETWORK")
return
}
/* Added by ritoseo */
"network available" -> {
val tv = findViewById<TextView>(R.id.textViewHeaderIp)
tv.setText(intent.getStringExtra("address")!!)
Utils.renderVfdString(tv.text.toString())
}
"video call" -> {
callUri.setText(intent.getStringExtra("peer")!!)
@ -2571,6 +2874,17 @@ class MainActivity : AppCompatActivity() {
"update info" -> {
updateInfo()
}
"camera connected" -> {
if(!isCallExist()) {
startSelfMoitoringCamera()
}
}
"camera removed" -> {
if(!isCallExist()) {
stopSelfMoitoringCamera()
resetSelfMoitoringCamera()
}
}
/********************/
"call", "dial" -> {
if (Call.inCall()) {
@ -2603,9 +2917,19 @@ class MainActivity : AppCompatActivity() {
resumeCall = call
if(ev[0] == "call answer") {
stopSelfMoitoringCamera() // 모니터링 셀프 뷰 종료
Handler(Looper.getMainLooper()).postDelayed({
videoButton.performClick()
}, 1000)
Handler(Looper.getMainLooper()).postDelayed({
if(BaresipService.connectedCameraCount == 0) {
println("Answer Call setVideoFake 실행")
call.setVideoFake()
}
//call.setVideoDirection(Api.SDP_RECVONLY)
}, 1500)
}
}
"call missed" -> {
@ -2654,6 +2978,8 @@ class MainActivity : AppCompatActivity() {
val BTN_MISC = 188
//val BTN_MENU = 58
val SCANCODE_OPTION = 357 //MENU키 -> OPTION키로 매핑
val SCANCODE_RED = 398
val SCANCODE_GREEN = 399
//println("onKeyDown : ${keyCode}")
//println("onKeyDown2 : ${event?.scanCode}")
val stream = if (am.mode == AudioManager.MODE_RINGTONE)
@ -2686,11 +3012,29 @@ class MainActivity : AppCompatActivity() {
editable.delete(length - 1, length)
}
}
KeyEvent.KEYCODE_ENTER, KeyEvent.KEYCODE_DPAD_CENTER -> {
if(BaresipService.sourceSelect == 1) {
BaresipService.sourceSelect = 2
} else {
BaresipService.sourceSelect = 1
}
return true
}
BTN_MISC -> {
return true
}
}
when (event?.scanCode) {
SCANCODE_RED -> {
if(!isCallExist()) {
startSelfMoitoringCamera()
}
}
SCANCODE_GREEN -> {
if(!isCallExist()) {
stopSelfMoitoringCamera()
}
}
SCANCODE_OPTION -> {
// val dialog = findViewById<FrameLayout>(R.id.dialogLayout)
// if(dialog.isVisible) {
@ -2786,6 +3130,7 @@ class MainActivity : AppCompatActivity() {
}
updateDisplay()
BaresipService.supportedCameras = Utils.supportedCameras(applicationContext).isNotEmpty()
handleNextEvent()
return
}
@ -2870,6 +3215,13 @@ class MainActivity : AppCompatActivity() {
showCall(ua)
}
"call established" -> {
val ua = BaresipService.uas[0]
val call = ua.currentCall()
if(call != null) {
val callNo = call.peerUri.split("@")[0].filter { it.isDigit() }
Utils.renderVfdString("CALL,${callNo}")
}
if (aor == aorSpinner.tag) {
dtmf.text = null
dtmf.hint = getString(R.string.dtmf)
@ -3031,6 +3383,7 @@ class MainActivity : AppCompatActivity() {
switchVideoLayout(false)
//callVideoButton.requestFocus()
Utils.renderVfdString(binding.textViewHeaderIp.text.toString())
BaresipService.isMicMuted = false
BaresipService.isVideoMuted = false
}
@ -3653,6 +4006,7 @@ class MainActivity : AppCompatActivity() {
}
private fun makeCall(kind: String, lookForContact: Boolean = true) {
stopSelfMoitoringCamera() // 모니터링 Self 카메라 멈춤
callUri.setAdapter(null)
val ua = BaresipService.uas[aorSpinner.selectedItemPosition]
val aor = ua.account.aor

View File

@ -607,6 +607,31 @@ object Utils {
}
}
var VfdLock : Object = Object()
fun renderVfdString(info : String, align : String = "center") {
val file = File("/dev/ttyUSB10")
if (!file.exists()) {
return
}
Thread {
synchronized(VfdLock) {
var renderStr = "'@"
if (align == "center") {
var space = 0
space = (16 - info.length) / 2
for (i in 1..space) {
renderStr += ","
}
renderStr += info
renderStr += "'"
}
Utils.runShellOrder("sendserial /dev/ttyUSB10 115200 term ${renderStr}")
}
}.start()
}
fun putFileContents(filePath: String, contents: ByteArray): Boolean {
try {
File(filePath).writeBytes(contents)
@ -1372,6 +1397,108 @@ object Utils {
}
}
fun setAuxVolumeByMix(volume : Int) {
CoroutineScope(Dispatchers.Default).launch {
Utils.runShellOrderSuper("tinymix -D 2 \"ADC Capture Volume\" ${volume}")
BaresipService.auxVolume = volume
Config.replaceVariable("aux_volume", volume.toString())
Config.save()
propertySet("sys.ritosip.aux.volume", volume.toString())
}
}
enum class IpType { IPv4, IPv6, NONE }
object IpValidator {
private val IPV4_REGEX =
Regex("""^(?:0|[1-9]\d{0,2})(?:\.(?:0|[1-9]\d{0,2})){3}$""") // 선행 0 금지(단, "0"은 허용)
// ----- Public API -----
fun detectIpType(input: String?): IpType {
val s = input?.trim().orEmpty()
if (s.isEmpty()) return IpType.NONE
return when {
isValidIPv4(s) -> IpType.IPv4
isValidIPv6(s) -> IpType.IPv6
else -> IpType.NONE
}
}
fun isValidIPv4(s: String): Boolean {
if (!IPV4_REGEX.matches(s)) return false
// 각 옥텟 0..255
return s.split('.').all { part ->
val v = part.toIntOrNull() ?: return false
v in 0..255
}
}
fun isValidIPv6(s: String): Boolean {
// 스코프 ID 분리 (예: fe80::1%eth0)
val zoneIndex = s.indexOf('%')
val (addr, zone) = if (zoneIndex >= 0) {
s.substring(0, zoneIndex) to s.substring(zoneIndex + 1)
} else s to null
if (zone != null && zone.isEmpty()) return false
if (zone != null && !zone.all { it.isLetterOrDigit() || it == '.' || it == '_' || it == '-' }) return false
// 최소 콜론 2개(IPv6 형태인지 빠른 거르기)
if (addr.count { it == ':' } < 2) return false
// 임베디드 IPv4 처리 (마지막 부분에 IPv4가 올 수 있음)
val lastColon = addr.lastIndexOf(':')
val hasEmbeddedV4 = addr.contains('.')
val (base, v4tail) = if (hasEmbeddedV4 && lastColon >= 0) {
addr.substring(0, lastColon) to addr.substring(lastColon + 1)
} else addr to null
if (v4tail != null && !isValidIPv4(v4tail)) return false
// "::"는 최대 한 번만 허용
val ddc = base.indexOf("::")
if (ddc != -1 && base.indexOf("::", startIndex = ddc + 1) != -1) return false
val hextetRegex = Regex("^[0-9A-Fa-f]{1,4}$")
fun splitHextets(part: String): List<String> {
if (part.isEmpty()) return emptyList()
val pieces = part.split(':')
// 빈 조각은 허용하지 않음(단, "::" 압축에서만 허용)
if (pieces.any { it.isEmpty() }) return emptyList()
return pieces
}
val (leftH, rightH) = if (ddc >= 0) {
val left = base.substring(0, ddc)
val right = base.substring(ddc + 2)
splitHextets(left) to splitHextets(right)
} else {
splitHextets(base) to emptyList()
}
if (leftH.isEmpty() && ddc == -1 && base.isEmpty()) return false // 완전히 빈 주소는 불가
// 각 hextet 유효성
if (leftH.any { !hextetRegex.matches(it) }) return false
if (rightH.any { !hextetRegex.matches(it) }) return false
val explicitHextets = leftH.size + rightH.size
val totalUnits = explicitHextets + if (hasEmbeddedV4) 2 else 0
if (ddc >= 0) {
// 압축이 있는 경우: 총 단위(hextet 8개 기준)가 8보다 작아야 함(압축으로 채움)
if (totalUnits >= 8) return false
} else {
// 압축이 없는 경우: 총 단위가 정확히 8이어야 함
if (totalUnits != 8) return false
}
return true
}
}
fun runShellOrderSuper(order : String) {
val process = Runtime.getRuntime().exec("ritosysc shell-order=" + order)
process.waitFor()
@ -1409,6 +1536,13 @@ object Utils {
return found
}
fun setSourceSelect(idx : Int) {
if(idx >= -1 && idx <= 2) {
BaresipService.sourceSelect = idx
propertySet("sys.ritosip.source.select", idx.toString())
}
}
fun checkCameraConnection(idx : Int): String {
if(idx == 0) {
val file = File("/d/hdmirx/status") // 파일 경로 설정
@ -1547,6 +1681,30 @@ object Utils {
return netInfo
}
fun ping(host: String, count: Int = 4): List<Double> {
val command = arrayOf("/system/bin/ping", "-W", "2", "-c", count.toString(), host)
val process = ProcessBuilder(*command).redirectErrorStream(true).start()
val reader = BufferedReader(InputStreamReader(process.inputStream))
val regex = Regex("time=([0-9.]+) ms")
val rtts = mutableListOf<Double>()
reader.useLines { lines ->
lines.forEach { line ->
// 로그에 찍기 (Logcat)
android.util.Log.d("PingUtil", line)
val match = regex.find(line)
if (match != null) {
rtts.add(match.groupValues[1].toDouble())
}
}
}
process.waitFor()
return rtts
}
fun calculateNetmask(prefixLength: Int): String {
val mask = (0xFFFFFFFF shl (32 - prefixLength))
return listOf(

View File

@ -0,0 +1,15 @@
<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<item android:state_focused="true">
<shape android:shape="rectangle">
<solid android:color="@android:color/transparent"/>
<stroke android:width="3dp" android:color="#FF9800"/>
<corners android:radius="8dp"/>
</shape>
</item>
<item>
<shape android:shape="rectangle">
<solid android:color="@android:color/transparent"/>
</shape>
</item>
</selector>

View File

@ -0,0 +1,24 @@
<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<item android:state_focused="true">
<shape android:shape="rectangle">
<stroke android:width="4dp" android:color="#FF9800" />
<solid android:color="#0098FF" /> <!-- 포커스 배경 (주황색 강조) -->
<corners android:radius="8dp" />
</shape>
</item>
<item android:state_pressed="true">
<shape android:shape="rectangle">
<stroke android:width="4dp" android:color="#FF9800" />
<solid android:color="#26A7FF" /> <!-- 포커스 배경 (주황색 강조) -->
<corners android:radius="8dp" />
</shape>
</item>
<item>
<shape android:shape="rectangle">
<stroke android:width="4dp" android:color="#88FFFFFF" />
<solid android:color="#0098FF" /> <!-- 포커스 배경 (회색) -->
<corners android:radius="8dp" />
</shape>
</item>
</selector>

View File

@ -933,6 +933,9 @@
android:focusable="true"
android:focusableInTouchMode="true"
android:paddingRight="15dp"
android:nextFocusLeft="@id/radioDhcp"
android:nextFocusRight="@id/radioStatic"
android:background="@drawable/bg_button_selector_radio"
android:text="DHCP"
android:textSize="24sp" />
@ -942,42 +945,103 @@
android:layout_height="wrap_content"
android:focusable="true"
android:focusableInTouchMode="true"
android:nextFocusLeft="@id/radioDhcp"
android:nextFocusRight="@id/radioStatic"
android:background="@drawable/bg_button_selector_radio"
android:paddingRight="15dp"
android:text="STATIC"
android:textSize="24sp" />
</RadioGroup>
<EditText
android:id="@+id/editIp"
<LinearLayout
android:id="@+id/layoutStaticIp"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="IP Address"
android:inputType="phone"
android:textSize="24sp" />
android:layout_marginTop="4dp"
android:visibility="gone"
android:orientation="vertical">
<EditText
android:id="@+id/editNetmask"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="Netmask"
android:inputType="phone"
android:textSize="24sp" />
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:padding="16dp">
<TextView
android:layout_width="200px"
android:layout_height="wrap_content"
android:text="IP"
android:textSize="24sp"
/>
<EditText
android:id="@+id/editIp"
android:layout_width="1000px"
android:layout_height="wrap_content"
android:hint="IP Address"
android:inputType="phone"
android:textSize="24sp" />
</LinearLayout>
<EditText
android:id="@+id/editGateway"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="Gateway"
android:inputType="phone"
android:textSize="24sp" />
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:padding="16dp">
<TextView
android:layout_width="200px"
android:layout_height="wrap_content"
android:text="NETMASK"
android:textSize="24sp"
/>
<EditText
android:id="@+id/editNetmask"
android:layout_width="1000px"
android:layout_height="wrap_content"
android:hint="Netmask"
android:inputType="phone"
android:textSize="24sp" />
</LinearLayout>
<EditText
android:id="@+id/editDns"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="DNS"
android:inputType="phone"
android:textSize="24sp" />
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:padding="16dp">
<TextView
android:layout_width="200px"
android:layout_height="wrap_content"
android:text="GATEWAY"
android:textSize="24sp"
/>
<EditText
android:id="@+id/editGateway"
android:layout_width="1000px"
android:layout_height="wrap_content"
android:hint="Gateway"
android:inputType="phone"
android:textSize="24sp" />
</LinearLayout>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:padding="16dp">
<TextView
android:layout_width="200px"
android:layout_height="wrap_content"
android:text="DNS"
android:textSize="24sp"
/>
<EditText
android:id="@+id/editDns"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="DNS"
android:inputType="phone"
android:textSize="24sp" />
</LinearLayout>
</LinearLayout>
<LinearLayout
android:layout_width="match_parent"
@ -985,27 +1049,35 @@
android:layout_marginTop="16dp"
android:orientation="horizontal">
<Button
<androidx.appcompat.widget.AppCompatButton
android:id="@+id/btnApply"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_margin="16dp"
android:paddingHorizontal="20dp"
android:paddingVertical="10dp"
android:paddingHorizontal="30dp"
android:paddingVertical="15dp"
android:textColor="@color/colorWhite"
android:background="@drawable/bg_button_selector_setting"
android:nextFocusLeft="@id/btnApply"
android:nextFocusRight="@id/btnCancel"
android:nextFocusDown="@id/btnApply"
android:textStyle="bold"
android:text="적용하기"
android:textSize="24sp" />
<Button
<androidx.appcompat.widget.AppCompatButton
android:id="@+id/btnCancel"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_margin="16dp"
android:paddingHorizontal="20dp"
android:paddingVertical="10dp"
android:paddingHorizontal="30dp"
android:paddingVertical="15dp"
android:textColor="@color/colorWhite"
android:background="@drawable/bg_button_selector_setting"
android:nextFocusLeft="@id/btnApply"
android:nextFocusRight="@id/btnCancel"
android:nextFocusDown="@id/btnCancel"
android:textStyle="bold"
android:text="취소하기"
android:textSize="24sp" />
</LinearLayout>
@ -1144,6 +1216,8 @@
android:focusableInTouchMode="true"
android:paddingRight="15dp"
android:nextFocusUp="@id/editSipServer"
android:nextFocusLeft="@id/radioTransportUdp"
android:background="@drawable/bg_button_selector_radio"
android:text="UDP"
android:textSize="24sp" />
@ -1155,6 +1229,7 @@
android:focusableInTouchMode="true"
android:paddingRight="15dp"
android:nextFocusUp="@id/editSipServer"
android:background="@drawable/bg_button_selector_radio"
android:text="TCP"
android:textSize="24sp" />
@ -1166,6 +1241,8 @@
android:focusableInTouchMode="true"
android:paddingRight="15dp"
android:nextFocusUp="@id/editSipServer"
android:nextFocusRight="@id/radioTransportTls"
android:background="@drawable/bg_button_selector_radio"
android:text="TLS"
android:textSize="24sp" />
</RadioGroup>
@ -1205,6 +1282,8 @@
android:focusableInTouchMode="true"
android:paddingRight="15dp"
android:nextFocusDown="@id/btnApplyServer"
android:nextFocusLeft="@id/radioEncryptNone"
android:background="@drawable/bg_button_selector_radio"
android:text="사용안함"
android:textSize="24sp" />
@ -1216,6 +1295,8 @@
android:focusableInTouchMode="true"
android:paddingRight="15dp"
android:nextFocusDown="@id/btnApplyServer"
android:nextFocusRight="@id/radioEncryptSrtp"
android:background="@drawable/bg_button_selector_radio"
android:text="SRTP"
android:textSize="24sp" />
@ -1230,31 +1311,54 @@
android:layout_marginTop="16dp"
android:orientation="horizontal">
<Button
<androidx.appcompat.widget.AppCompatButton
android:id="@+id/btnApplyServer"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_margin="16dp"
android:paddingHorizontal="20dp"
android:paddingVertical="10dp"
android:paddingHorizontal="30dp"
android:paddingVertical="15dp"
android:nextFocusUp="@id/radioEncryptNone"
android:nextFocusLeft="@id/btnApplyServer"
android:nextFocusRight="@id/btnCancelServer"
android:textColor="@color/colorWhite"
android:background="@drawable/bg_button_selector_setting"
android:textStyle="bold"
android:text="적용하기"
android:textSize="24sp" />
<Button
<androidx.appcompat.widget.AppCompatButton
android:id="@+id/btnCancelServer"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_margin="16dp"
android:paddingHorizontal="20dp"
android:paddingVertical="10dp"
android:paddingHorizontal="30dp"
android:paddingVertical="15dp"
android:nextFocusUp="@id/radioEncryptNone"
android:nextFocusLeft="@id/btnApplyServer"
android:nextFocusRight="@id/btnCancelServer"
android:nextFocusRight="@id/btnPingTestServer"
android:textColor="@color/colorWhite"
android:background="@drawable/bg_button_selector_setting"
android:textStyle="bold"
android:text="취소하기"
android:textSize="24sp" />
<androidx.appcompat.widget.AppCompatButton
android:id="@+id/btnPingTestServer"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_margin="16dp"
android:paddingHorizontal="30dp"
android:paddingVertical="15dp"
android:nextFocusUp="@id/radioEncryptNone"
android:nextFocusLeft="@id/btnCancelServer"
android:nextFocusRight="@id/btnPingTestServer"
android:textColor="@color/colorWhite"
android:background="@drawable/bg_button_selector_setting"
android:textStyle="bold"
android:text="PING확인"
android:textSize="24sp" />
</LinearLayout>
</LinearLayout>
@ -1302,6 +1406,7 @@
android:max="24"
android:layout_height="wrap_content"
android:nextFocusUp="@id/seekbarMicVolume"
android:nextFocusDown="@id/seekbarAuxVolume"
android:nextFocusLeft="@id/seekbarMicVolume"
android:nextFocusRight="@id/seekbarMicVolume"
android:textSize="24sp" />
@ -1317,23 +1422,62 @@
</LinearLayout>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:gravity="center_vertical"
android:padding="16dp">
<TextView
android:layout_width="200px"
android:layout_height="wrap_content"
android:text="AUX 볼륨"
android:textSize="24sp"
/>
<SeekBar
android:id="@+id/seekbarAuxVolume"
android:layout_width="600px"
android:max="127"
android:layout_height="wrap_content"
android:nextFocusUp="@id/seekbarMicVolume"
android:nextFocusDown="@id/btnDoneVolume"
android:nextFocusLeft="@id/seekbarAuxVolume"
android:nextFocusRight="@id/seekbarAuxVolume"
android:textSize="24sp" />
<TextView
android:id="@+id/textAuxVolume"
android:layout_width="200px"
android:layout_marginLeft="30px"
android:layout_height="wrap_content"
android:text=""
android:textSize="36sp"
/>
</LinearLayout>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:orientation="horizontal">
<Button
<androidx.appcompat.widget.AppCompatButton
android:id="@+id/btnDoneVolume"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_margin="16dp"
android:paddingHorizontal="20dp"
android:paddingVertical="10dp"
android:nextFocusUp="@id/seekbarMicVolume"
android:paddingHorizontal="30dp"
android:paddingVertical="15dp"
android:nextFocusUp="@id/seekbarAuxVolume"
android:nextFocusLeft="@id/btnDoneVolume"
android:nextFocusRight="@id/btnDoneVolume"
android:nextFocusDown="@id/btnDoneVolume"
android:textColor="@color/colorWhite"
android:background="@drawable/bg_button_selector_setting"
android:textStyle="bold"
android:text="돌아가기"
android:textSize="24sp" />
</LinearLayout>
@ -1549,4 +1693,24 @@
</FrameLayout>
<FrameLayout
android:id="@+id/selfViewLayout"
android:layout_marginLeft="1400px"
android:layout_marginTop="740px"
android:layout_width="480px"
android:layout_height="270px"
android:background="@drawable/custom_border"
android:visibility="invisible">
<androidx.camera.view.PreviewView
android:id="@+id/previewView"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:implementationMode="performance"
app:scaleType="fillCenter"/>
</FrameLayout>
</RelativeLayout>