From ccd62d913b10a153bbd6310b0679896393c9dee5 Mon Sep 17 00:00:00 2001 From: ritoseo Date: Wed, 23 Apr 2025 17:08:09 +0900 Subject: [PATCH] =?UTF-8?q?=ED=86=A0=EC=8A=A4=ED=8A=B8=20=EB=94=94?= =?UTF-8?q?=EC=9E=90=EC=9D=B8=20=EB=B3=80=EA=B2=BD=20=ED=86=B5=ED=99=94?= =?UTF-8?q?=EC=9D=B4=EB=A0=A5=20=EC=82=AD=EC=A0=9C=20=EA=B8=B0=EB=8A=A5=20?= =?UTF-8?q?=EC=97=B0=EB=8F=99=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/src/main/assets/config.static | 3 +- app/src/main/cpp/baresip.c | 10 + .../com/tutpro/baresip/plus/BaresipService.kt | 63 +++++- .../com/tutpro/baresip/plus/CallHistory.kt | 11 + .../kotlin/com/tutpro/baresip/plus/Config.kt | 18 +- .../com/tutpro/baresip/plus/MainActivity.kt | 196 +++++++++++++++--- .../kotlin/com/tutpro/baresip/plus/Utils.kt | 27 +++ .../main/res/drawable/toast_frame_rito.xml | 23 ++ app/src/main/res/layout/activity_main.xml | 8 +- app/src/main/res/layout/custom_toast.xml | 2 +- .../baresip/lib/arm64-v8a/libbaresip.a | Bin 5567156 -> 5599396 bytes 11 files changed, 325 insertions(+), 36 deletions(-) create mode 100644 app/src/main/res/drawable/toast_frame_rito.xml diff --git a/app/src/main/assets/config.static b/app/src/main/assets/config.static index 17bb0f1..29b6eaf 100644 --- a/app/src/main/assets/config.static +++ b/app/src/main/assets/config.static @@ -5,7 +5,7 @@ call_hold_other_calls yes audio_player aaudio,nil audio_source aaudio,nil audio_alert aaudio,nil -audio_level no +audio_level yes ausrc_format s16 auplay_format s16 auenc_format s16 @@ -36,6 +36,7 @@ module srtp.so module dtls_srtp.so module gzrtp.so module uuid.so +module vumeter.so module_app account.so module_app debug_cmd.so module_app mwi.so diff --git a/app/src/main/cpp/baresip.c b/app/src/main/cpp/baresip.c index f1ee081..36f4616 100644 --- a/app/src/main/cpp/baresip.c +++ b/app/src/main/cpp/baresip.c @@ -305,6 +305,16 @@ static void event_handler(enum ua_event ev, struct bevent *event, void *arg) len = re_snprintf(event_buf, sizeof event_buf, "recorder sessionid,%r", &data); break; } + /* Added by ritoseo */ + case UA_EVENT_VU_TX: + len = re_snprintf(event_buf, sizeof event_buf, "vu_tx_report,%s", prm); + //LOGE("call tx audio level, %s\n", prm); + break; + case UA_EVENT_VU_RX: + len = re_snprintf(event_buf, sizeof event_buf, "vu_rx_report,%s", prm); + //LOGE("call rx audio level, %s\n", prm); + break; + /********************/ default: return; } diff --git a/app/src/main/kotlin/com/tutpro/baresip/plus/BaresipService.kt b/app/src/main/kotlin/com/tutpro/baresip/plus/BaresipService.kt index 9a98e72..76661bf 100644 --- a/app/src/main/kotlin/com/tutpro/baresip/plus/BaresipService.kt +++ b/app/src/main/kotlin/com/tutpro/baresip/plus/BaresipService.kt @@ -326,6 +326,10 @@ class BaresipService: Service() { responseIntent.putExtra("data", resArr.toString()) responseIntent.setPackage("kr.co.rito.sipsvc"); context?.sendBroadcast(responseIntent) + } else if(req == "remove_history") { + CallsActivity.uaHistory.clear() + BaresipService.callHistory.clear() + CallHistoryNew.delete() } else if(req == "contact_list") { sendContactList() } else if(req == "save_address") { @@ -550,8 +554,15 @@ class BaresipService: Service() { displaySplitMode = DISPLAY_SPLIT_MODE_원거리_대하단_근거리_소상단 } else if(layout == "layout8") { displaySplitMode = DISPLAY_SPLIT_MODE_원거리_반좌단_근거리_반우단 + } else if(layout == "layout9") { + displaySplitMode = DISPLAY_SPLIT_MODE_원거리_최대_근거리_없음 + } else if(layout == "layout10") { + displaySplitMode = DISPLAY_SPLIT_MODE_원거리_없음_근거리_최대 } + Config.replaceVariable("display_split_mode", displaySplitMode.toString()) + Config.save() + sendActivityAction("update layout") } } else if(req == "misc_setting") { @@ -559,12 +570,17 @@ class BaresipService: Service() { if(param != null) { val json = JSONObject(param) val device_name = json.getString("device_name") + val display_type = json.getString("display_type") Config.replaceVariable("device_name", device_name) BaresipService.deviceName = device_name + + Config.replaceVariable("far_view_display_id", display_type) + BaresipService.farViewDisplayId = display_type.toInt() Config.save() sendActivityAction("update info") + sendActivityAction("update display") } } } @@ -929,14 +945,24 @@ class BaresipService: Service() { if(display2 == "connected") connectedCount++ + //println("Changed connectedDisplayCount : ${connectedDisplayCount}, connectedCount : ${connectedCount}") if(connectedDisplayCount != connectedCount) { val displayManager = getSystemService(DISPLAY_SERVICE) as DisplayManager val displays = displayManager.displays - if(connectedCount == displays.size) { + if(connectedCount > connectedDisplayCount) { + if(connectedCount == displays.size) { + connectedDisplayCount = connectedCount + //println("Changed connectedDisplayCount : ${connectedDisplayCount}") + + sendActivityAction("update display") + } + } else { connectedDisplayCount = connectedCount + //println("Changed connectedDisplayCount : ${connectedDisplayCount}") sendActivityAction("update display") } + } //connectedDisplayCount = connectedCount Utils.propertySet("sys.ritosip.display1.status", display1) @@ -1030,7 +1056,17 @@ class BaresipService: Service() { } } + if(BaresipService.deviceName != null) { + Utils.propertySet("sys.ritosip.device_name", BaresipService.deviceName) + } + if(BaresipService.callAnswerMode != null) { + Utils.propertySet("sys.ritosip.answer_mode", BaresipService.callAnswerMode) + } + + Utils.propertySet("sys.ritosip.far_view_display_id", BaresipService.farViewDisplayId.toString()) + Utils.propertySet("sys.ritosip.display_split_mode", BaresipService.displaySplitMode.toString()) + Utils.propertySet("sys.ritosip.sip.total_duration", BaresipService.totalDurationSeconds.toString()) captureCount++ delay(100) @@ -1125,6 +1161,11 @@ class BaresipService: Service() { CallHistoryNew.save() } + val value = Utils.readNumberFromFile("/sdcard/Documents/sip_total_duration") + if(value != null) { + BaresipService.totalDurationSeconds = value + } + Message.restore() hotSpotAddresses = Utils.hotSpotAddresses() @@ -1366,6 +1407,16 @@ class BaresipService: Service() { return } + if (ev[0] == "vu_tx_report") { + Utils.propertySet("sys.ritosip.call.audio.tx_level", ev[1]) + return + } + + if (ev[0] == "vu_rx_report") { + Utils.propertySet("sys.ritosip.call.audio.rx_level", ev[1]) + return + } + val ua = UserAgent.ofUap(uap) val aor = ua?.account?.aor @@ -1770,6 +1821,13 @@ class BaresipService: Service() { CallHistoryNew.save() ua.account.missedCalls = ua.account.missedCalls || missed } + + /* Added by ritoseo */ + val duration = call.duration() + BaresipService.totalDurationSeconds += duration + Utils.saveNumberToFile("/sdcard/Documents/sip_total_duration", BaresipService.totalDurationSeconds) + /* ---------------- */ + if (!Utils.isVisible()) { if (missed) { val caller = Utils.friendlyUri(this, call.peerUri, ua.account) @@ -2471,12 +2529,15 @@ class BaresipService: Service() { val DISPLAY_SPLIT_MODE_원거리_대좌단_근거리_소우단 = 6 val DISPLAY_SPLIT_MODE_원거리_대하단_근거리_소상단 = 7 val DISPLAY_SPLIT_MODE_원거리_반좌단_근거리_반우단 = 8 + val DISPLAY_SPLIT_MODE_원거리_최대_근거리_없음 = 9 + val DISPLAY_SPLIT_MODE_원거리_없음_근거리_최대 = 10 var isSupportAudioCall = false var deviceIpAddress = "0.0.0.0" var deviceName = "" var preferTransport = "udp" var videoFormatHeight = 480 var connectedDisplayCount = 0 + var prevConnectedDisplayCount = 0 var callAnswerMode = "auto" var audioInputDevice = "usb" var useDisplaySplitMode = false diff --git a/app/src/main/kotlin/com/tutpro/baresip/plus/CallHistory.kt b/app/src/main/kotlin/com/tutpro/baresip/plus/CallHistory.kt index 279ed52..8515ccc 100644 --- a/app/src/main/kotlin/com/tutpro/baresip/plus/CallHistory.kt +++ b/app/src/main/kotlin/com/tutpro/baresip/plus/CallHistory.kt @@ -75,6 +75,17 @@ class CallHistoryNew(val aor: String, val peerUri: String, val direction: String } } + fun delete() { + Log.d(TAG, "Deleting history of ${BaresipService.callHistory.size} calls") + val file = File(BaresipService.filesPath + "/call_history") + try { + file.delete() + } catch (e: IOException) { + Log.e(TAG, "Deleting exception: $e") + e.printStackTrace() + } + } + fun restore() { val file = File(BaresipService.filesPath + "/call_history") if (file.exists()) { diff --git a/app/src/main/kotlin/com/tutpro/baresip/plus/Config.kt b/app/src/main/kotlin/com/tutpro/baresip/plus/Config.kt index d249652..e04d782 100644 --- a/app/src/main/kotlin/com/tutpro/baresip/plus/Config.kt +++ b/app/src/main/kotlin/com/tutpro/baresip/plus/Config.kt @@ -276,9 +276,25 @@ object Config { BaresipService.callAnswerMode = "auto" config = "${config}answer_mode ${BaresipService.callAnswerMode}\n" + + val farViewDisplayId = previousVariable("far_view_display_id") + if (farViewDisplayId != "") + BaresipService.farViewDisplayId = farViewDisplayId.toInt() + else + BaresipService.farViewDisplayId = 1 + config = "${config}far_view_display_id ${BaresipService.farViewDisplayId}\n" + + + val displaySplitMode = previousVariable("display_split_mode") + if (displaySplitMode != "") + BaresipService.displaySplitMode = displaySplitMode.toInt() + else + BaresipService.displaySplitMode = BaresipService.DISPLAY_SPLIT_MODE_원거리_최대_근거리_우하단 + config = "${config}display_split_mode ${BaresipService.displaySplitMode}\n" + + save() BaresipService.isConfigInitialized = true - } private fun previousVariable(name: String): String { diff --git a/app/src/main/kotlin/com/tutpro/baresip/plus/MainActivity.kt b/app/src/main/kotlin/com/tutpro/baresip/plus/MainActivity.kt index bef2699..6fd2679 100644 --- a/app/src/main/kotlin/com/tutpro/baresip/plus/MainActivity.kt +++ b/app/src/main/kotlin/com/tutpro/baresip/plus/MainActivity.kt @@ -195,7 +195,7 @@ class MainActivity : AppCompatActivity() { if(!isKeyboardVisible) { val currentFocusView = currentFocus if(event.action == KeyEvent.ACTION_UP) { - println("pokaRITO pokachip z. " + event) + println("pokaRITO pokachip z. " + event + " focusView : " + currentFocusView) if(event.keyCode == KeyEvent.KEYCODE_DPAD_UP) { if(navUpList.contains(currentFocusView)) { val view = navUpList.get(currentFocusView)!! @@ -208,23 +208,20 @@ class MainActivity : AppCompatActivity() { return false } } + } else { + if(currentFocusView == binding.aorSpinner) { + callStartButton.requestFocus() + return false + } } } else if(event.keyCode == KeyEvent.KEYCODE_DPAD_DOWN) { - println("pokaRITO pokachip k. " + currentFocusView) - if(binding.radioStatic.isFocused) { - println("pokaRITO pokachip radioStatic focused") - } if(navDownList.contains(currentFocusView)) { - println("pokaRITO pokachip zz. " + currentFocusView) val view = navDownList.get(currentFocusView)!! if(view.isVisible) { - println("pokaRITO pokachip zzz. " + view) view.requestFocus() return false } - } else if(currentFocusView is RadioButton) { - println("pokaRITO pokachip rr. " + currentFocusView) } } else if(event.keyCode == KeyEvent.KEYCODE_ENTER) { @@ -438,7 +435,12 @@ class MainActivity : AppCompatActivity() { dialogImgBtn2 = binding.dialogButtonImg2 callStartButton = binding.callStartBtn callStartButton.setOnClickListener { - callVideoButton.performClick() + val status = Utils.propertyGet("sys.ritosip.sip.status") + if(status == "registered") { + callVideoButton.performClick() + } else { + showCustomToast(this, "SIP서버에 연결 되어 있지 않습니다.") + } } //callHistoryButton = binding.callHistoryBtn settingButton = binding.settingBtn @@ -1323,6 +1325,7 @@ class MainActivity : AppCompatActivity() { // } catch(e : java.lang.Exception) { // } // presentation?.setContentView(videoView.surfaceSelfView) + videoView.surfaceView.bringToFront() videoView.surfaceSelfView.bringToFront() } else { // try { @@ -1550,6 +1553,48 @@ class MainActivity : AppCompatActivity() { prm2.leftMargin = 960 prm2.topMargin = 270 + videoView.surfaceSelfView.layoutParams = prm2 + } else if(BaresipService.displaySplitMode == BaresipService.DISPLAY_SPLIT_MODE_원거리_최대_근거리_없음) { + var prm1: RelativeLayout.LayoutParams = + RelativeLayout.LayoutParams( + 1920, + 1080 + ) + prm1.leftMargin = 0 + prm1.topMargin = 0 + + videoView.surfaceView.layoutParams = prm1 + + + var prm2: RelativeLayout.LayoutParams = + RelativeLayout.LayoutParams( + 1, + 1 + ) + prm2.leftMargin = 0 + prm2.topMargin = 0 + + videoView.surfaceSelfView.layoutParams = prm2 + } else if(BaresipService.displaySplitMode == BaresipService.DISPLAY_SPLIT_MODE_원거리_없음_근거리_최대) { + var prm1: RelativeLayout.LayoutParams = + RelativeLayout.LayoutParams( + 1, + 1 + ) + prm1.leftMargin = 0 + prm1.topMargin = 0 + + videoView.surfaceView.layoutParams = prm1 + + + var prm2: RelativeLayout.LayoutParams = + RelativeLayout.LayoutParams( + 1920, + 1080 + ) + prm2.leftMargin = 0 + prm2.topMargin = 0 + videoView.surfaceSelfView.layoutParams = prm2 } @@ -1568,8 +1613,44 @@ class MainActivity : AppCompatActivity() { } private fun updateDisplay() { - println("Update Display : ${BaresipService.connectedDisplayCount}") + println("Update Display : ${BaresipService.connectedDisplayCount}, prevDisplayCount : ${BaresipService.prevConnectedDisplayCount}") if(BaresipService.connectedDisplayCount < 2) { + if(BaresipService.prevConnectedDisplayCount != BaresipService.connectedDisplayCount) { + try { + presentation?.dismiss() + } catch(e : java.lang.Exception) { + } + + try { + val parentView = videoView.surfaceView.parent + (parentView as ViewGroup).removeView(videoView.surfaceView) + } catch(e : java.lang.Exception) { + } + + try { + val parentView = videoView.surfaceSelfView.parent + (parentView as ViewGroup).removeView(videoView.surfaceSelfView) + } catch(e : java.lang.Exception) { + } + + try { + val parentView = videoView.standbyView.parent + (parentView as ViewGroup).removeView(videoView.standbyView) + } catch(e : java.lang.Exception) { + } + + + var prm: FrameLayout.LayoutParams = + FrameLayout.LayoutParams( + 1920, + 1080 + ) + prm.leftMargin = 0 + prm.topMargin = 0 + videoView.surfaceView.layoutParams = prm + videoLayout.addView(videoView.surfaceView) + } + try { val parentView = videoView.surfaceSelfView.parent (parentView as ViewGroup).removeView(videoView.surfaceSelfView) @@ -1589,6 +1670,17 @@ class MainActivity : AppCompatActivity() { updateDisplayLayout() } else { + try { + presentation?.dismiss() + } catch(e : java.lang.Exception) { + } + + try { + val parentView = videoView.surfaceView.parent + (parentView as ViewGroup).removeView(videoView.surfaceView) + } catch(e : java.lang.Exception) { + } + try { val parentView = videoView.surfaceSelfView.parent (parentView as ViewGroup).removeView(videoView.surfaceSelfView) @@ -1601,6 +1693,13 @@ class MainActivity : AppCompatActivity() { } catch(e : java.lang.Exception) { } + var useSecondScreenAsFar = false + if(BaresipService.farViewDisplayId == 1) { + useSecondScreenAsFar = false + } else if(BaresipService.farViewDisplayId == 2) { + useSecondScreenAsFar = true + } + val displayManager = getSystemService(DISPLAY_SERVICE) as DisplayManager val displays = displayManager.displays val secondDisplay = displays[1] // 두 번째 디스플레이 가져오기 @@ -1615,31 +1714,57 @@ class MainActivity : AppCompatActivity() { prm1.topMargin = 0 videoView.surfaceView.layoutParams = prm1 - var prm2: RelativeLayout.LayoutParams = - RelativeLayout.LayoutParams( - 1920, - 1080 - ) - prm2.leftMargin = 0 - prm2.topMargin = 0 - videoView.surfaceSelfView.layoutParams = prm2 - presentation?.setContentView(R.layout.presentation_layout) - val presentationView = presentation?.window?.decorView?.findViewById(R.id.presentationLayout) - try { - presentationView?.addView(videoView.surfaceSelfView) - } catch(e : java.lang.Exception) { - } - try { + if(useSecondScreenAsFar) { + //presentation?.setContentView(videoView.surfaceView) + presentation?.setContentView(R.layout.presentation_layout) + val presentationView = presentation?.window?.decorView?.findViewById(R.id.presentationLayout) + presentationView?.addView(videoView.surfaceView) presentationView?.addView(videoView.standbyView) - } catch(e : java.lang.Exception) { + } else { + var prm2: RelativeLayout.LayoutParams = + RelativeLayout.LayoutParams( + 1920, + 1080 + ) + prm2.leftMargin = 0 + prm2.topMargin = 0 + videoView.surfaceSelfView.layoutParams = prm2 + presentation?.setContentView(R.layout.presentation_layout) + val presentationView = presentation?.window?.decorView?.findViewById(R.id.presentationLayout) + try { + presentationView?.addView(videoView.surfaceSelfView) + } catch(e : java.lang.Exception) { + } + try { + presentationView?.addView(videoView.standbyView) + } catch(e : java.lang.Exception) { + } } + + if(useSecondScreenAsFar) { + var prm2: RelativeLayout.LayoutParams = + RelativeLayout.LayoutParams( + 1920, + 1080 + ) + prm2.leftMargin = 0 + prm2.topMargin = 0 + videoView.surfaceSelfView.layoutParams = prm2 + videoLayout.addView(videoView.surfaceSelfView) + } else { + videoLayout.addView(videoView.surfaceView) + } + if(isCallExist()) { videoView.surfaceSelfView.bringToFront() + videoView.surfaceView.bringToFront() } else { videoView.standbyView.bringToFront() } presentation?.show() } + + BaresipService.prevConnectedDisplayCount = BaresipService.connectedDisplayCount } private fun addVideoLayoutViews() { @@ -1678,18 +1803,29 @@ class MainActivity : AppCompatActivity() { videoView.surfaceSelfView.layoutParams = prm2 videoLayout.addView(videoView.surfaceSelfView) + + updateDisplayLayout() } else { - val useSecondScreenAsFar = false; + var useSecondScreenAsFar = false // DisplayManager 가져오기 val displayManager = getSystemService(DISPLAY_SERVICE) as DisplayManager val displays = displayManager.displays + if(BaresipService.farViewDisplayId == 1) { + useSecondScreenAsFar = false + } else if(BaresipService.farViewDisplayId == 2) { + useSecondScreenAsFar = true + } + if (displays.size > 1) { // 보조 디스플레이가 있는 경우 println("디스플레이 상태 : [0]${Utils.checkDisplayConnection(0)} [1]${Utils.checkDisplayConnection(1)}") val secondDisplay = displays[1] // 두 번째 디스플레이 가져오기 presentation = SecondScreenPresentation(this, secondDisplay) if(useSecondScreenAsFar) { - presentation?.setContentView(videoView.surfaceView) + presentation?.setContentView(R.layout.presentation_layout) + val presentationView = presentation?.window?.decorView?.findViewById(R.id.presentationLayout) + presentationView?.addView(videoView.surfaceView) + presentationView?.addView(videoView.standbyView) } else { //presentation?.setContentView(videoView.surfaceSelfView) var prm2: FrameLayout.LayoutParams = @@ -2270,6 +2406,8 @@ class MainActivity : AppCompatActivity() { AppCompatDelegate.setDefaultNightMode(Preferences(applicationContext).displayTheme) delegate.applyDayNight() } + + updateDisplay() handleNextEvent() return } diff --git a/app/src/main/kotlin/com/tutpro/baresip/plus/Utils.kt b/app/src/main/kotlin/com/tutpro/baresip/plus/Utils.kt index 7e82907..31e1da0 100644 --- a/app/src/main/kotlin/com/tutpro/baresip/plus/Utils.kt +++ b/app/src/main/kotlin/com/tutpro/baresip/plus/Utils.kt @@ -1331,6 +1331,33 @@ object Utils { return "plugout" } + fun saveNumberToFile(path: String, number: Int) { + val file = File(path) + try { + file.writeText(number.toString()) + Log.d("FileSave", "숫자 저장 완료: $number") + + RandomAccessFile(path, "rw").use { raf -> + raf.fd.sync() // eMMC에 확실히 기록 + raf.close() + } + } catch (e: IOException) { + Log.e("FileSave", "파일 저장 실패 : " + e.toString()) + } + } + + fun readNumberFromFile(path: String): Int? { + val file = File(path) + return try { + val text = file.readText().trim() + Log.d("FileRead", "읽은 숫자: $text") + text.toIntOrNull() + } catch (e: IOException) { + Log.e("FileRead", "파일 읽기 실패 : " + e.toString()) + null + } + } + fun fileContainsString(filePath: String, keyword: String): Boolean { val file = File(filePath) if (!file.exists()) return false diff --git a/app/src/main/res/drawable/toast_frame_rito.xml b/app/src/main/res/drawable/toast_frame_rito.xml new file mode 100644 index 0000000..eec1827 --- /dev/null +++ b/app/src/main/res/drawable/toast_frame_rito.xml @@ -0,0 +1,23 @@ + + + + + + + diff --git a/app/src/main/res/layout/activity_main.xml b/app/src/main/res/layout/activity_main.xml index 436b09e..e12b0c4 100644 --- a/app/src/main/res/layout/activity_main.xml +++ b/app/src/main/res/layout/activity_main.xml @@ -161,6 +161,8 @@ android:layout_height="64dp" android:background="@color/cardview_shadow_start_color" android:clickable="false" + android:focusable="false" + android:focusableInTouchMode="false" android:gravity="center_vertical" android:popupBackground="@color/colorSpinnerDropdown" android:spinnerMode="dropdown" /> @@ -727,7 +729,7 @@ android:layout_width="match_parent" android:layout_height="wrap_content" android:hint="Netmask" - android:inputType="text" + android:inputType="phone" android:textSize="24sp" /> diff --git a/distribution.video/baresip/lib/arm64-v8a/libbaresip.a b/distribution.video/baresip/lib/arm64-v8a/libbaresip.a index 9ac12d6b144912c10ca26a6243f5c1061a544b92..2d153f852ab1ed8ec4567298a4ea07521bd283dc 100644 GIT binary patch delta 21244 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 W4pK|%$YyVGOMQB-cRaD7ck@385zNv6