UI가독성 변경 및 VFD 제어 추가
FakeView incoming call 동작 되도록 처리
This commit is contained in:
parent
267ebbfc44
commit
74803af21b
@ -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"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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" />
|
||||||
|
@ -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
|
||||||
|
@ -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
|
||||||
|
@ -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())
|
||||||
/********************/
|
/********************/
|
||||||
|
@ -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
|
||||||
|
@ -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(
|
||||||
|
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: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>
|
||||||
|
Loading…
x
Reference in New Issue
Block a user