UI가독성 변경 및 VFD 제어 추가
FakeView incoming call 동작 되도록 처리
This commit is contained in:
parent
267ebbfc44
commit
74803af21b
@ -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"
|
||||
}
|
||||
|
||||
|
@ -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" />
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
||||
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)
|
||||
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")
|
||||
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,7 +2343,9 @@ class BaresipService: Service() {
|
||||
}
|
||||
}
|
||||
|
||||
fun showCustomToast(context: Context, message: String) {
|
||||
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)
|
||||
|
||||
@ -2283,10 +2353,12 @@ class BaresipService: Service() {
|
||||
textView.text = message
|
||||
|
||||
val toast = Toast(context)
|
||||
toast.duration = Toast.LENGTH_LONG
|
||||
toast.duration = duration
|
||||
toast.view = layout
|
||||
toast.show()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun getActionText(@StringRes stringRes: Int, @ColorRes colorRes: Int): Spannable {
|
||||
val spannable: Spannable = SpannableString(applicationContext.getText(stringRes))
|
||||
@ -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
|
||||
|
@ -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())
|
||||
/********************/
|
||||
|
@ -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,11 +411,20 @@ class MainActivity : AppCompatActivity() {
|
||||
view.requestFocus()
|
||||
return false
|
||||
} else {
|
||||
if(currentFocusView == binding.btnApply) {
|
||||
binding.radioDhcp.requestFocus()
|
||||
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) {
|
||||
callStartButton.requestFocus()
|
||||
@ -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
|
||||
|
@ -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(
|
||||
|
15
app/src/main/res/drawable/bg_button_selector_radio.xml
Normal file
15
app/src/main/res/drawable/bg_button_selector_radio.xml
Normal 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>
|
24
app/src/main/res/drawable/bg_button_selector_setting.xml
Normal file
24
app/src/main/res/drawable/bg_button_selector_setting.xml
Normal 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>
|
@ -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,35 +945,93 @@
|
||||
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>
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/layoutStaticIp"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="4dp"
|
||||
android:visibility="gone"
|
||||
android:orientation="vertical">
|
||||
|
||||
<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="match_parent"
|
||||
android:layout_width="1000px"
|
||||
android:layout_height="wrap_content"
|
||||
android:hint="IP Address"
|
||||
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="NETMASK"
|
||||
android:textSize="24sp"
|
||||
/>
|
||||
<EditText
|
||||
android:id="@+id/editNetmask"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_width="1000px"
|
||||
android:layout_height="wrap_content"
|
||||
android:hint="Netmask"
|
||||
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="GATEWAY"
|
||||
android:textSize="24sp"
|
||||
/>
|
||||
<EditText
|
||||
android:id="@+id/editGateway"
|
||||
android:layout_width="match_parent"
|
||||
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"
|
||||
@ -978,6 +1039,9 @@
|
||||
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>
|
||||
|
Loading…
x
Reference in New Issue
Block a user