diff --git a/app/build.gradle b/app/build.gradle index a7c526e..d52af13 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -9,8 +9,8 @@ android { applicationId = 'kr.co.rito.ritosip' minSdkVersion 29 targetSdkVersion 35 - versionCode = 100 - versionName = '1.0.0' + versionCode = 104 + versionName = '1.0.4' externalNativeBuild { cmake { cFlags '-DHAVE_INTTYPES_H -lstdc++' @@ -45,6 +45,7 @@ android { // minifyEnabled false proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' + signingConfig signingConfigs.debug } } android.applicationVariants.all { variant -> diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 5daf56d..da656b9 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -23,7 +23,7 @@ android:maxSdkVersion="32" /> diff --git a/app/src/main/assets/config.static b/app/src/main/assets/config.static index 0a795dc..ea0ec0c 100644 --- a/app/src/main/assets/config.static +++ b/app/src/main/assets/config.static @@ -29,6 +29,7 @@ module vp8.so module vp9.so module av1.so module snapshot.so +module fakevideo.so module stun.so module turn.so module ice.so @@ -37,11 +38,13 @@ module dtls_srtp.so module gzrtp.so module uuid.so module vumeter.so +module webrtc_aecm.so module_app account.so module_app debug_cmd.so module_app mwi.so avcodec_h264enc h264_mediacodec video_fps 30 +mic_volume 20 evdev_device /dev/input/event0 opus_samplerate 48000 opus_stereo no diff --git a/app/src/main/cpp/baresip.c b/app/src/main/cpp/baresip.c index 36f4616..851f3e0 100644 --- a/app/src/main/cpp/baresip.c +++ b/app/src/main/cpp/baresip.c @@ -1785,6 +1785,18 @@ JNIEXPORT jint JNICALL Java_com_tutpro_baresip_plus_Api_call_1set_1video_1source return err; } +JNIEXPORT jint JNICALL Java_com_tutpro_baresip_plus_Api_call_1set_1video_1fake( + JNIEnv *env, jobject obj, jlong call) +{ + (void)env; + (void)obj; + int err; + re_thread_enter(); + err = video_set_source(call_video((struct call *)call), "fakevideo", NULL); + re_thread_leave(); + return err; +} + JNIEXPORT void JNICALL Java_com_tutpro_baresip_plus_Api_call_1destroy( JNIEnv *env, jobject obj, jlong call) { @@ -1808,6 +1820,36 @@ JNIEXPORT jint JNICALL Java_com_tutpro_baresip_plus_Api_cmd_1exec( return res; } + +JNIEXPORT jstring JNICALL Java_com_tutpro_baresip_plus_Api_set_1audio_1source(JNIEnv *env, jobject obj) +{ + (void)obj; + struct list *aucodecl = baresip_ausrcl(); + struct le *le; + char codec_buf[256]; + char *start = &(codec_buf[0]); + unsigned int left = sizeof codec_buf; + int len; + for (le = list_head(aucodecl); le != NULL; le = le->next) { + const struct ausrc *ac = le->data; + if (start == &(codec_buf[0])) + len = re_snprintf(start, left, "<%s>", ac->name); + else + len = re_snprintf(start, left, ",<%s>", ac->name); + if (len == -1) { + LOGE("failed to print codec to buffer\n"); + codec_buf[0] = '\0'; + return (*env)->NewStringUTF(env, codec_buf); + } + start = start + len; + left = left - len; + } + *start = '\0'; + return (*env)->NewStringUTF(env, codec_buf); +} + + + JNIEXPORT jstring JNICALL Java_com_tutpro_baresip_plus_Api_audio_1codecs(JNIEnv *env, jobject obj) { (void)obj; diff --git a/app/src/main/kotlin/com/tutpro/baresip/plus/Api.kt b/app/src/main/kotlin/com/tutpro/baresip/plus/Api.kt index 0a64fe6..0ba903b 100644 --- a/app/src/main/kotlin/com/tutpro/baresip/plus/Api.kt +++ b/app/src/main/kotlin/com/tutpro/baresip/plus/Api.kt @@ -98,6 +98,7 @@ object Api { external fun call_state(callp: Long): Int external fun call_has_video(callp: Long): Boolean external fun call_set_video_source(callp: Long, front: Boolean): Int + external fun call_set_video_fake(callp: Long): Int // added by ritoseo external fun call_set_video_direction(callp: Long, dir: Int) external fun call_set_video_mute(callp: Long, mute: Boolean) // added by ritoseo external fun call_set_media_direction(callp: Long, adir: Int, vdir: Int) @@ -113,6 +114,7 @@ object Api { external fun message_send(uap: Long, peer_uri: String, message: String, time: String): Int + external fun set_audio_source(): String external fun audio_codecs(): String external fun video_codecs(): String 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 89c989f..25561b9 100644 --- a/app/src/main/kotlin/com/tutpro/baresip/plus/BaresipService.kt +++ b/app/src/main/kotlin/com/tutpro/baresip/plus/BaresipService.kt @@ -35,14 +35,12 @@ import android.telecom.TelecomManager import android.text.Spannable import android.text.SpannableString import android.text.style.ForegroundColorSpan -import android.util.JsonReader import android.util.Size import android.view.LayoutInflater import android.view.View import android.widget.RemoteViews import android.widget.TextView import android.widget.Toast -import androidx.activity.result.contract.ActivityResultContracts import androidx.annotation.ColorRes import androidx.annotation.Keep import androidx.annotation.StringRes @@ -63,11 +61,8 @@ import org.json.JSONObject import java.io.File import java.io.IOException import java.net.InetAddress -import java.nio.ByteBuffer import java.nio.charset.Charset import java.nio.charset.StandardCharsets -import java.time.Clock -import java.time.LocalDate import java.time.LocalDateTime import java.util.* import kotlin.concurrent.schedule @@ -269,7 +264,6 @@ class BaresipService: Service() { } } - sipReqeustReceiver = object : BroadcastReceiver() { fun sendContactList() { val resArr : JSONArray = JSONArray() @@ -570,11 +564,17 @@ class BaresipService: Service() { if(param != null) { val json = JSONObject(param) val device_name = json.getString("device_name") + val mic_volume = json.getString("mic_volume") val display_type = json.getString("display_type") Config.replaceVariable("device_name", device_name) BaresipService.deviceName = device_name + val mic_volume_int = mic_volume.toInt() + if(mic_volume_int >= 0 && mic_volume_int <= 24) { + Utils.setMicVolumeByMix(mic_volume_int) + } + Config.replaceVariable("far_view_display_id", display_type) BaresipService.farViewDisplayId = display_type.toInt() Config.save() @@ -759,14 +759,28 @@ class BaresipService: Service() { } val scope = CoroutineScope(Dispatchers.Default) - var captureCount = 0 + var captureCount : Long = 0 var running = true val job = scope.launch { println("camera capture manager is running...") Utils.deleteFiles("/mnt/obb", "jpg") var updateDate = LocalDateTime.now() + var cameraState = -1 while (running) { + +// var devices = am.getDevices(AudioManager.GET_DEVICES_INPUTS) +// for (device in devices) { +// var typeName = "모름 ${device.type}" +// if(device.type == AudioDeviceInfo.TYPE_USB_HEADSET) { +// typeName = "USB헤드셋(${device.id})" +// } else if(device.type == AudioDeviceInfo.TYPE_BUILTIN_MIC) { +// typeName = "내장마이크(${device.id})" +// } +// println("[입력장치] ${typeName} : ${device.productName}") +// } + updateAudioSourceDevice(applicationContext) + if(File("/mnt/obb/camera_near.rgb565.done").exists() && !File("/mnt/obb/camera_near.jpg").exists()) { try { val nearBuf = Utils.readFileToByteBuffer("/mnt/obb/camera_near.rgb565") @@ -970,25 +984,88 @@ class BaresipService: Service() { val camera1 = Utils.checkCameraConnection(0) val camera2 = Utils.checkCameraConnection(1) + val prevCamera1 = Utils.propertyGet("sys.ritosip.camera1.status") Utils.propertySet("sys.ritosip.camera1.status", camera1) //call.setVideoSource(!BaresipService.cameraFront) val prevCamera2 = Utils.propertyGet("sys.ritosip.camera2.status") Utils.propertySet("sys.ritosip.camera2.status", camera2) - if(camera2 != prevCamera2) { + if(camera1 != "plugin" && camera2 != "plugin" && cameraState != 0) { if(uas.size > 0) { val ua = uas[0] val call = ua.currentCall() if(call != null) { - if(camera2 == "plugin") { - BaresipService.cameraFront = false; // Contents Input - call.setVideoSource(false) - } else { - BaresipService.cameraFront = true; // Camera Input - call.setVideoSource(true) - } + call.setVideoFake() + cameraState = 0 } } + } else if(camera1 == "plugin" && camera2 != "plugin" && (cameraState == 0 || cameraState == 2)) { + if(uas.size > 0) { + val ua = uas[0] + val call = ua.currentCall() + + if(call != null) { + Log.d(TAG, "RITO camera1 detect!") + //val process = Runtime.getRuntime().exec("ritosysc shell-order=killall android.hardware.camera.provider@2.4-service android.hardware.camera.provider@2.4-external-service cameraserver android.hardware.tv.input@1.0-service") + if(cameraState == 0) { + val process = Runtime.getRuntime() + .exec("ritosysc shell-order=killall cameraserver") + process.waitFor() + process.destroy() + delay(1500) // 1초마다 실행 + } + BaresipService.cameraFront = true; + call.setVideoSource(true) + cameraState = 1 + //delay(3000) // 1초마다 실행 + } + } + } else if(camera2 == "plugin" && cameraState != 2) { + if(uas.size > 0) { + val ua = uas[0] + val call = ua.currentCall() + + if(call != null) { + Log.d(TAG, "RITO camera2 detect!") + //val process = Runtime.getRuntime().exec("ritosysc shell-order=killall android.hardware.camera.provider@2.4-service android.hardware.camera.provider@2.4-external-service cameraserver android.hardware.tv.input@1.0-service") +// val process = Runtime.getRuntime().exec("ritosysc shell-order=killall cameraserver") +// process.waitFor() +// process.destroy() +// delay(1500) // 1초마다 실행 + BaresipService.cameraFront = false; + call.setVideoSource(false) + cameraState = 2 + //delay(3000) // 1초마다 실행 + } + } + } +// else if(camera2 != prevCamera2) { +// if(uas.size > 0) { +// val ua = uas[0] +// val call = ua.currentCall() +// +// if(call != null) { +// cameraState = 2 +// if(camera2 == "plugin") { +// BaresipService.cameraFront = false; // Contents Input +// call.setVideoSource(false) +// } else { +// BaresipService.cameraFront = true; // Camera Input +// call.setVideoSource(true) +// } +// } +// } +// } + + if(uas.size > 0) { + val ua = uas[0] + val call = ua.currentCall() + + if(call == null) { + cameraState = -1 + } + } else { + cameraState = -1 } if(BaresipService.cameraFront) { @@ -1000,15 +1077,17 @@ class BaresipService: Service() { val usbMicExist = Utils.checkUsbMicrophone() if(usbMicExist) { Utils.propertySet("sys.ritosip.usb.microphone", "connected") - if(audioInputDevice == "usb") { - Utils.propertySet("sys.rito.audio.input.device", "usb") - } else { - Utils.propertySet("sys.rito.audio.input.device", "codec") - } +// if(audioInputDevice == "usb") { +// Utils.propertySet("sys.rito.audio.input.device", "usb") +// } else { +// Utils.propertySet("sys.rito.audio.input.device", "codec") +// } } else { Utils.propertySet("sys.ritosip.usb.microphone", "disconnected") } + Utils.setAudioSourceDevice(audioInputDevice) + delay(100) // 1초마다 실행 val now = LocalDateTime.now() @@ -1051,7 +1130,7 @@ class BaresipService: Service() { if (farFilePath.length > 0) Utils.propertySet("sys.ritosip.far_screen_file", farFilePath) } else { - Utils.propertySet("sys.ritosip.near_screen_file", "") + //Utils.propertySet("sys.ritosip.near_screen_file", "") Utils.propertySet("sys.ritosip.far_screen_file", "") } } @@ -1068,6 +1147,54 @@ class BaresipService: Service() { Utils.propertySet("sys.ritosip.display_split_mode", BaresipService.displaySplitMode.toString()) Utils.propertySet("sys.ritosip.sip.total_duration", BaresipService.totalDurationSeconds.toString()) + // Call 중이 아닌 경우 Local Camera 화면 캡쳐 + val camera1 = Utils.checkCameraConnection(0) + if(cameraState == -1) { + if(camera1 == "plugin") { + //Utils.runShellOrderSuper("killall -9 v4l2-ctl") + Utils.runShellOrderSuper("timeout 2s v4l2-ctl --device=/dev/video20 --stream-mmap=3 --stream-to=/mnt/obb/camera.rgb888 --stream-count=1 && chmod 666 /mnt/obb/camera.rgb888") + + try { + val nearBuf = Utils.readFileToByteBuffer("/mnt/obb/camera.rgb888") + var bufSize = nearBuf.capacity() + var width : Int = 1280 + var height : Int = 720 + if(bufSize == 1280 * 720 * 3) { + width = 1280 + height = 720 + } else if(bufSize == 1920 * 1080 * 3) { + width = 1920 + height = 1080 + } + + if(bufSize > 0) { + var nearFilePath = "/mnt/obb/near_$captureCount.jpg" + Utils.saveRGB888AsJPG( + applicationContext, + nearBuf, + width, + height, + nearFilePath + ) + + val destFile = File(nearFilePath) + destFile.setReadable(true, false) + Utils.propertySet("sys.ritosip.near_screen_file", nearFilePath) + } + } catch(e : Exception) { + //e.printStackTrace() + } + } else { + var nearFilePath = "/mnt/obb/near_$captureCount.jpg" + Utils.saveDrawableAsJpg(applicationContext, R.drawable.nocamera, nearFilePath) + + val destFile = File(nearFilePath) + destFile.setReadable(true, false) + Utils.propertySet("sys.ritosip.near_screen_file", nearFilePath) + } + + } + captureCount++ delay(100) @@ -1161,7 +1288,7 @@ class BaresipService: Service() { CallHistoryNew.save() } - val value = Utils.readNumberFromFile("/sdcard/Documents/sip_total_duration") + val value = Utils.readNumberFromFile("/sdcard/Documents/sip_total_duration.txt") if(value != null) { BaresipService.totalDurationSeconds = value } @@ -1225,6 +1352,8 @@ class BaresipService: Service() { applyTransportConfiguration() + Utils.setMicVolumeByMix(BaresipService.micVolume) + } "Start Content Observer" -> { @@ -1826,7 +1955,9 @@ class BaresipService: Service() { /* Added by ritoseo */ val duration = call.duration() BaresipService.totalDurationSeconds += duration - Utils.saveNumberToFile("/sdcard/Documents/sip_total_duration", BaresipService.totalDurationSeconds) + Utils.saveNumberToFile("/sdcard/Documents/sip_total_duration.txt", BaresipService.totalDurationSeconds) + //Utils.saveNumberToFile(File(Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOCUMENTS), "sip_total_duration").absolutePath, BaresipService.totalDurationSeconds) + /* ---------------- */ if (!Utils.isVisible()) { @@ -2267,11 +2398,15 @@ class BaresipService: Service() { } private fun setCallVolume() { + println("Call volume setting!") + callVolume = 0 if (callVolume != 0) for (streamType in listOf(AudioManager.STREAM_MUSIC, AudioManager.STREAM_VOICE_CALL)) { origVolume[streamType] = am.getStreamVolume(streamType) val maxVolume = am.getStreamMaxVolume(streamType) am.setStreamVolume(streamType, (callVolume * 0.1 * maxVolume).roundToInt(), 0) + println("Orig/new/max $streamType volume is " + + "${origVolume[streamType]}/${am.getStreamVolume(streamType)}/$maxVolume") Log.d(TAG, "Orig/new/max $streamType volume is " + "${origVolume[streamType]}/${am.getStreamVolume(streamType)}/$maxVolume") } @@ -2548,6 +2683,26 @@ class BaresipService: Service() { var nearViewDisplayId = 2 var farViewLayout : Rect = Rect(0, 0, 1920, 1080) var nearViewLayout : Rect = Rect(0, 0, 1920, 1080) + var audioDeviceUsbId = -1 + var audioDeviceCodecId = -1 + var micVolume = 20 + + fun updateAudioSourceDevice(ctx: Context) { + val am = ctx.getSystemService(Context.AUDIO_SERVICE) as AudioManager + var devices = am.getDevices(AudioManager.GET_DEVICES_INPUTS) + var usbId = -1 + var codecId = -1 + for (device in devices) { + if(device.type == AudioDeviceInfo.TYPE_USB_HEADSET) { + usbId = device.id + } else if(device.type == AudioDeviceInfo.TYPE_BUILTIN_MIC) { + codecId = device.id + } + } + + audioDeviceUsbId = usbId + audioDeviceCodecId = codecId + } fun requestAudioFocus(ctx: Context): Boolean { Log.d(TAG, "Requesting audio focus") diff --git a/app/src/main/kotlin/com/tutpro/baresip/plus/Call.kt b/app/src/main/kotlin/com/tutpro/baresip/plus/Call.kt index 260bf24..b36b1df 100644 --- a/app/src/main/kotlin/com/tutpro/baresip/plus/Call.kt +++ b/app/src/main/kotlin/com/tutpro/baresip/plus/Call.kt @@ -46,6 +46,10 @@ class Call(val callp: Long, val ua: UserAgent, val peerUri: String, val dir: Str return Api.call_set_video_source(callp, front) } + fun setVideoFake(): Int { + return Api.call_set_video_fake(callp) + } + fun hold(): Boolean { return Api.call_hold(callp, true) == 0 } 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 0466c4c..9d1e06e 100644 --- a/app/src/main/kotlin/com/tutpro/baresip/plus/Config.kt +++ b/app/src/main/kotlin/com/tutpro/baresip/plus/Config.kt @@ -211,6 +211,14 @@ object Config { */ /* Added by ritoseo */ + val micVolume = previousVariable("mic_volume") + if (micVolume != "") { + config = "${config}mic_volume $micVolume\n" + BaresipService.micVolume = micVolume.toInt() + } else { + config = "${config}mic_volume ${BaresipService.micVolume}\n" + } + for (size in defaultSizes) videoSizes.add(size.toString()) /********************/ @@ -225,12 +233,15 @@ object Config { Size(videoSize.substringBefore("x").toInt(), videoSize.substringAfter("x").toInt()) } + + BaresipService.videoSize = Size(1280, 720) + //BaresipService.videoSize = Size(640, 480) config = "${config}video_size " + "${BaresipService.videoSize.width}x${BaresipService.videoSize.height}\n" /* Added by ritoseo */ - config = "${config}video_bitrate 60000000\n" + config = "${config}video_bitrate 100000000\n" config = "${config}rtp_video_tos 184\n" config = "${config}rtp_bandwidth 512-8192\n" /********************/ 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 9c05c62..bef9d4b 100644 --- a/app/src/main/kotlin/com/tutpro/baresip/plus/MainActivity.kt +++ b/app/src/main/kotlin/com/tutpro/baresip/plus/MainActivity.kt @@ -11,6 +11,7 @@ import android.content.* import android.content.Intent.ACTION_CALL import android.content.Intent.ACTION_DIAL import android.content.Intent.ACTION_VIEW +import android.content.pm.PackageInfo import android.content.pm.PackageManager import android.content.res.Configuration import android.graphics.Rect @@ -19,10 +20,8 @@ import android.media.AudioManager import android.media.MediaActionSound import android.net.Uri import android.os.* -import android.os.StrictMode.VmPolicy import android.provider.DocumentsContract import android.provider.MediaStore -import android.provider.MediaStore.Audio.Radio import android.text.InputType import android.text.TextWatcher import android.util.TypedValue @@ -48,14 +47,18 @@ import androidx.lifecycle.Observer import androidx.swiperefreshlayout.widget.SwipeRefreshLayout import com.google.android.material.dialog.MaterialAlertDialogBuilder import com.google.android.material.snackbar.Snackbar +import com.tutpro.baresip.plus.Utils.getAppVersion import com.tutpro.baresip.plus.Utils.showSnackBar import com.tutpro.baresip.plus.databinding.ActivityMainBinding +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch +import org.json.JSONObject import java.io.File import java.text.SimpleDateFormat import java.time.LocalDateTime import java.time.format.DateTimeFormatter import java.util.* -import kotlin.collections.HashMap import kotlin.system.exitProcess @@ -138,6 +141,11 @@ class MainActivity : AppCompatActivity() { private lateinit var callStartButton: AppCompatButton private lateinit var callHistoryButton: AppCompatButton private lateinit var settingButton: AppCompatButton + private lateinit var layoutButton: AppCompatButton + private lateinit var serverButton: AppCompatButton + private lateinit var volumeButton: AppCompatButton + private lateinit var networkButton: AppCompatButton + private lateinit var backwardButton: AppCompatButton private var callHandler: Handler = Handler(Looper.getMainLooper()) private var callRunnable: Runnable? = null @@ -191,6 +199,9 @@ class MainActivity : AppCompatActivity() { lateinit var navUpList : HashMap lateinit var navDownList : HashMap + lateinit var navUpServerList : HashMap + lateinit var navDownServerList : HashMap + override fun dispatchKeyEvent(event: KeyEvent): Boolean { if(!isKeyboardVisible) { val currentFocusView = currentFocus @@ -214,6 +225,24 @@ class MainActivity : AppCompatActivity() { return false } } + + if(navUpServerList.contains(currentFocusView)) { + val view = navUpServerList.get(currentFocusView)!! + if(view.isVisible) { + view.requestFocus() + return false + } else { + if(currentFocusView == binding.btnApply) { + binding.radioDhcp.requestFocus() + return false + } + } + } else { + if(currentFocusView == binding.aorSpinner) { + callStartButton.requestFocus() + return false + } + } } else if(event.keyCode == KeyEvent.KEYCODE_DPAD_DOWN) { if(navDownList.contains(currentFocusView)) { @@ -223,12 +252,22 @@ class MainActivity : AppCompatActivity() { return false } } - + if(navDownServerList.contains(currentFocusView)) { + val view = navDownServerList.get(currentFocusView)!! + if(view.isVisible) { + view.requestFocus() + return false + } + } } else if(event.keyCode == KeyEvent.KEYCODE_ENTER) { if(currentFocusView == binding.editIp || currentFocusView == binding.editGateway || currentFocusView == binding.editNetmask || - currentFocusView == binding.editDns) { + currentFocusView == binding.editDns || + currentFocusView == binding.editDisplayName || + currentFocusView == binding.editSipId || + currentFocusView == binding.editSipPassword + ) { if(imm.isActive) { imm.showSoftInput(currentFocusView, InputMethodManager.SHOW_IMPLICIT) imm.restartInput(currentFocusView) @@ -242,6 +281,9 @@ class MainActivity : AppCompatActivity() { if(navUpList.contains(currentFocusView) || navDownList.contains(currentFocusView)) { found = true } + if(navUpServerList.contains(currentFocusView) || navDownServerList.contains(currentFocusView)) { + found = true + } if(found) { return true @@ -253,6 +295,12 @@ class MainActivity : AppCompatActivity() { currentFocusView == binding.editDns) { return true } + + if(currentFocusView == binding.editDisplayName || + currentFocusView == binding.editSipId || + currentFocusView == binding.editSipPassword) { + return true + } } } } else { @@ -389,6 +437,8 @@ class MainActivity : AppCompatActivity() { setupKeyboardVisibilityListener() + Utils.propertySet("sys.ritosip.version", getAppVersion(this)) + // Must be done after view has been created this.setShowWhenLocked(true) this.setTurnScreenOn( true) @@ -453,10 +503,213 @@ class MainActivity : AppCompatActivity() { // binding.baseButtonLayout.visibility = View.INVISIBLE // binding.mainActivityLayout.visibility = View.INVISIBLE // binding.defaultLayout.visibility = View.INVISIBLE + +// binding.networkSettingLayout.visibility = View.VISIBLE +// binding.radioDhcp.requestFocus() + + binding.settingLayout.visibility = View.VISIBLE + binding.dialogButtonLayout.requestFocus() + } + + networkButton = binding.dialogButtonNetwork + networkButton.setOnClickListener { + binding.networkSettingLayout.bringToFront() binding.networkSettingLayout.visibility = View.VISIBLE binding.radioDhcp.requestFocus() } + layoutButton = binding.dialogButtonLayout + layoutButton.setOnClickListener { + binding.layoutSettingLayout.bringToFront() + binding.layoutSettingLayout.visibility = View.VISIBLE + var splitMode = Utils.propertyGet("sys.ritosip.display_split_mode") + if(splitMode == "2") { + binding.layoutType2.requestFocus() + } else if(splitMode == "3") { + binding.layoutType3.requestFocus() + } else if(splitMode == "4") { + binding.layoutType4.requestFocus() + } else if(splitMode == "5") { + binding.layoutType5.requestFocus() + } else if(splitMode == "6") { + binding.layoutType6.requestFocus() + } else if(splitMode == "7") { + binding.layoutType7.requestFocus() + } else if(splitMode == "8") { + binding.layoutType8.requestFocus() + } else if(splitMode == "9") { + binding.layoutType9.requestFocus() + } else if(splitMode == "10") { + binding.layoutType10.requestFocus() + } else { + binding.layoutType1.requestFocus() + } + } + + binding.layoutType1.setOnClickListener { + sendSettingByBroadcast("layout_setting", "{\"layout\":\"layout1\"}") + } + binding.layoutType2.setOnClickListener { + sendSettingByBroadcast("layout_setting", "{\"layout\":\"layout2\"}") + } + binding.layoutType3.setOnClickListener { + sendSettingByBroadcast("layout_setting", "{\"layout\":\"layout3\"}") + } + binding.layoutType4.setOnClickListener { + sendSettingByBroadcast("layout_setting", "{\"layout\":\"layout4\"}") + } + binding.layoutType5.setOnClickListener { + sendSettingByBroadcast("layout_setting", "{\"layout\":\"layout5\"}") + } + binding.layoutType6.setOnClickListener { + sendSettingByBroadcast("layout_setting", "{\"layout\":\"layout6\"}") + } + + binding.layoutType7.setOnClickListener { + sendSettingByBroadcast("layout_setting", "{\"layout\":\"layout7\"}") + } + binding.layoutType8.setOnClickListener { + sendSettingByBroadcast("layout_setting", "{\"layout\":\"layout8\"}") + } + binding.layoutType9.setOnClickListener { + sendSettingByBroadcast("layout_setting", "{\"layout\":\"layout9\"}") + } + binding.layoutType10.setOnClickListener { + sendSettingByBroadcast("layout_setting", "{\"layout\":\"layout10\"}") + } + + + binding.dialogButtonLayoutInCall.setOnClickListener { + binding.layoutSettingLayout.bringToFront() + binding.layoutSettingLayout.visibility = View.VISIBLE + var splitMode = Utils.propertyGet("sys.ritosip.display_split_mode") + if(splitMode == "2") { + binding.layoutType2.requestFocus() + } else if(splitMode == "3") { + binding.layoutType3.requestFocus() + } else if(splitMode == "4") { + binding.layoutType4.requestFocus() + } else if(splitMode == "5") { + binding.layoutType5.requestFocus() + } else if(splitMode == "6") { + binding.layoutType6.requestFocus() + } else if(splitMode == "7") { + binding.layoutType7.requestFocus() + } else if(splitMode == "8") { + binding.layoutType8.requestFocus() + } else if(splitMode == "9") { + binding.layoutType9.requestFocus() + } else if(splitMode == "10") { + binding.layoutType10.requestFocus() + } else { + binding.layoutType1.requestFocus() + } + } + + serverButton = binding.dialogButtonServer + serverButton.setOnClickListener { + binding.serverSettingLayout.bringToFront() + binding.serverSettingLayout.visibility = View.VISIBLE + binding.editDisplayName.setText(Utils.propertyGet("sys.ritosip.account.display_name")) + binding.editSipId.setText(Utils.propertyGet("sys.ritosip.account.account_name")) + binding.editSipServer.setText(Utils.propertyGet("sys.ritosip.account.server_address")) + var transport = Utils.propertyGet("sys.ritosip.account.prefer_transport") + if(transport == "tcp") { + binding.radioTransportTcp.isChecked = true + } else if(transport == "tls") { + binding.radioTransportTls.isChecked = true + } else { + binding.radioTransportUdp.isChecked = true + } + var encryption = Utils.propertyGet("sys.ritosip.account.media_encryption") + if(encryption == "srtp") { + binding.radioEncryptSrtp.isChecked = true + } else { + binding.radioEncryptNone.isChecked = true + } + binding.editDisplayName.requestFocus() + } + + volumeButton = binding.dialogButtonVolume + volumeButton.setOnClickListener { + binding.volumeSettingLayout.bringToFront() + binding.volumeSettingLayout.visibility = View.VISIBLE + binding.seekbarMicVolume.requestFocus() + + binding.seekbarMicVolume.progress = BaresipService.micVolume + val currentValue = binding.seekbarMicVolume.progress + binding.textMicVolume.setText(currentValue.toString()) + } + + binding.dialogButtonVolumeInCall.setOnClickListener { + binding.volumeSettingLayout.bringToFront() + binding.volumeSettingLayout.visibility = View.VISIBLE + binding.seekbarMicVolume.requestFocus() + + binding.seekbarMicVolume.progress = BaresipService.micVolume + val currentValue = binding.seekbarMicVolume.progress + binding.textMicVolume.setText(currentValue.toString()) + } + + binding.seekbarMicVolume.setOnSeekBarChangeListener(object : SeekBar.OnSeekBarChangeListener{ + override fun onProgressChanged(seekBar: SeekBar?, progress: Int, fromUser: Boolean) { + // progress: 현재 값 + // fromUser: 사용자가 직접 움직였는지 여부 + val currentValue = progress + println("현재 값: $currentValue") + binding.textMicVolume.setText(currentValue.toString()) + Utils.setMicVolumeByMix(currentValue) + } + + override fun onStartTrackingTouch(seekBar: SeekBar?) { + // 터치 시작 시 호출 + + } + + override fun onStopTrackingTouch(seekBar: SeekBar?) { + // 터치 끝날 때 호출 + } + }) + + binding.btnDoneVolume.setOnClickListener { + binding.volumeSettingLayout.visibility = View.INVISIBLE + if(binding.settingLayout.visibility == View.VISIBLE) { + binding.dialogButtonVolume.requestFocus() + } else if(binding.settingLayoutInCall.visibility == View.VISIBLE) { + binding.dialogButtonVolumeInCall.requestFocus() + } + } + + backwardButton = binding.dialogButtonBackward + backwardButton.setOnClickListener { + if(binding.settingLayout.visibility == View.VISIBLE) { + binding.settingLayout.visibility = View.INVISIBLE + binding.settingBtn.requestFocus() + } + } + + binding.dialogButtonHangUp.setOnClickListener { + if(binding.settingLayoutInCall.visibility == View.VISIBLE) { + hangupButton.performClick() + binding.settingLayoutInCall.visibility = View.INVISIBLE + } + } + + binding.dialogButtonBackwardInCall.setOnClickListener { + if(binding.settingLayoutInCall.visibility == View.VISIBLE) { + binding.settingLayoutInCall.visibility = View.INVISIBLE + } + } + + binding.layoutBackward.setOnClickListener { + binding.layoutSettingLayout.visibility = View.INVISIBLE + if(binding.settingLayout.visibility == View.VISIBLE) { + binding.dialogButtonLayout.requestFocus() + } else if(binding.settingLayoutInCall.visibility == View.VISIBLE) { + binding.dialogButtonLayoutInCall.requestFocus() + } + } + BaresipService.supportedCameras = Utils.supportedCameras(applicationContext).isNotEmpty() @@ -530,12 +783,87 @@ class MainActivity : AppCompatActivity() { showCustomToast(applicationContext, "STATIC 적용되었습니다.") binding.networkSettingLayout.visibility = View.INVISIBLE } + binding.dialogButtonNetwork.requestFocus() } + binding.btnCancel.setOnClickListener { _ -> binding.networkSettingLayout.visibility = View.INVISIBLE + binding.dialogButtonNetwork.requestFocus() } + + binding.btnApplyServer.setOnClickListener { _ -> + showCustomToast(applicationContext, "적용되었습니다.") + var param : JSONObject = JSONObject() + param.put("display_name", binding.editDisplayName.text) + param.put("account_name", binding.editSipId.text) + var password = binding.editSipPassword.text.toString() + if(password.isEmpty()) { + if (File(filesDir.absolutePath + "/accounts").exists()) { + val accounts = String( + Utils.getFileContents(filesDir.absolutePath + "/accounts")!!, + Charsets.UTF_8 + ).lines().toMutableList() + + password = getPassword(accounts) + } + } + param.put("password", password) + param.put("server_address", binding.editSipServer.text) + var transport = "udp" + if(binding.radioTransportTcp.isChecked) { + transport = "tcp" + } else if(binding.radioTransportTls.isChecked) { + transport = "tls" + } + param.put("transport", transport) + var encrypt = "none" + if(binding.radioEncryptSrtp.isChecked) { + encrypt = "srtpo" + } + param.put("media_encryption", encrypt) + println("PARAM >> " + param.toString()) + sendSettingByBroadcast("set_account", param.toString()) + binding.serverSettingLayout.visibility = View.INVISIBLE + binding.dialogButtonServer.requestFocus() + } + + binding.btnCancelServer.setOnClickListener { _ -> + binding.serverSettingLayout.visibility = View.INVISIBLE + binding.dialogButtonServer.requestFocus() + } + + navUpServerList = HashMap() + navDownServerList = HashMap() + + navUpServerList.put(binding.editDisplayName, binding.btnApplyServer) + navUpServerList.put(binding.editSipId, binding.editDisplayName) + navUpServerList.put(binding.editSipPassword, binding.editSipId) + navUpServerList.put(binding.editSipServer, binding.editSipPassword) + navUpServerList.put(binding.radioTransportUdp, binding.editSipServer) + navUpServerList.put(binding.radioTransportTcp, binding.editSipServer) + navUpServerList.put(binding.radioTransportTls, binding.editSipServer) + navUpServerList.put(binding.radioEncryptNone, binding.radioTransportUdp) + navUpServerList.put(binding.radioEncryptSrtp, binding.radioTransportUdp) + navUpServerList.put(binding.btnApplyServer, binding.radioEncryptNone) + navUpServerList.put(binding.btnCancelServer, binding.radioEncryptNone) + //navUpList.put(binding.btnApply, binding.radioDhcp) + + navDownServerList.put(binding.editDisplayName, binding.editSipId) + navDownServerList.put(binding.editSipId, binding.editSipPassword) + navDownServerList.put(binding.editSipPassword, binding.editSipServer) + navDownServerList.put(binding.editSipServer, binding.radioTransportUdp) + navDownServerList.put(binding.radioTransportUdp, binding.radioEncryptNone) + navDownServerList.put(binding.radioTransportTcp, binding.radioEncryptNone) + navDownServerList.put(binding.radioTransportTls, binding.radioEncryptNone) + navDownServerList.put(binding.radioEncryptNone, binding.btnApplyServer) + navDownServerList.put(binding.radioEncryptSrtp, binding.btnApplyServer) + navDownServerList.put(binding.btnApplyServer, binding.editDisplayName) + navDownServerList.put(binding.btnCancelServer, binding.editDisplayName) + + + updateFieldsVisibility() val radioGroup = findViewById(R.id.radioGroup) @@ -1179,11 +1507,12 @@ class MainActivity : AppCompatActivity() { else getString(R.string.audio_permissions) ) { requestPermissionsLauncher.launch(permissions) } - else - startBaresip() +// else +// startBaresip() } } + startBaresip() addVideoLayoutViews() if (!BaresipService.isServiceRunning) { @@ -1192,7 +1521,7 @@ class MainActivity : AppCompatActivity() { Utils.getFileContents(filesDir.absolutePath + "/accounts")!!, Charsets.UTF_8 ).lines().toMutableList() - askPasswords(accounts) + //askPasswords(accounts) } else { // Baresip is started for the first time requestPermissionsLauncher.launch(permissions) @@ -1201,6 +1530,14 @@ class MainActivity : AppCompatActivity() { } // OnCreate + fun sendSettingByBroadcast(req : String, param : String) { + val responseIntent = Intent("kr.co.rito.ritosip.REQUEST") + responseIntent.putExtra("request", req) + responseIntent.putExtra("param", param) + responseIntent.setPackage("kr.co.rito.ritosip"); + sendBroadcast(responseIntent) + } + override fun onStart() { super.onStart() Log.d(TAG, "Main onStart") @@ -2055,6 +2392,7 @@ class MainActivity : AppCompatActivity() { hangupButton.performClick() } videoLayout.addView(hb) + hb.visibility = View.INVISIBLE // by ritoseo // Info Button val ib = ImageButton(this) @@ -2354,13 +2692,50 @@ class MainActivity : AppCompatActivity() { } when (event?.scanCode) { SCANCODE_OPTION -> { - val dialog = findViewById(R.id.dialogLayout) - if(dialog.isVisible) { - dialog.visibility = View.INVISIBLE +// val dialog = findViewById(R.id.dialogLayout) +// if(dialog.isVisible) { +// dialog.visibility = View.INVISIBLE +// } else { +// dialog.visibility = View.VISIBLE +// findViewById(R.id.dialogButtonImg1).requestFocus() +// } + +// val dialog = findViewById(R.id.settingLayout) +// if (dialog.isVisible) { +// dialog.visibility = View.INVISIBLE +// } else { +// dialog.bringToFront() +// dialog.visibility = View.VISIBLE +// findViewById(R.id.dialogButtonLayout).requestFocus() +// } + if(binding.layoutSettingLayout.visibility == View.VISIBLE || + binding.networkSettingLayout.visibility == View.VISIBLE || + binding.serverSettingLayout.visibility == View.VISIBLE || + binding.volumeSettingLayout.visibility == View.VISIBLE) + return true + + if (Call.inCall()) { + val dialog = findViewById(R.id.settingLayoutInCall) + if (dialog.isVisible) { + dialog.visibility = View.INVISIBLE + println("settingLayoutInCall 비활성화") + } else { + dialog.bringToFront() + dialog.visibility = View.VISIBLE + findViewById(R.id.dialogButtonLayoutInCall).requestFocus() + println("settingLayoutInCall 활성화") + } } else { - dialog.visibility = View.VISIBLE - findViewById(R.id.dialogButtonImg1).requestFocus() + val dialog = findViewById(R.id.settingLayout) + if (dialog.isVisible) { + dialog.visibility = View.INVISIBLE + } else { + dialog.bringToFront() + dialog.visibility = View.VISIBLE + findViewById(R.id.dialogButtonLayout).requestFocus() + } } + // val dialog = findViewById(R.id.dialogBase) // val customButton = LayoutInflater.from(this).inflate(R.layout.image_button, null) // dialog.addView(customButton) @@ -3097,6 +3472,19 @@ class MainActivity : AppCompatActivity() { } } + private fun getPassword(accounts: MutableList) : String { + if (accounts.isNotEmpty()) { + val account = accounts.removeAt(0) + val params = account.substringAfter(">") + if ((Utils.paramValue(params, "auth_user") != "") && + (Utils.paramValue(params, "auth_pass") != "")) { + return Utils.paramValue(params, "auth_pass").trim('"') + } + } + + return "" + } + private fun askPasswords(accounts: MutableList) { if (accounts.isNotEmpty()) { val account = accounts.removeAt(0) @@ -3315,8 +3703,8 @@ class MainActivity : AppCompatActivity() { callVideoButton.visibility = View.INVISIBLE callVideoButton.isEnabled = false switchVideoLayout(true) - hangupButton.visibility = View.VISIBLE - hangupButton.isEnabled = true + hangupButton.visibility = View.INVISIBLE + hangupButton.isEnabled = false if (Build.VERSION.SDK_INT < 31) { Log.d(TAG, "Setting audio mode to MODE_IN_COMMUNICATION") am.mode = AudioManager.MODE_IN_COMMUNICATION @@ -3470,7 +3858,10 @@ class MainActivity : AppCompatActivity() { //callVideoButton.visibility = View.VISIBLE callVideoButton.visibility = View.INVISIBLE // modified by ritoseo callVideoButton.isEnabled = true + + binding.callBackground.visibility = View.GONE hangupButton.visibility = View.INVISIBLE + binding.callBackground.visibility = View.GONE answerButton.visibility = View.INVISIBLE answerVideoButton.visibility = View.INVISIBLE rejectButton.visibility = View.INVISIBLE @@ -3501,8 +3892,10 @@ class MainActivity : AppCompatActivity() { securityButton.visibility = View.INVISIBLE diverter.visibility = View.GONE callButton.visibility = View.INVISIBLE - hangupButton.visibility = View.VISIBLE - hangupButton.isEnabled = true + + binding.callBackground.visibility = View.VISIBLE + hangupButton.visibility = View.INVISIBLE + hangupButton.isEnabled = false answerButton.visibility = View.INVISIBLE answerVideoButton.visibility = View.INVISIBLE rejectButton.visibility = View.INVISIBLE @@ -3528,6 +3921,8 @@ class MainActivity : AppCompatActivity() { callButton.visibility = View.INVISIBLE callVideoButton.visibility = View.INVISIBLE switchVideoLayout(true) + + binding.callBackground.visibility = View.GONE hangupButton.visibility = View.INVISIBLE answerButton.visibility = View.VISIBLE answerButton.isEnabled = true @@ -3603,8 +3998,9 @@ class MainActivity : AppCompatActivity() { switchVideoLayout(true) - hangupButton.visibility = View.VISIBLE - hangupButton.isEnabled = true + binding.callBackground.visibility = View.VISIBLE + hangupButton.visibility = View.INVISIBLE + hangupButton.isEnabled = false answerButton.visibility = View.INVISIBLE answerVideoButton.visibility = View.INVISIBLE rejectButton.visibility = View.INVISIBLE 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 31e1da0..64ac2ef 100644 --- a/app/src/main/kotlin/com/tutpro/baresip/plus/Utils.kt +++ b/app/src/main/kotlin/com/tutpro/baresip/plus/Utils.kt @@ -14,6 +14,8 @@ import android.graphics.Bitmap.createScaledBitmap import android.graphics.BitmapFactory import android.graphics.Canvas import android.graphics.Color +import android.graphics.drawable.BitmapDrawable +import android.graphics.drawable.Drawable import android.hardware.camera2.CameraAccessException import android.hardware.camera2.CameraCharacteristics import android.hardware.camera2.CameraManager @@ -49,6 +51,9 @@ import androidx.lifecycle.Lifecycle import androidx.lifecycle.ProcessLifecycleOwner import com.google.android.material.dialog.MaterialAlertDialogBuilder import com.google.android.material.snackbar.Snackbar +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch import java.io.* import java.lang.reflect.Method import java.net.Inet4Address @@ -1175,6 +1180,62 @@ object Utils { } } + fun rgb888ToBitmap(rgbData: ByteArray, width: Int, height: Int): Bitmap? { + val bitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888) + val pixels = IntArray(width * height) + for (i in pixels.indices) { + val b = rgbData[i * 3].toInt() and 0xFF + val g = rgbData[i * 3 + 1].toInt() and 0xFF + val r = rgbData[i * 3 + 2].toInt() and 0xFF + pixels[i] = 0xFF shl 24 or (r shl 16) or (g shl 8) or b + } + bitmap.setPixels(pixels, 0, width, 0, 0, width, height) + return bitmap + } + fun saveRGB888AsJPG(context: Context, rgb565Data: ByteBuffer, width: Int, height: Int, fileName: String) { + // RGB565 포맷의 빈 Bitmap 생성 + val bitmap = rgb888ToBitmap(rgb565Data.array(), width, height) + if(bitmap == null) + return + + try { + // 저장할 파일 경로 설정 + val file = File(fileName) + + try { + FileOutputStream(file).use { fos -> + // Bitmap을 JPG 형식으로 저장 (품질 90%) + bitmap.compress(Bitmap.CompressFormat.JPEG, 90, fos) + fos.flush() + } + } catch (e: IOException) { + e.printStackTrace() + } + } catch(e: java.lang.Exception) { + + } + } + + fun saveDrawableAsJpg(context: Context, drawableResId: Int, filename: String): Boolean { + val drawable: Drawable? = ContextCompat.getDrawable(context, drawableResId) + if (drawable == null || drawable !is BitmapDrawable) { + return false + } + + val bitmap: Bitmap = drawable.bitmap + + val file = File(filename) + return try { + FileOutputStream(file).use { out -> + bitmap.compress(Bitmap.CompressFormat.JPEG, 90, out) + } + true + } catch (e: Exception) { + e.printStackTrace() + false + } + } + fun copyFile(sourcePath: String, destinationPath: String): Boolean { return try { FileInputStream(sourcePath).use { input -> @@ -1259,7 +1320,7 @@ object Utils { } } - fun setAudioSourceDevice(dev : String) { + fun setAudioSourceDeviceV1(dev : String) { propertySet("sys.rito.audio.input.device", dev) //val process = Runtime.getRuntime().exec("ritosysc shell-order=killall audioserver") val process = Runtime.getRuntime().exec("ritosysc shell-order=killall android.hardware.audio.service") @@ -1267,6 +1328,22 @@ object Utils { process.destroy() } + fun setAudioSourceDevice(dev : String) { + propertySet("sys.ritosip.audio.input.device", dev) + if(dev == "usb") { + if(BaresipService.audioDeviceUsbId == -1) { + propertySet("sys.ritosip.audio.source", BaresipService.audioDeviceCodecId.toString()) + propertySet("sys.ritosip.audio.source.volume", "0") + } else { + propertySet("sys.ritosip.audio.source", BaresipService.audioDeviceUsbId.toString()) + propertySet("sys.ritosip.audio.source.volume", "100") + } + } else if(dev == "codec") { + propertySet("sys.ritosip.audio.source", BaresipService.audioDeviceCodecId.toString()) + propertySet("sys.ritosip.audio.source.volume", "100") + } + } + fun checkNetworkIpType() { val process = Runtime.getRuntime().exec("ritosysc shell-order=rm /mnt/obb/ip_static;grep STATIC /data/misc/ethernet/ipconfig.txt && touch /mnt/obb/ip_static") process.waitFor() @@ -1279,6 +1356,28 @@ object Utils { } } + fun runShellOrder(order : String) { + val process = Runtime.getRuntime().exec(order) + process.waitFor() + process.destroy() + } + + fun setMicVolumeByMix(volume : Int) { + CoroutineScope(Dispatchers.Default).launch { + Utils.runShellOrderSuper("tinymix -D 3 \"Mic Capture Volume\" ${volume}") + BaresipService.micVolume = volume + Config.replaceVariable("mic_volume", volume.toString()) + Config.save() + propertySet("sys.ritosip.mic.volume", volume.toString()) + } + } + + fun runShellOrderSuper(order : String) { + val process = Runtime.getRuntime().exec("ritosysc shell-order=" + order) + process.waitFor() + process.destroy() + } + fun save(dev : String) { propertySet("sys.rito.audio.input.device", dev) //val process = Runtime.getRuntime().exec("ritosysc shell-order=killall audioserver") @@ -1315,7 +1414,17 @@ object Utils { val file = File("/d/hdmirx/status") // 파일 경로 설정 file.bufferedReader().use { reader -> val line = reader.readLine() // 첫 번째 줄 읽기 - return line.split(":")[1].trim() + val plugStatus = line.split(":")[1].trim() + if(plugStatus == "plugin") { + val line2 = reader.readLine() // 두 번째 줄 읽기 + val lockStatus = line2.split(":")[1].trim().startsWith("Lock") + + if (lockStatus) { + return "plugin" + } + } + + return "plugout" } } else if(idx == 1) { val file = File("/sys/module/lt6911uxc/parameters/status") // 파일 경로 설정 @@ -1334,13 +1443,21 @@ object Utils { fun saveNumberToFile(path: String, number: Int) { val file = File(path) try { + //println("파일 존재 여부: ${file.exists()}, 디렉토리 여부: ${file.isDirectory}, 파일 여부: ${file.isFile}") + if(!file.exists()) { + file.createNewFile() + } + file.writeText(number.toString()) + Log.d("FileSave", "숫자 저장 완료: $number") RandomAccessFile(path, "rw").use { raf -> raf.fd.sync() // eMMC에 확실히 기록 raf.close() } + + file.setWritable(true, false) } catch (e: IOException) { Log.e("FileSave", "파일 저장 실패 : " + e.toString()) } diff --git a/app/src/main/res/drawable/backward.png b/app/src/main/res/drawable/backward.png new file mode 100644 index 0000000..647e450 Binary files /dev/null and b/app/src/main/res/drawable/backward.png differ diff --git a/app/src/main/res/drawable/bg_button_selector_2.xml b/app/src/main/res/drawable/bg_button_selector_2.xml new file mode 100644 index 0000000..adf6267 --- /dev/null +++ b/app/src/main/res/drawable/bg_button_selector_2.xml @@ -0,0 +1,24 @@ + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/hangup_big.xml b/app/src/main/res/drawable/hangup_big.xml new file mode 100644 index 0000000..5095ea9 --- /dev/null +++ b/app/src/main/res/drawable/hangup_big.xml @@ -0,0 +1,5 @@ + + + diff --git a/app/src/main/res/drawable/layout.png b/app/src/main/res/drawable/layout.png new file mode 100644 index 0000000..a3640cc Binary files /dev/null and b/app/src/main/res/drawable/layout.png differ diff --git a/app/src/main/res/drawable/layout1.png b/app/src/main/res/drawable/layout1.png new file mode 100644 index 0000000..7ab492a Binary files /dev/null and b/app/src/main/res/drawable/layout1.png differ diff --git a/app/src/main/res/drawable/layout10.png b/app/src/main/res/drawable/layout10.png new file mode 100644 index 0000000..406af34 Binary files /dev/null and b/app/src/main/res/drawable/layout10.png differ diff --git a/app/src/main/res/drawable/layout2.png b/app/src/main/res/drawable/layout2.png new file mode 100644 index 0000000..b1b7297 Binary files /dev/null and b/app/src/main/res/drawable/layout2.png differ diff --git a/app/src/main/res/drawable/layout3.png b/app/src/main/res/drawable/layout3.png new file mode 100644 index 0000000..c46ca07 Binary files /dev/null and b/app/src/main/res/drawable/layout3.png differ diff --git a/app/src/main/res/drawable/layout4.png b/app/src/main/res/drawable/layout4.png new file mode 100644 index 0000000..63351a6 Binary files /dev/null and b/app/src/main/res/drawable/layout4.png differ diff --git a/app/src/main/res/drawable/layout5.png b/app/src/main/res/drawable/layout5.png new file mode 100644 index 0000000..f4c3a1a Binary files /dev/null and b/app/src/main/res/drawable/layout5.png differ diff --git a/app/src/main/res/drawable/layout6.png b/app/src/main/res/drawable/layout6.png new file mode 100644 index 0000000..4f29557 Binary files /dev/null and b/app/src/main/res/drawable/layout6.png differ diff --git a/app/src/main/res/drawable/layout7.png b/app/src/main/res/drawable/layout7.png new file mode 100644 index 0000000..02c82ae Binary files /dev/null and b/app/src/main/res/drawable/layout7.png differ diff --git a/app/src/main/res/drawable/layout8.png b/app/src/main/res/drawable/layout8.png new file mode 100644 index 0000000..9586f54 Binary files /dev/null and b/app/src/main/res/drawable/layout8.png differ diff --git a/app/src/main/res/drawable/layout9.png b/app/src/main/res/drawable/layout9.png new file mode 100644 index 0000000..7515053 Binary files /dev/null and b/app/src/main/res/drawable/layout9.png differ diff --git a/app/src/main/res/drawable/network.png b/app/src/main/res/drawable/network.png new file mode 100644 index 0000000..337baa5 Binary files /dev/null and b/app/src/main/res/drawable/network.png differ diff --git a/app/src/main/res/drawable/nocamera.png b/app/src/main/res/drawable/nocamera.png new file mode 100644 index 0000000..56c77d2 Binary files /dev/null and b/app/src/main/res/drawable/nocamera.png differ diff --git a/app/src/main/res/drawable/server.png b/app/src/main/res/drawable/server.png new file mode 100644 index 0000000..bfb189e Binary files /dev/null and b/app/src/main/res/drawable/server.png differ diff --git a/app/src/main/res/drawable/volume.png b/app/src/main/res/drawable/volume.png new file mode 100644 index 0000000..a5ccbd7 Binary files /dev/null and b/app/src/main/res/drawable/volume.png differ diff --git a/app/src/main/res/layout/activity_main.xml b/app/src/main/res/layout/activity_main.xml index e12b0c4..10c9540 100644 --- a/app/src/main/res/layout/activity_main.xml +++ b/app/src/main/res/layout/activity_main.xml @@ -19,6 +19,15 @@ app:srcCompat="@drawable/osvc_splash" /> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + @@ -771,10 +1004,549 @@ android:layout_margin="16dp" android:paddingHorizontal="20dp" android:paddingVertical="10dp" + android:nextFocusLeft="@id/btnApply" + android:nextFocusRight="@id/btnCancel" android:text="취소하기" android:textSize="24sp" /> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +