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' applicationId = 'kr.co.rito.ritosip'
minSdkVersion 29 minSdkVersion 29
targetSdkVersion 35 targetSdkVersion 35
versionCode = 104 versionCode = 106
versionName = '1.0.4' versionName = '1.0.6'
externalNativeBuild { externalNativeBuild {
cmake { cmake {
cFlags '-DHAVE_INTTYPES_H -lstdc++' cFlags '-DHAVE_INTTYPES_H -lstdc++'
@ -86,5 +86,11 @@ dependencies {
implementation "androidx.activity:activity-ktx:1.9.3" implementation "androidx.activity:activity-ktx:1.9.3"
implementation "androidx.fragment:fragment-ktx:1.8.5" implementation "androidx.fragment:fragment-ktx:1.8.5"
implementation 'androidx.media:media:1.7.0' 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:name="android.permission.WRITE_EXTERNAL_STORAGE"
android:maxSdkVersion="32" android:maxSdkVersion="32"
tools:ignore="ScopedStorage" /> 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.READ_CONTACTS" />
<uses-permission android:name="android.permission.WRITE_CONTACTS" /> <uses-permission android:name="android.permission.WRITE_CONTACTS" />
<uses-permission android:name="android.permission.CALL_PHONE" /> <uses-permission android:name="android.permission.CALL_PHONE" />

View File

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

View File

@ -54,8 +54,10 @@ import androidx.media.AudioFocusRequestCompat
import androidx.media.AudioManagerCompat import androidx.media.AudioManagerCompat
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.delay import kotlinx.coroutines.delay
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import org.json.JSONArray import org.json.JSONArray
import org.json.JSONObject import org.json.JSONObject
import java.io.File 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_SINGLE_TOP or Intent.FLAG_ACTIVITY_CLEAR_TOP or
Intent.FLAG_ACTIVITY_NEW_TASK Intent.FLAG_ACTIVITY_NEW_TASK
newIntent.putExtra("action", "network available") newIntent.putExtra("action", "network available")
//newIntent.putExtra("address", netInfo.get("ip")) //newIntent.putExtra("address", netInfo.get("ip"))
newIntent.putExtra("address", deviceIpAddress) newIntent.putExtra("address", deviceIpAddress)
@ -205,6 +208,8 @@ class BaresipService: Service() {
allNetworks.remove(network) allNetworks.remove(network)
if (isServiceRunning) if (isServiceRunning)
updateNetwork() updateNetwork()
Utils.renderVfdString("NO,NETWORK")
} }
override fun onCapabilitiesChanged(network: Network, caps: NetworkCapabilities) { override fun onCapabilitiesChanged(network: Network, caps: NetworkCapabilities) {
@ -564,19 +569,38 @@ class BaresipService: Service() {
if(param != null) { if(param != null) {
val json = JSONObject(param) val json = JSONObject(param)
val device_name = json.getString("device_name") val device_name = json.getString("device_name")
val mic_volume = json.getString("mic_volume")
val display_type = json.getString("display_type") val display_type = json.getString("display_type")
Config.replaceVariable("device_name", device_name) Config.replaceVariable("device_name", device_name)
BaresipService.deviceName = device_name BaresipService.deviceName = device_name
val mic_volume_int = mic_volume.toInt() try {
if(mic_volume_int >= 0 && mic_volume_int <= 24) { val mic_volume = json.getString("mic_volume")
Utils.setMicVolumeByMix(mic_volume_int) 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) Config.replaceVariable("far_view_display_id", display_type)
BaresipService.farViewDisplayId = display_type.toInt() try {
BaresipService.farViewDisplayId = display_type.toInt()
} catch(e:Exception) {
}
Config.save() Config.save()
sendActivityAction("update info") sendActivityAction("update info")
@ -759,7 +783,7 @@ class BaresipService: Service() {
} }
val scope = CoroutineScope(Dispatchers.Default) val scope = CoroutineScope(Dispatchers.Default)
var captureCount : Long = 0 //var captureCount : Long = 0
var running = true var running = true
val job = scope.launch { val job = scope.launch {
@ -811,7 +835,10 @@ class BaresipService: Service() {
val bufSize : Int = farBuf.capacity() / 2 val bufSize : Int = farBuf.capacity() / 2
var width : Int = 1280 var width : Int = 1280
var height : Int = 720 var height : Int = 720
if(bufSize == 1280 * 720) { if(bufSize == 720 * 480) {
width = 720
height = 480
} else if(bufSize == 1280 * 720) {
width = 1280 width = 1280
height = 720 height = 720
} else if(bufSize == 1920 * 1080) { } else if(bufSize == 1920 * 1080) {
@ -989,23 +1016,49 @@ class BaresipService: Service() {
//call.setVideoSource(!BaresipService.cameraFront) //call.setVideoSource(!BaresipService.cameraFront)
val prevCamera2 = Utils.propertyGet("sys.ritosip.camera2.status") val prevCamera2 = Utils.propertyGet("sys.ritosip.camera2.status")
Utils.propertySet("sys.ritosip.camera2.status", camera2) 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(camera1 != "plugin" && camera2 != "plugin" && cameraState != 0) {
if(uas.size > 0) { if(uas.size > 0) {
val ua = uas[0] val ua = uas[0]
val call = ua.currentCall() val call = ua.currentCall()
if(call != null) { if(call != null) {
delay(200)
call.setVideoFake() call.setVideoFake()
cameraState = 0 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) { if(uas.size > 0) {
val ua = uas[0] val ua = uas[0]
val call = ua.currentCall() val call = ua.currentCall()
if(call != null) { 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") //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) { if(cameraState == 0) {
val process = Runtime.getRuntime() val process = Runtime.getRuntime()
@ -1016,24 +1069,31 @@ class BaresipService: Service() {
} }
BaresipService.cameraFront = true; BaresipService.cameraFront = true;
call.setVideoSource(true) call.setVideoSource(true)
if(cameraState == 2) {
showCustomToast(applicationContext, "입력소스 HDMI-1로 전환됩니다.", Toast.LENGTH_SHORT)
}
cameraState = 1 cameraState = 1
//delay(3000) // 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) { if(uas.size > 0) {
val ua = uas[0] val ua = uas[0]
val call = ua.currentCall() val call = ua.currentCall()
if(call != null) { 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 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") // val process = Runtime.getRuntime().exec("ritosysc shell-order=killall cameraserver")
// process.waitFor() // process.waitFor()
// process.destroy() // process.destroy()
// delay(1500) // 1초마다 실행 // delay(1500) // 1초마다 실행
if(cameraState == 1 || camera1 == "plugin") {
showCustomToast(applicationContext, "입력소스 HDMI-2로 전환됩니다.", Toast.LENGTH_SHORT)
}
BaresipService.cameraFront = false; BaresipService.cameraFront = false;
call.setVideoSource(false) call.setVideoSource(false)
cameraState = 2 cameraState = 2
//delay(3000) // 1초마다 실행 //delay(3000) // 1초마다 실행
} }
@ -1063,6 +1123,7 @@ class BaresipService: Service() {
if(call == null) { if(call == null) {
cameraState = -1 cameraState = -1
BaresipService.sourceSelect = 1
} }
} else { } else {
cameraState = -1 cameraState = -1
@ -1152,7 +1213,9 @@ class BaresipService: Service() {
if(cameraState == -1) { if(cameraState == -1) {
if(camera1 == "plugin") { if(camera1 == "plugin") {
//Utils.runShellOrderSuper("killall -9 v4l2-ctl") //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 { try {
val nearBuf = Utils.readFileToByteBuffer("/mnt/obb/camera.rgb888") val nearBuf = Utils.readFileToByteBuffer("/mnt/obb/camera.rgb888")
@ -1288,7 +1351,11 @@ class BaresipService: Service() {
CallHistoryNew.save() 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) { if(value != null) {
BaresipService.totalDurationSeconds = value BaresipService.totalDurationSeconds = value
} }
@ -1353,7 +1420,7 @@ class BaresipService: Service() {
applyTransportConfiguration() applyTransportConfiguration()
Utils.setMicVolumeByMix(BaresipService.micVolume) Utils.setMicVolumeByMix(BaresipService.micVolume)
Utils.setAuxVolumeByMix(BaresipService.auxVolume)
} }
"Start Content Observer" -> { "Start Content Observer" -> {
@ -1680,6 +1747,7 @@ class BaresipService: Service() {
} }
return return
} }
// callp holds SIP message pointer // callp holds SIP message pointer
Api.ua_accept(uap, callp) Api.ua_accept(uap, callp)
return return
@ -2275,17 +2343,21 @@ class BaresipService: Service() {
} }
} }
fun showCustomToast(context: Context, message: String) { fun showCustomToast(context: Context, message: String, duration: Int = Toast.LENGTH_LONG) {
val inflater = LayoutInflater.from(context) GlobalScope.launch(Dispatchers.IO) {
val layout: View = inflater.inflate(R.layout.custom_toast, null) 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) val textView: TextView = layout.findViewById(R.id.toast_text)
textView.text = message textView.text = message
val toast = Toast(context) val toast = Toast(context)
toast.duration = Toast.LENGTH_LONG toast.duration = duration
toast.view = layout toast.view = layout
toast.show() toast.show()
}
}
} }
private fun getActionText(@StringRes stringRes: Int, @ColorRes colorRes: Int): Spannable { private fun getActionText(@StringRes stringRes: Int, @ColorRes colorRes: Int): Spannable {
@ -2686,6 +2758,11 @@ class BaresipService: Service() {
var audioDeviceUsbId = -1 var audioDeviceUsbId = -1
var audioDeviceCodecId = -1 var audioDeviceCodecId = -1
var micVolume = 20 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) { fun updateAudioSourceDevice(ctx: Context) {
val am = ctx.getSystemService(Context.AUDIO_SERVICE) as AudioManager val am = ctx.getSystemService(Context.AUDIO_SERVICE) as AudioManager

View File

@ -219,6 +219,14 @@ object Config {
config = "${config}mic_volume ${BaresipService.micVolume}\n" 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) for (size in defaultSizes)
videoSizes.add(size.toString()) videoSizes.add(size.toString())
/********************/ /********************/

View File

@ -11,9 +11,10 @@ import android.content.*
import android.content.Intent.ACTION_CALL import android.content.Intent.ACTION_CALL
import android.content.Intent.ACTION_DIAL import android.content.Intent.ACTION_DIAL
import android.content.Intent.ACTION_VIEW import android.content.Intent.ACTION_VIEW
import android.content.pm.PackageInfo
import android.content.pm.PackageManager import android.content.pm.PackageManager
import android.content.res.Configuration import android.content.res.Configuration
import android.graphics.Bitmap
import android.graphics.Matrix
import android.graphics.Rect import android.graphics.Rect
import android.hardware.display.DisplayManager import android.hardware.display.DisplayManager
import android.media.AudioManager import android.media.AudioManager
@ -32,10 +33,12 @@ import androidx.activity.OnBackPressedCallback
import androidx.activity.enableEdgeToEdge import androidx.activity.enableEdgeToEdge
import androidx.activity.result.ActivityResultLauncher import androidx.activity.result.ActivityResultLauncher
import androidx.activity.result.contract.ActivityResultContracts import androidx.activity.result.contract.ActivityResultContracts
import androidx.annotation.OptIn
import androidx.annotation.RequiresApi import androidx.annotation.RequiresApi
import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.app.AppCompatActivity
import androidx.appcompat.app.AppCompatDelegate import androidx.appcompat.app.AppCompatDelegate
import androidx.appcompat.widget.AppCompatButton import androidx.appcompat.widget.AppCompatButton
import androidx.camera.view.PreviewView
import androidx.core.app.ActivityCompat import androidx.core.app.ActivityCompat
import androidx.core.content.ContextCompat import androidx.core.content.ContextCompat
import androidx.core.graphics.Insets 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.getAppVersion
import com.tutpro.baresip.plus.Utils.showSnackBar import com.tutpro.baresip.plus.Utils.showSnackBar
import com.tutpro.baresip.plus.databinding.ActivityMainBinding import com.tutpro.baresip.plus.databinding.ActivityMainBinding
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import org.json.JSONObject import org.json.JSONObject
import java.io.File import java.io.File
import java.text.SimpleDateFormat import java.text.SimpleDateFormat
import java.time.LocalDateTime import java.time.LocalDateTime
import java.time.format.DateTimeFormatter import java.time.format.DateTimeFormatter
import java.util.* import java.util.*
import java.util.concurrent.Executors
import kotlin.system.exitProcess 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) { 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() { class MainActivity : AppCompatActivity() {
private lateinit var binding: ActivityMainBinding private lateinit var binding: ActivityMainBinding
@ -147,6 +215,10 @@ class MainActivity : AppCompatActivity() {
private lateinit var networkButton: AppCompatButton private lateinit var networkButton: AppCompatButton
private lateinit var backwardButton: 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 callHandler: Handler = Handler(Looper.getMainLooper())
private var callRunnable: Runnable? = null private var callRunnable: Runnable? = null
private var downloadsInputUri: Uri? = null private var downloadsInputUri: Uri? = null
@ -197,11 +269,136 @@ class MainActivity : AppCompatActivity() {
} }
lateinit var navUpList : HashMap<View, View> lateinit var navUpList : HashMap<View, View>
lateinit var navUpListAlter : HashMap<View, View>
lateinit var navDownList : HashMap<View, View> lateinit var navDownList : HashMap<View, View>
lateinit var navDownListAlter : HashMap<View, View>
lateinit var navUpServerList : HashMap<View, View> lateinit var navUpServerList : HashMap<View, View>
lateinit var navDownServerList : 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 { override fun dispatchKeyEvent(event: KeyEvent): Boolean {
if(!isKeyboardVisible) { if(!isKeyboardVisible) {
val currentFocusView = currentFocus val currentFocusView = currentFocus
@ -214,10 +411,19 @@ class MainActivity : AppCompatActivity() {
view.requestFocus() view.requestFocus()
return false return false
} else { } else {
if(currentFocusView == binding.btnApply) { if(navUpListAlter.contains(currentFocusView)) {
binding.radioDhcp.requestFocus() val view = navUpListAlter.get(currentFocusView)!!
return false if(view.isVisible) {
view.requestFocus()
return false
}
} }
// if(currentFocusView == binding.btnApply) {
// binding.radioDhcp.requestFocus()
// return false
// }
} }
} else { } else {
if(currentFocusView == binding.aorSpinner) { if(currentFocusView == binding.aorSpinner) {
@ -250,6 +456,14 @@ class MainActivity : AppCompatActivity() {
if(view.isVisible) { if(view.isVisible) {
view.requestFocus() view.requestFocus()
return false return false
} else {
if(navDownListAlter.contains(currentFocusView)) {
val view = navDownListAlter.get(currentFocusView)!!
if(view.isVisible) {
view.requestFocus()
return false
}
}
} }
} }
if(navDownServerList.contains(currentFocusView)) { if(navDownServerList.contains(currentFocusView)) {
@ -281,6 +495,9 @@ class MainActivity : AppCompatActivity() {
if(navUpList.contains(currentFocusView) || navDownList.contains(currentFocusView)) { if(navUpList.contains(currentFocusView) || navDownList.contains(currentFocusView)) {
found = true found = true
} }
// if(!found && navDownListAlter.contains(currentFocusView)) {
// found = true
// }
if(navUpServerList.contains(currentFocusView) || navDownServerList.contains(currentFocusView)) { if(navUpServerList.contains(currentFocusView) || navDownServerList.contains(currentFocusView)) {
found = true found = true
} }
@ -369,6 +586,7 @@ class MainActivity : AppCompatActivity() {
private fun updateFieldsVisibility() { private fun updateFieldsVisibility() {
val isStatic = binding.radioStatic.isChecked val isStatic = binding.radioStatic.isChecked
val visibility = if (isStatic) View.VISIBLE else View.GONE val visibility = if (isStatic) View.VISIBLE else View.GONE
binding.layoutStaticIp.visibility = visibility
binding.editIp.visibility = visibility binding.editIp.visibility = visibility
binding.editNetmask.visibility = visibility binding.editNetmask.visibility = visibility
binding.editGateway.visibility = visibility binding.editGateway.visibility = visibility
@ -478,6 +696,8 @@ class MainActivity : AppCompatActivity() {
dialpadButton = binding.dialpadButton dialpadButton = binding.dialpadButton
swipeRefresh = binding.swipeRefresh swipeRefresh = binding.swipeRefresh
previewView = binding.previewView
dialogImgBtn1 = binding.dialogButtonImg1 dialogImgBtn1 = binding.dialogButtonImg1
dialogImgBtn1.setOnClickListener { dialogImgBtn1.setOnClickListener {
@ -639,6 +859,10 @@ class MainActivity : AppCompatActivity() {
binding.seekbarMicVolume.progress = BaresipService.micVolume binding.seekbarMicVolume.progress = BaresipService.micVolume
val currentValue = binding.seekbarMicVolume.progress val currentValue = binding.seekbarMicVolume.progress
binding.textMicVolume.setText(currentValue.toString()) binding.textMicVolume.setText(currentValue.toString())
binding.seekbarAuxVolume.progress = BaresipService.auxVolume
val currentValueAux = binding.seekbarAuxVolume.progress
binding.textAuxVolume.setText(currentValueAux.toString())
} }
binding.dialogButtonVolumeInCall.setOnClickListener { binding.dialogButtonVolumeInCall.setOnClickListener {
@ -649,6 +873,10 @@ class MainActivity : AppCompatActivity() {
binding.seekbarMicVolume.progress = BaresipService.micVolume binding.seekbarMicVolume.progress = BaresipService.micVolume
val currentValue = binding.seekbarMicVolume.progress val currentValue = binding.seekbarMicVolume.progress
binding.textMicVolume.setText(currentValue.toString()) 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{ binding.seekbarMicVolume.setOnSeekBarChangeListener(object : SeekBar.OnSeekBarChangeListener{
@ -656,7 +884,7 @@ class MainActivity : AppCompatActivity() {
// progress: 현재 값 // progress: 현재 값
// fromUser: 사용자가 직접 움직였는지 여부 // fromUser: 사용자가 직접 움직였는지 여부
val currentValue = progress val currentValue = progress
println("현재 값: $currentValue") println("현재 MIC 값: $currentValue")
binding.textMicVolume.setText(currentValue.toString()) binding.textMicVolume.setText(currentValue.toString())
Utils.setMicVolumeByMix(currentValue) 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.btnDoneVolume.setOnClickListener {
binding.volumeSettingLayout.visibility = View.INVISIBLE binding.volumeSettingLayout.visibility = View.INVISIBLE
if(binding.settingLayout.visibility == View.VISIBLE) { if(binding.settingLayout.visibility == View.VISIBLE) {
@ -731,21 +979,29 @@ class MainActivity : AppCompatActivity() {
imm.showSoftInput(it, InputMethodManager.SHOW_IMPLICIT) imm.showSoftInput(it, InputMethodManager.SHOW_IMPLICIT)
} }
navUpList = HashMap<View, View>() navUpList = HashMap<View, View>()
navUpListAlter = HashMap<View, View>()
navDownList = HashMap<View, View>() navDownList = HashMap<View, View>()
navDownListAlter = HashMap<View, View>()
navUpList.put(binding.editIp, binding.radioStatic) navUpList.put(binding.editIp, binding.radioStatic)
navUpList.put(binding.editNetmask, binding.editIp) navUpList.put(binding.editNetmask, binding.editIp)
navUpList.put(binding.editGateway, binding.editNetmask) navUpList.put(binding.editGateway, binding.editNetmask)
navUpList.put(binding.editDns, binding.editGateway) navUpList.put(binding.editDns, binding.editGateway)
navUpList.put(binding.btnApply, binding.editDns) 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) //navUpList.put(binding.btnApply, binding.radioDhcp)
navDownList.put(binding.radioDhcp, binding.editIp)
navDownList.put(binding.radioStatic, 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.editIp, binding.editNetmask)
navDownList.put(binding.editNetmask, binding.editGateway) navDownList.put(binding.editNetmask, binding.editGateway)
navDownList.put(binding.editGateway, binding.editDns) navDownList.put(binding.editGateway, binding.editDns)
navDownList.put(binding.editDns, binding.btnApply) 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.editIp, downView = binding.editNetmask)
// setDpadNavigation(binding.editNetmask, upView = binding.editIp, downView = binding.editGateway) // setDpadNavigation(binding.editNetmask, upView = binding.editIp, downView = binding.editGateway)
@ -767,6 +1023,31 @@ class MainActivity : AppCompatActivity() {
showCustomToast(applicationContext, "DHCP 적용되었습니다.") showCustomToast(applicationContext, "DHCP 적용되었습니다.")
binding.networkSettingLayout.visibility = View.INVISIBLE binding.networkSettingLayout.visibility = View.INVISIBLE
} else { } 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>() val params: HashMap<String, String> = HashMap<String, String>()
params.put("IPV4TYPE", "STATIC") params.put("IPV4TYPE", "STATIC")
params.put("DEVTYPE", "ETHERNET") params.put("DEVTYPE", "ETHERNET")
@ -834,6 +1115,22 @@ class MainActivity : AppCompatActivity() {
binding.dialogButtonServer.requestFocus() 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>() navUpServerList = HashMap<View, View>()
navDownServerList = 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 } // OnCreate
fun sendSettingByBroadcast(req : String, param : String) { fun sendSettingByBroadcast(req : String, param : String) {
@ -2547,12 +2847,15 @@ class MainActivity : AppCompatActivity() {
Utils.alertView(this, getString(R.string.notice), Utils.alertView(this, getString(R.string.notice),
getString(R.string.no_network)) getString(R.string.no_network))
*/ */
Utils.renderVfdString("NO,NETWORK")
return return
} }
/* Added by ritoseo */ /* Added by ritoseo */
"network available" -> { "network available" -> {
val tv = findViewById<TextView>(R.id.textViewHeaderIp) val tv = findViewById<TextView>(R.id.textViewHeaderIp)
tv.setText(intent.getStringExtra("address")!!) tv.setText(intent.getStringExtra("address")!!)
Utils.renderVfdString(tv.text.toString())
} }
"video call" -> { "video call" -> {
callUri.setText(intent.getStringExtra("peer")!!) callUri.setText(intent.getStringExtra("peer")!!)
@ -2571,6 +2874,17 @@ class MainActivity : AppCompatActivity() {
"update info" -> { "update info" -> {
updateInfo() updateInfo()
} }
"camera connected" -> {
if(!isCallExist()) {
startSelfMoitoringCamera()
}
}
"camera removed" -> {
if(!isCallExist()) {
stopSelfMoitoringCamera()
resetSelfMoitoringCamera()
}
}
/********************/ /********************/
"call", "dial" -> { "call", "dial" -> {
if (Call.inCall()) { if (Call.inCall()) {
@ -2603,9 +2917,19 @@ class MainActivity : AppCompatActivity() {
resumeCall = call resumeCall = call
if(ev[0] == "call answer") { if(ev[0] == "call answer") {
stopSelfMoitoringCamera() // 모니터링 셀프 뷰 종료
Handler(Looper.getMainLooper()).postDelayed({ Handler(Looper.getMainLooper()).postDelayed({
videoButton.performClick() videoButton.performClick()
}, 1000) }, 1000)
Handler(Looper.getMainLooper()).postDelayed({
if(BaresipService.connectedCameraCount == 0) {
println("Answer Call setVideoFake 실행")
call.setVideoFake()
}
//call.setVideoDirection(Api.SDP_RECVONLY)
}, 1500)
} }
} }
"call missed" -> { "call missed" -> {
@ -2654,6 +2978,8 @@ class MainActivity : AppCompatActivity() {
val BTN_MISC = 188 val BTN_MISC = 188
//val BTN_MENU = 58 //val BTN_MENU = 58
val SCANCODE_OPTION = 357 //MENU키 -> OPTION키로 매핑 val SCANCODE_OPTION = 357 //MENU키 -> OPTION키로 매핑
val SCANCODE_RED = 398
val SCANCODE_GREEN = 399
//println("onKeyDown : ${keyCode}") //println("onKeyDown : ${keyCode}")
//println("onKeyDown2 : ${event?.scanCode}") //println("onKeyDown2 : ${event?.scanCode}")
val stream = if (am.mode == AudioManager.MODE_RINGTONE) val stream = if (am.mode == AudioManager.MODE_RINGTONE)
@ -2686,11 +3012,29 @@ class MainActivity : AppCompatActivity() {
editable.delete(length - 1, length) 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 -> { BTN_MISC -> {
return true return true
} }
} }
when (event?.scanCode) { when (event?.scanCode) {
SCANCODE_RED -> {
if(!isCallExist()) {
startSelfMoitoringCamera()
}
}
SCANCODE_GREEN -> {
if(!isCallExist()) {
stopSelfMoitoringCamera()
}
}
SCANCODE_OPTION -> { SCANCODE_OPTION -> {
// val dialog = findViewById<FrameLayout>(R.id.dialogLayout) // val dialog = findViewById<FrameLayout>(R.id.dialogLayout)
// if(dialog.isVisible) { // if(dialog.isVisible) {
@ -2786,6 +3130,7 @@ class MainActivity : AppCompatActivity() {
} }
updateDisplay() updateDisplay()
BaresipService.supportedCameras = Utils.supportedCameras(applicationContext).isNotEmpty()
handleNextEvent() handleNextEvent()
return return
} }
@ -2870,6 +3215,13 @@ class MainActivity : AppCompatActivity() {
showCall(ua) showCall(ua)
} }
"call established" -> { "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) { if (aor == aorSpinner.tag) {
dtmf.text = null dtmf.text = null
dtmf.hint = getString(R.string.dtmf) dtmf.hint = getString(R.string.dtmf)
@ -3031,6 +3383,7 @@ class MainActivity : AppCompatActivity() {
switchVideoLayout(false) switchVideoLayout(false)
//callVideoButton.requestFocus() //callVideoButton.requestFocus()
Utils.renderVfdString(binding.textViewHeaderIp.text.toString())
BaresipService.isMicMuted = false BaresipService.isMicMuted = false
BaresipService.isVideoMuted = false BaresipService.isVideoMuted = false
} }
@ -3653,6 +4006,7 @@ class MainActivity : AppCompatActivity() {
} }
private fun makeCall(kind: String, lookForContact: Boolean = true) { private fun makeCall(kind: String, lookForContact: Boolean = true) {
stopSelfMoitoringCamera() // 모니터링 Self 카메라 멈춤
callUri.setAdapter(null) callUri.setAdapter(null)
val ua = BaresipService.uas[aorSpinner.selectedItemPosition] val ua = BaresipService.uas[aorSpinner.selectedItemPosition]
val aor = ua.account.aor 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 { fun putFileContents(filePath: String, contents: ByteArray): Boolean {
try { try {
File(filePath).writeBytes(contents) 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) { fun runShellOrderSuper(order : String) {
val process = Runtime.getRuntime().exec("ritosysc shell-order=" + order) val process = Runtime.getRuntime().exec("ritosysc shell-order=" + order)
process.waitFor() process.waitFor()
@ -1409,6 +1536,13 @@ object Utils {
return found 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 { fun checkCameraConnection(idx : Int): String {
if(idx == 0) { if(idx == 0) {
val file = File("/d/hdmirx/status") // 파일 경로 설정 val file = File("/d/hdmirx/status") // 파일 경로 설정
@ -1547,6 +1681,30 @@ object Utils {
return netInfo 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 { fun calculateNetmask(prefixLength: Int): String {
val mask = (0xFFFFFFFF shl (32 - prefixLength)) val mask = (0xFFFFFFFF shl (32 - prefixLength))
return listOf( 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:focusable="true"
android:focusableInTouchMode="true" android:focusableInTouchMode="true"
android:paddingRight="15dp" android:paddingRight="15dp"
android:nextFocusLeft="@id/radioDhcp"
android:nextFocusRight="@id/radioStatic"
android:background="@drawable/bg_button_selector_radio"
android:text="DHCP" android:text="DHCP"
android:textSize="24sp" /> android:textSize="24sp" />
@ -942,42 +945,103 @@
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:focusable="true" android:focusable="true"
android:focusableInTouchMode="true" android:focusableInTouchMode="true"
android:nextFocusLeft="@id/radioDhcp"
android:nextFocusRight="@id/radioStatic"
android:background="@drawable/bg_button_selector_radio"
android:paddingRight="15dp" android:paddingRight="15dp"
android:text="STATIC" android:text="STATIC"
android:textSize="24sp" /> android:textSize="24sp" />
</RadioGroup> </RadioGroup>
<EditText <LinearLayout
android:id="@+id/editIp" android:id="@+id/layoutStaticIp"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:hint="IP Address" android:layout_marginTop="4dp"
android:inputType="phone" android:visibility="gone"
android:textSize="24sp" /> android:orientation="vertical">
<EditText <LinearLayout
android:id="@+id/editNetmask" android:layout_width="match_parent"
android:layout_width="match_parent" android:layout_height="wrap_content"
android:layout_height="wrap_content" android:orientation="horizontal"
android:hint="Netmask" android:padding="16dp">
android:inputType="phone" <TextView
android:textSize="24sp" /> 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 <LinearLayout
android:id="@+id/editGateway" android:layout_width="match_parent"
android:layout_width="match_parent" android:layout_height="wrap_content"
android:layout_height="wrap_content" android:orientation="horizontal"
android:hint="Gateway" android:padding="16dp">
android:inputType="phone" <TextView
android:textSize="24sp" /> 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 <LinearLayout
android:id="@+id/editDns" android:layout_width="match_parent"
android:layout_width="match_parent" android:layout_height="wrap_content"
android:layout_height="wrap_content" android:orientation="horizontal"
android:hint="DNS" android:padding="16dp">
android:inputType="phone" <TextView
android:textSize="24sp" /> 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 <LinearLayout
android:layout_width="match_parent" android:layout_width="match_parent"
@ -985,27 +1049,35 @@
android:layout_marginTop="16dp" android:layout_marginTop="16dp"
android:orientation="horizontal"> android:orientation="horizontal">
<Button <androidx.appcompat.widget.AppCompatButton
android:id="@+id/btnApply" android:id="@+id/btnApply"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_margin="16dp" android:layout_margin="16dp"
android:paddingHorizontal="20dp" android:paddingHorizontal="30dp"
android:paddingVertical="10dp" android:paddingVertical="15dp"
android:textColor="@color/colorWhite"
android:background="@drawable/bg_button_selector_setting"
android:nextFocusLeft="@id/btnApply" android:nextFocusLeft="@id/btnApply"
android:nextFocusRight="@id/btnCancel" android:nextFocusRight="@id/btnCancel"
android:nextFocusDown="@id/btnApply"
android:textStyle="bold"
android:text="적용하기" android:text="적용하기"
android:textSize="24sp" /> android:textSize="24sp" />
<Button <androidx.appcompat.widget.AppCompatButton
android:id="@+id/btnCancel" android:id="@+id/btnCancel"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_margin="16dp" android:layout_margin="16dp"
android:paddingHorizontal="20dp" android:paddingHorizontal="30dp"
android:paddingVertical="10dp" android:paddingVertical="15dp"
android:textColor="@color/colorWhite"
android:background="@drawable/bg_button_selector_setting"
android:nextFocusLeft="@id/btnApply" android:nextFocusLeft="@id/btnApply"
android:nextFocusRight="@id/btnCancel" android:nextFocusRight="@id/btnCancel"
android:nextFocusDown="@id/btnCancel"
android:textStyle="bold"
android:text="취소하기" android:text="취소하기"
android:textSize="24sp" /> android:textSize="24sp" />
</LinearLayout> </LinearLayout>
@ -1144,6 +1216,8 @@
android:focusableInTouchMode="true" android:focusableInTouchMode="true"
android:paddingRight="15dp" android:paddingRight="15dp"
android:nextFocusUp="@id/editSipServer" android:nextFocusUp="@id/editSipServer"
android:nextFocusLeft="@id/radioTransportUdp"
android:background="@drawable/bg_button_selector_radio"
android:text="UDP" android:text="UDP"
android:textSize="24sp" /> android:textSize="24sp" />
@ -1155,6 +1229,7 @@
android:focusableInTouchMode="true" android:focusableInTouchMode="true"
android:paddingRight="15dp" android:paddingRight="15dp"
android:nextFocusUp="@id/editSipServer" android:nextFocusUp="@id/editSipServer"
android:background="@drawable/bg_button_selector_radio"
android:text="TCP" android:text="TCP"
android:textSize="24sp" /> android:textSize="24sp" />
@ -1166,6 +1241,8 @@
android:focusableInTouchMode="true" android:focusableInTouchMode="true"
android:paddingRight="15dp" android:paddingRight="15dp"
android:nextFocusUp="@id/editSipServer" android:nextFocusUp="@id/editSipServer"
android:nextFocusRight="@id/radioTransportTls"
android:background="@drawable/bg_button_selector_radio"
android:text="TLS" android:text="TLS"
android:textSize="24sp" /> android:textSize="24sp" />
</RadioGroup> </RadioGroup>
@ -1205,6 +1282,8 @@
android:focusableInTouchMode="true" android:focusableInTouchMode="true"
android:paddingRight="15dp" android:paddingRight="15dp"
android:nextFocusDown="@id/btnApplyServer" android:nextFocusDown="@id/btnApplyServer"
android:nextFocusLeft="@id/radioEncryptNone"
android:background="@drawable/bg_button_selector_radio"
android:text="사용안함" android:text="사용안함"
android:textSize="24sp" /> android:textSize="24sp" />
@ -1216,6 +1295,8 @@
android:focusableInTouchMode="true" android:focusableInTouchMode="true"
android:paddingRight="15dp" android:paddingRight="15dp"
android:nextFocusDown="@id/btnApplyServer" android:nextFocusDown="@id/btnApplyServer"
android:nextFocusRight="@id/radioEncryptSrtp"
android:background="@drawable/bg_button_selector_radio"
android:text="SRTP" android:text="SRTP"
android:textSize="24sp" /> android:textSize="24sp" />
@ -1230,31 +1311,54 @@
android:layout_marginTop="16dp" android:layout_marginTop="16dp"
android:orientation="horizontal"> android:orientation="horizontal">
<Button <androidx.appcompat.widget.AppCompatButton
android:id="@+id/btnApplyServer" android:id="@+id/btnApplyServer"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_margin="16dp" android:layout_margin="16dp"
android:paddingHorizontal="20dp" android:paddingHorizontal="30dp"
android:paddingVertical="10dp" android:paddingVertical="15dp"
android:nextFocusUp="@id/radioEncryptNone" android:nextFocusUp="@id/radioEncryptNone"
android:nextFocusLeft="@id/btnApplyServer" android:nextFocusLeft="@id/btnApplyServer"
android:nextFocusRight="@id/btnCancelServer" android:nextFocusRight="@id/btnCancelServer"
android:textColor="@color/colorWhite"
android:background="@drawable/bg_button_selector_setting"
android:textStyle="bold"
android:text="적용하기" android:text="적용하기"
android:textSize="24sp" /> android:textSize="24sp" />
<Button <androidx.appcompat.widget.AppCompatButton
android:id="@+id/btnCancelServer" android:id="@+id/btnCancelServer"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_margin="16dp" android:layout_margin="16dp"
android:paddingHorizontal="20dp" android:paddingHorizontal="30dp"
android:paddingVertical="10dp" android:paddingVertical="15dp"
android:nextFocusUp="@id/radioEncryptNone" android:nextFocusUp="@id/radioEncryptNone"
android:nextFocusLeft="@id/btnApplyServer" 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:text="취소하기"
android:textSize="24sp" /> 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>
</LinearLayout> </LinearLayout>
@ -1302,6 +1406,7 @@
android:max="24" android:max="24"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:nextFocusUp="@id/seekbarMicVolume" android:nextFocusUp="@id/seekbarMicVolume"
android:nextFocusDown="@id/seekbarAuxVolume"
android:nextFocusLeft="@id/seekbarMicVolume" android:nextFocusLeft="@id/seekbarMicVolume"
android:nextFocusRight="@id/seekbarMicVolume" android:nextFocusRight="@id/seekbarMicVolume"
android:textSize="24sp" /> android:textSize="24sp" />
@ -1317,23 +1422,62 @@
</LinearLayout> </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 <LinearLayout
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginTop="16dp" android:layout_marginTop="16dp"
android:orientation="horizontal"> android:orientation="horizontal">
<Button <androidx.appcompat.widget.AppCompatButton
android:id="@+id/btnDoneVolume" android:id="@+id/btnDoneVolume"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_margin="16dp" android:layout_margin="16dp"
android:paddingHorizontal="20dp" android:paddingHorizontal="30dp"
android:paddingVertical="10dp" android:paddingVertical="15dp"
android:nextFocusUp="@id/seekbarMicVolume" android:nextFocusUp="@id/seekbarAuxVolume"
android:nextFocusLeft="@id/btnDoneVolume" android:nextFocusLeft="@id/btnDoneVolume"
android:nextFocusRight="@id/btnDoneVolume" android:nextFocusRight="@id/btnDoneVolume"
android:nextFocusDown="@id/btnDoneVolume" android:nextFocusDown="@id/btnDoneVolume"
android:textColor="@color/colorWhite"
android:background="@drawable/bg_button_selector_setting"
android:textStyle="bold"
android:text="돌아가기" android:text="돌아가기"
android:textSize="24sp" /> android:textSize="24sp" />
</LinearLayout> </LinearLayout>
@ -1549,4 +1693,24 @@
</FrameLayout> </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> </RelativeLayout>