Repository: juha-h/baresip-studio Branch: master Commit: 5742a1c3f727 Files: 1047 Total size: 1.5 MB Directory structure: gitextract_1vqbv6_p/ ├── .clang-format ├── .gitignore ├── .gitmodules ├── LICENSE ├── PrivacyPolicy.txt ├── README.md ├── app/ │ ├── build.gradle.kts │ ├── proguard-rules.pro │ └── src/ │ └── main/ │ ├── AndroidManifest.xml │ ├── assets/ │ │ ├── accounts │ │ ├── config.static │ │ └── contacts │ ├── cpp/ │ │ ├── CMakeLists.txt │ │ ├── baresip.c │ │ └── logger.h │ ├── kotlin/ │ │ └── com/ │ │ └── tutpro/ │ │ └── baresip/ │ │ ├── AboutScreen.kt │ │ ├── Account.kt │ │ ├── AccountScreen.kt │ │ ├── AccountViewModel.kt │ │ ├── AccountsScreen.kt │ │ ├── AndroidContactScreen.kt │ │ ├── Api.kt │ │ ├── AudioScreen.kt │ │ ├── BaresipApp.kt │ │ ├── BaresipContactScreen.kt │ │ ├── BaresipService.kt │ │ ├── Blocked.kt │ │ ├── BlockedScreen.kt │ │ ├── BootCompletedReceiver.kt │ │ ├── Call.kt │ │ ├── CallDetailsScreen.kt │ │ ├── CallHistory.kt │ │ ├── CallRow.kt │ │ ├── CallsScreen.kt │ │ ├── ChatScreen.kt │ │ ├── ChatsScreen.kt │ │ ├── Codec.kt │ │ ├── CodecsScreen.kt │ │ ├── Colors.kt │ │ ├── Config.kt │ │ ├── ConnectionService.kt │ │ ├── Constants.kt │ │ ├── Contact.kt │ │ ├── ContactsScreen.kt │ │ ├── CustomElements.kt │ │ ├── DraggableLazyList.kt │ │ ├── Event.kt │ │ ├── InCallService.kt │ │ ├── Log.kt │ │ ├── MainActivity.kt │ │ ├── MainScreen.kt │ │ ├── Message.kt │ │ ├── Preferences.kt │ │ ├── ServiceEvent.kt │ │ ├── SettingsScreen.kt │ │ ├── SettingsViewModel.kt │ │ ├── TaskReceiver.kt │ │ ├── Theme.kt │ │ ├── UserAgent.kt │ │ ├── Utils.kt │ │ └── ViewModel.kt │ └── res/ │ ├── drawable/ │ │ ├── circle_green.xml │ │ ├── circle_green_blind.xml │ │ ├── circle_red.xml │ │ ├── circle_red_blind.xml │ │ ├── circle_white.xml │ │ ├── circle_yellow.xml │ │ ├── circle_yellow_blind.xml │ │ ├── ic_launcher_foreground.xml │ │ ├── ic_notification_b.xml │ │ ├── ic_notification_call.xml │ │ ├── ic_notification_call_end.xml │ │ ├── ic_notification_call_missed.xml │ │ ├── ic_notification_delete.xml │ │ ├── ic_notification_message.xml │ │ ├── ic_notification_reply.xml │ │ └── ic_notification_save.xml │ ├── layout/ │ │ └── status_notification.xml │ ├── mipmap-anydpi/ │ │ ├── ic_launcher.xml │ │ └── ic_launcher_round.xml │ ├── values/ │ │ ├── colors.xml │ │ └── strings.xml │ ├── values-ar/ │ │ └── strings.xml │ ├── values-bg/ │ │ └── strings.xml │ ├── values-ca/ │ │ └── strings.xml │ ├── values-cs/ │ │ └── strings.xml │ ├── values-de/ │ │ └── strings.xml │ ├── values-el/ │ │ └── strings.xml │ ├── values-es/ │ │ └── strings.xml │ ├── values-fi/ │ │ └── strings.xml │ ├── values-fr/ │ │ └── strings.xml │ ├── values-hr/ │ │ └── strings.xml │ ├── values-in/ │ │ └── strings.xml │ ├── values-iw/ │ │ └── strings.xml │ ├── values-ja-rJP/ │ │ └── strings.xml │ ├── values-ko/ │ │ └── strings.xml │ ├── values-nb-rNO/ │ │ └── strings.xml │ ├── values-night/ │ │ └── colors.xml │ ├── values-pl/ │ │ └── strings.xml │ ├── values-pt/ │ │ └── strings.xml │ ├── values-pt-rBR/ │ │ └── strings.xml │ ├── values-ro/ │ │ └── strings.xml │ ├── values-ru/ │ │ └── strings.xml │ ├── values-sl/ │ │ └── strings.xml │ ├── values-sv/ │ │ └── strings.xml │ ├── values-ta/ │ │ └── strings.xml │ ├── values-uk/ │ │ └── strings.xml │ └── values-zh-rCN/ │ └── strings.xml ├── build.gradle.kts ├── fastlane/ │ └── metadata/ │ └── android/ │ ├── de-DE/ │ │ ├── changelogs/ │ │ │ ├── 10.1.0.txt │ │ │ ├── 10.2.0.txt │ │ │ └── 10.3.0.txt │ │ ├── full_description.txt │ │ ├── short_description.txt │ │ └── title.txt │ ├── el/ │ │ └── title.txt │ ├── en-US/ │ │ ├── changelogs/ │ │ │ ├── 10.0.0.txt │ │ │ ├── 10.1.0.txt │ │ │ ├── 10.2.0.txt │ │ │ ├── 10.3.0.txt │ │ │ ├── 10.4.0.txt │ │ │ ├── 11.0.0.txt │ │ │ ├── 11.1.0.txt │ │ │ ├── 11.2.0.txt │ │ │ ├── 11.3.0.txt │ │ │ ├── 12.0.0.txt │ │ │ ├── 12.1.0.txt │ │ │ ├── 12.2.0.txt │ │ │ ├── 12.2.1.txt │ │ │ ├── 13.0.0.txt │ │ │ ├── 13.0.1.txt │ │ │ ├── 14.0.0.txt │ │ │ ├── 15.0.0.txt │ │ │ ├── 15.1.0.txt │ │ │ ├── 15.1.1.txt │ │ │ ├── 16.0.0.txt │ │ │ ├── 16.0.1.txt │ │ │ ├── 16.1.0.txt │ │ │ ├── 17.0.0.txt │ │ │ ├── 17.1.0.txt │ │ │ ├── 17.2.0.txt │ │ │ ├── 17.2.1.txt │ │ │ ├── 17.2.2.txt │ │ │ ├── 17.2.3.txt │ │ │ ├── 17.3.0.txt │ │ │ ├── 17.4.0.txt │ │ │ ├── 18.0.0.txt │ │ │ ├── 18.0.1.txt │ │ │ ├── 18.1.0.txt │ │ │ ├── 18.1.1.txt │ │ │ ├── 18.1.2.txt │ │ │ ├── 18.1.3.txt │ │ │ ├── 18.2.0.txt │ │ │ ├── 18.2.1.txt │ │ │ ├── 18.3.0.txt │ │ │ ├── 19.0.0.txt │ │ │ ├── 19.1.0.txt │ │ │ ├── 20.0.0.txt │ │ │ ├── 20.0.1.txt │ │ │ ├── 20.0.2.txt │ │ │ ├── 20.1.0.txt │ │ │ ├── 21.0.0.txt │ │ │ ├── 21.1.0.txt │ │ │ ├── 21.2.0.txt │ │ │ ├── 22.0.0.txt │ │ │ ├── 22.1.0.txt │ │ │ ├── 23.0.0.txt │ │ │ ├── 23.1.0.txt │ │ │ ├── 23.2.0.txt │ │ │ ├── 24.0.0.txt │ │ │ ├── 24.1.0.txt │ │ │ ├── 24.2.0.txt │ │ │ ├── 24.3.0.txt │ │ │ ├── 24.4.0.txt │ │ │ ├── 25.0.0.txt │ │ │ ├── 26.0.0.txt │ │ │ ├── 26.0.1.txt │ │ │ ├── 26.1.0.txt │ │ │ ├── 26.1.1.txt │ │ │ ├── 26.1.2.txt │ │ │ ├── 27.0.0.txt │ │ │ ├── 27.0.1.txt │ │ │ ├── 28.0.0.txt │ │ │ ├── 28.1.0.txt │ │ │ ├── 28.1.1.txt │ │ │ ├── 28.2.0.txt │ │ │ ├── 29.0.0.txt │ │ │ ├── 29.1.0.txt │ │ │ ├── 29.2.0.txt │ │ │ ├── 29.2.1.txt │ │ │ ├── 3.2.0.txt │ │ │ ├── 3.2.2.txt │ │ │ ├── 30.0.0.txt │ │ │ ├── 30.0.1.txt │ │ │ ├── 30.1.0.txt │ │ │ ├── 30.2.0.txt │ │ │ ├── 30.3.0.txt │ │ │ ├── 31.0.0.txt │ │ │ ├── 31.1.0.txt │ │ │ ├── 31.2.0.txt │ │ │ ├── 31.2.1.txt │ │ │ ├── 32.0.0.txt │ │ │ ├── 32.1.0.txt │ │ │ ├── 32.2.0.txt │ │ │ ├── 32.3.0.txt │ │ │ ├── 33.0.0.txt │ │ │ ├── 34.0.0.txt │ │ │ ├── 34.1.0.txt │ │ │ ├── 35.0.0.txt │ │ │ ├── 35.1.0.txt │ │ │ ├── 35.2.0.txt │ │ │ ├── 36.0.0.txt │ │ │ ├── 36.1.0.txt │ │ │ ├── 36.1.1.txt │ │ │ ├── 36.1.2.txt │ │ │ ├── 36.2.0.txt │ │ │ ├── 37.0.0.txt │ │ │ ├── 37.0.1.txt │ │ │ ├── 37.1.0.txt │ │ │ ├── 37.2.0.txt │ │ │ ├── 37.3.0.txt │ │ │ ├── 37.4.0.txt │ │ │ ├── 38.0.0.txt │ │ │ ├── 39.0.0.txt │ │ │ ├── 39.1.0.txt │ │ │ ├── 4.0.0.txt │ │ │ ├── 4.1.0.txt │ │ │ ├── 4.1.1.txt │ │ │ ├── 4.1.2.txt │ │ │ ├── 40.0.0.txt │ │ │ ├── 40.0.1.txt │ │ │ ├── 40.1.0.txt │ │ │ ├── 41.0.0.txt │ │ │ ├── 42.0.0.txt │ │ │ ├── 42.1.0.txt │ │ │ ├── 42.2.0.txt │ │ │ ├── 42.3.0.txt │ │ │ ├── 43.0.0.txt │ │ │ ├── 43.0.2.txt │ │ │ ├── 43.1.0.txt │ │ │ ├── 44.0.0.txt │ │ │ ├── 44.1.0.txt │ │ │ ├── 44.2.0.txt │ │ │ ├── 446.txt │ │ │ ├── 447.txt │ │ │ ├── 448.txt │ │ │ ├── 449.txt │ │ │ ├── 45.0.0.txt │ │ │ ├── 45.1.0.txt │ │ │ ├── 45.1.1.txt │ │ │ ├── 45.1.2.txt │ │ │ ├── 450.txt │ │ │ ├── 451.txt │ │ │ ├── 452.txt │ │ │ ├── 453.txt │ │ │ ├── 454.txt │ │ │ ├── 455.txt │ │ │ ├── 456.txt │ │ │ ├── 457.txt │ │ │ ├── 458.txt │ │ │ ├── 459.txt │ │ │ ├── 46.0.0.txt │ │ │ ├── 46.0.1.txt │ │ │ ├── 46.1.0.txt │ │ │ ├── 46.1.1.txt │ │ │ ├── 46.2.0.txt │ │ │ ├── 460.txt │ │ │ ├── 461.txt │ │ │ ├── 462.txt │ │ │ ├── 463.txt │ │ │ ├── 464.txt │ │ │ ├── 466.txt │ │ │ ├── 467.txt │ │ │ ├── 468.txt │ │ │ ├── 469.txt │ │ │ ├── 47.0.0.txt │ │ │ ├── 47.1.0.txt │ │ │ ├── 47.2.0.txt │ │ │ ├── 470.txt │ │ │ ├── 471.txt │ │ │ ├── 472.txt │ │ │ ├── 473.txt │ │ │ ├── 474.txt │ │ │ ├── 475.txt │ │ │ ├── 476.txt │ │ │ ├── 477.txt │ │ │ ├── 478.txt │ │ │ ├── 479.txt │ │ │ ├── 48.0.0.txt │ │ │ ├── 48.0.1.txt │ │ │ ├── 48.1.0.txt │ │ │ ├── 48.2.0.txt │ │ │ ├── 480.txt │ │ │ ├── 481.txt │ │ │ ├── 482.txt │ │ │ ├── 483.txt │ │ │ ├── 484.txt │ │ │ ├── 487.txt │ │ │ ├── 488.txt │ │ │ ├── 489.txt │ │ │ ├── 49.0.0.txt │ │ │ ├── 49.0.1.txt │ │ │ ├── 49.1.0.txt │ │ │ ├── 49.1.1 │ │ │ ├── 49.1.2.txt │ │ │ ├── 49.1.3.txt │ │ │ ├── 49.2.0.txt │ │ │ ├── 49.3.1.txt │ │ │ ├── 49.4.0.txt │ │ │ ├── 491.txt │ │ │ ├── 492.txt │ │ │ ├── 5.0.0.txt │ │ │ ├── 5.1.0.txt │ │ │ ├── 5.2.0.txt │ │ │ ├── 5.2.1.txt │ │ │ ├── 5.3.0.txt │ │ │ ├── 5.3.1.txt │ │ │ ├── 5.4.0.txt │ │ │ ├── 5.4.1.txt │ │ │ ├── 5.4.2.txt │ │ │ ├── 50.0.0.txt │ │ │ ├── 50.1.0.txt │ │ │ ├── 50.1.1.txt │ │ │ ├── 50.1.3.txt │ │ │ ├── 50.1.4.txt │ │ │ ├── 50.1.5.txt │ │ │ ├── 50.2.0.txt │ │ │ ├── 50.2.1.txt │ │ │ ├── 50.2.2.txt │ │ │ ├── 51.0.0.txt │ │ │ ├── 51.1.0.txt │ │ │ ├── 51.2.0.txt │ │ │ ├── 52.0.0.txt │ │ │ ├── 52.1.0.txt │ │ │ ├── 52.2.0.txt │ │ │ ├── 53.0.0.txt │ │ │ ├── 53.1.0.txt │ │ │ ├── 53.1.1.txt │ │ │ ├── 53.1.2.txt │ │ │ ├── 53.2.0.txt │ │ │ ├── 53.2.1.txt │ │ │ ├── 53.2.2.txt │ │ │ ├── 53.2.3.txt │ │ │ ├── 54.0.0.txt │ │ │ ├── 54.1.0.txt │ │ │ ├── 54.2.0.txt │ │ │ ├── 55.0.0.txt │ │ │ ├── 55.0.1.txt │ │ │ ├── 55.0.2.txt │ │ │ ├── 55.1.0.txt │ │ │ ├── 55.2.0.txt │ │ │ ├── 56.0.0.txt │ │ │ ├── 56.1.0.txt │ │ │ ├── 56.2.0.txt │ │ │ ├── 56.3.0.txt │ │ │ ├── 57.0.0.txt │ │ │ ├── 57.1.0.txt │ │ │ ├── 57.1.1.txt │ │ │ ├── 57.2.0.txt │ │ │ ├── 58.0.0.txt │ │ │ ├── 59.0.0.txt │ │ │ ├── 59.0.1.txt │ │ │ ├── 59.0.2.txt │ │ │ ├── 59.0.3.txt │ │ │ ├── 59.0.4.txt │ │ │ ├── 59.1.0.txt │ │ │ ├── 59.2.0.txt │ │ │ ├── 59.3.0.txt │ │ │ ├── 59.4.0.txt │ │ │ ├── 59.4.1.txt │ │ │ ├── 59.4.2.txt │ │ │ ├── 59.4.3.txt │ │ │ ├── 59.5.0 │ │ │ ├── 59.6.0.txt │ │ │ ├── 59.7.0.txt │ │ │ ├── 6.0.0.txt │ │ │ ├── 6.0.1.txt │ │ │ ├── 6.0.2.txt │ │ │ ├── 6.1.0.txt │ │ │ ├── 6.2.0.txt │ │ │ ├── 6.2.1.txt │ │ │ ├── 6.3.0.txt │ │ │ ├── 6.3.1.txt │ │ │ ├── 6.4.0.txt │ │ │ ├── 60.0.0.txt │ │ │ ├── 60.1.0.txt │ │ │ ├── 60.2.0.txt │ │ │ ├── 60.3.0.txt │ │ │ ├── 60.4.0.txt │ │ │ ├── 60.4.1.txt │ │ │ ├── 60.4.2.txt │ │ │ ├── 61.0.0.txt │ │ │ ├── 61.0.1.txt │ │ │ ├── 61.1.0.txt │ │ │ ├── 62.0.0.txt │ │ │ ├── 62.1.0.txt │ │ │ ├── 63.0.0.txt │ │ │ ├── 63.1.0.txt │ │ │ ├── 63.2.0.txt │ │ │ ├── 63.2.1.txt │ │ │ ├── 63.2.2.txt │ │ │ ├── 63.2.3.txt │ │ │ ├── 63.2.5.txt │ │ │ ├── 63.3.0.txt │ │ │ ├── 64.0.0.txt │ │ │ ├── 64.1.0.txt │ │ │ ├── 64.2.0.txt │ │ │ ├── 64.3.0.txt │ │ │ ├── 64.3.1.txt │ │ │ ├── 65.0.0.txt │ │ │ ├── 65.1.0.txt │ │ │ ├── 65.2.0.txt │ │ │ ├── 65.2.1.txt │ │ │ ├── 65.3.0.txt │ │ │ ├── 66.0.0.txt │ │ │ ├── 66.1.0.txt │ │ │ ├── 66.1.1.txt │ │ │ ├── 66.1.2.txt │ │ │ ├── 66.1.3.txt │ │ │ ├── 66.1.4.txt │ │ │ ├── 66.1.5.txt │ │ │ ├── 66.1.6.txt │ │ │ ├── 67.0.0.txt │ │ │ ├── 67.0.1.txt │ │ │ ├── 67.0.2.txt │ │ │ ├── 67.0.3.txt │ │ │ ├── 67.1.0.txt │ │ │ ├── 67.2.0.txt │ │ │ ├── 68.0.0.txt │ │ │ ├── 68.1.0.txt │ │ │ ├── 68.1.1.txt │ │ │ ├── 68.1.2.txt │ │ │ ├── 7.0.0.txt │ │ │ ├── 7.1.0.txt │ │ │ ├── 7.1.1.txt │ │ │ ├── 7.1.2.txt │ │ │ ├── 7.2.0.txt │ │ │ ├── 7.3.0.txt │ │ │ ├── 7.4.0.txt │ │ │ ├── 8.0.0.txt │ │ │ ├── 8.0.1.txt │ │ │ ├── 8.0.2.txt │ │ │ ├── 8.1.0.txt │ │ │ ├── 8.2.0.txt │ │ │ ├── 8.3.0.txt │ │ │ ├── 8.3.1.txt │ │ │ ├── 8.3.2.txt │ │ │ ├── 8.3.3.txt │ │ │ ├── 8.3.4,txt │ │ │ ├── 8.3.5.txt │ │ │ ├── 8.3.6.txt │ │ │ ├── 8.4.0.txt │ │ │ ├── 8.4.1.txt │ │ │ ├── 8.4.2.txt │ │ │ ├── 8.4.3.txt │ │ │ ├── 8.4.4.txt │ │ │ ├── 8.5.0.txt │ │ │ ├── 8.5.1.txt │ │ │ ├── 8.5.2.txt │ │ │ ├── 8.6.0.txt │ │ │ ├── 9.0.0.txt │ │ │ ├── 9.1.0.txt │ │ │ ├── 9.2.0.txt │ │ │ ├── 9.3.0.txt │ │ │ └── 9.4.0.txt │ │ ├── full_description.txt │ │ ├── short_description.txt │ │ └── title.txt │ ├── fi-FI/ │ │ ├── changelogs/ │ │ │ ├── 10.0.0.txt │ │ │ ├── 10.1.0.txt │ │ │ ├── 10.2.0.txt │ │ │ ├── 10.3.0.txt │ │ │ ├── 10.4.0.txt │ │ │ ├── 11.0.0.txt │ │ │ ├── 11.1.0.txt │ │ │ ├── 11.2.0.txt │ │ │ ├── 11.3.0.txt │ │ │ ├── 12.0.0.txt │ │ │ ├── 12.1.0.txt │ │ │ ├── 12.2.0.txt │ │ │ ├── 12.2.1.txt │ │ │ └── 13.0.0.txt │ │ ├── full_description.txt │ │ ├── short_description.txt │ │ └── title.txt │ ├── iw-IL/ │ │ ├── changelogs/ │ │ │ ├── 10.0.0.txt │ │ │ ├── 10.1.0.txt │ │ │ ├── 10.2.0.txt │ │ │ ├── 10.3.0.txt │ │ │ ├── 10.4.0.txt │ │ │ ├── 11.0.0.txt │ │ │ ├── 11.1.0.txt │ │ │ ├── 11.2.0.txt │ │ │ ├── 11.3.0.txt │ │ │ ├── 12.0.0.txt │ │ │ ├── 12.1.0.txt │ │ │ ├── 12.2.0.txt │ │ │ ├── 12.2.1.txt │ │ │ ├── 13.0.0.txt │ │ │ ├── 13.0.1.txt │ │ │ ├── 14.0.0.txt │ │ │ ├── 15.0.0.txt │ │ │ ├── 15.1.0.txt │ │ │ ├── 15.1.1.txt │ │ │ ├── 16.0.0.txt │ │ │ ├── 16.0.1.txt │ │ │ ├── 16.1.0.txt │ │ │ ├── 17.0.0.txt │ │ │ ├── 17.1.0.txt │ │ │ ├── 17.2.0.txt │ │ │ ├── 17.2.1.txt │ │ │ ├── 17.2.2.txt │ │ │ ├── 17.2.3.txt │ │ │ ├── 17.3.0.txt │ │ │ ├── 17.4.0.txt │ │ │ ├── 18.0.0.txt │ │ │ ├── 18.0.1.txt │ │ │ ├── 18.1.0.txt │ │ │ ├── 18.1.1.txt │ │ │ ├── 18.1.2.txt │ │ │ ├── 18.1.3.txt │ │ │ ├── 18.2.0.txt │ │ │ ├── 18.2.1.txt │ │ │ ├── 18.3.0.txt │ │ │ ├── 19.0.0.txt │ │ │ ├── 19.1.0.txt │ │ │ ├── 20.0.0.txt │ │ │ ├── 20.0.1.txt │ │ │ ├── 20.0.2.txt │ │ │ ├── 20.1.0.txt │ │ │ ├── 21.0.0.txt │ │ │ ├── 21.1.0.txt │ │ │ ├── 21.2.0.txt │ │ │ ├── 22.0.0.txt │ │ │ ├── 22.1.0.txt │ │ │ ├── 23.0.0.txt │ │ │ ├── 23.1.0.txt │ │ │ ├── 23.2.0.txt │ │ │ ├── 24.0.0.txt │ │ │ ├── 24.1.0.txt │ │ │ ├── 24.2.0.txt │ │ │ ├── 24.3.0.txt │ │ │ ├── 24.4.0.txt │ │ │ ├── 25.0.0.txt │ │ │ ├── 26.0.0.txt │ │ │ ├── 26.0.1.txt │ │ │ ├── 26.1.0.txt │ │ │ ├── 26.1.1.txt │ │ │ ├── 26.1.2.txt │ │ │ ├── 27.0.0.txt │ │ │ ├── 27.0.1.txt │ │ │ ├── 28.0.0.txt │ │ │ ├── 28.1.0.txt │ │ │ ├── 28.1.1.txt │ │ │ ├── 28.2.0.txt │ │ │ ├── 29.0.0.txt │ │ │ ├── 29.1.0.txt │ │ │ ├── 29.2.0.txt │ │ │ ├── 29.2.1.txt │ │ │ ├── 3.2.0.txt │ │ │ ├── 3.2.2.txt │ │ │ ├── 30.0.0.txt │ │ │ ├── 30.1.0.txt │ │ │ ├── 30.2.0.txt │ │ │ ├── 30.3.0.txt │ │ │ ├── 37.4.0.txt │ │ │ ├── 53.2.2.txt │ │ │ └── 8.5.0.txt │ │ ├── full_description.txt │ │ ├── short_description.txt │ │ └── title.txt │ ├── nb-NO/ │ │ ├── changelogs/ │ │ │ ├── 10.2.0.txt │ │ │ ├── 12.0.0.txt │ │ │ ├── 12.2.1.txt │ │ │ ├── 3.2.0.txt │ │ │ ├── 3.2.2.txt │ │ │ ├── 36.0.0.txt │ │ │ ├── 36.1.2.txt │ │ │ ├── 4.0.0.txt │ │ │ ├── 4.1.0.txt │ │ │ ├── 4.1.1.txt │ │ │ ├── 4.1.2.txt │ │ │ ├── 5.0.0.txt │ │ │ ├── 5.1.0.txt │ │ │ ├── 5.2.0.txt │ │ │ ├── 5.2.1.txt │ │ │ ├── 5.3.0.txt │ │ │ ├── 5.3.1.txt │ │ │ ├── 5.4.0.txt │ │ │ ├── 5.4.1.txt │ │ │ ├── 5.4.2.txt │ │ │ ├── 6.0.0.txt │ │ │ ├── 6.0.1.txt │ │ │ ├── 6.0.2.txt │ │ │ ├── 6.1.0.txt │ │ │ ├── 6.2.0.txt │ │ │ ├── 6.2.1.txt │ │ │ ├── 6.3.0.txt │ │ │ ├── 6.3.1.txt │ │ │ ├── 6.4.0.txt │ │ │ ├── 7.0.0.txt │ │ │ ├── 7.1.0.txt │ │ │ ├── 7.1.1.txt │ │ │ ├── 7.1.2.txt │ │ │ ├── 7.2.0.txt │ │ │ ├── 7.3.0.txt │ │ │ ├── 7.4.0.txt │ │ │ ├── 8.0.0.txt │ │ │ ├── 8.0.1.txt │ │ │ ├── 8.0.2.txt │ │ │ ├── 8.1.0.txt │ │ │ ├── 8.2.0.txt │ │ │ ├── 8.3.0.txt │ │ │ ├── 8.3.1.txt │ │ │ ├── 8.3.2.txt │ │ │ ├── 8.3.3.txt │ │ │ ├── 8.3.5.txt │ │ │ ├── 8.3.6.txt │ │ │ ├── 8.4.0.txt │ │ │ ├── 8.4.1.txt │ │ │ ├── 8.4.2.txt │ │ │ ├── 8.4.3.txt │ │ │ ├── 8.4.4.txt │ │ │ ├── 8.5.0.txt │ │ │ ├── 8.5.1.txt │ │ │ ├── 8.5.2.txt │ │ │ ├── 8.6.0.txt │ │ │ ├── 9.0.0.txt │ │ │ ├── 9.1.0.txt │ │ │ ├── 9.2.0.txt │ │ │ └── 9.3.0.txt │ │ ├── full_description.txt │ │ ├── short_description.txt │ │ └── title.txt │ ├── pl/ │ │ └── changelogs/ │ │ └── 15.1.1.txt │ ├── ro/ │ │ ├── full_description.txt │ │ ├── short_description.txt │ │ └── title.txt │ ├── ru-RU/ │ │ ├── changelogs/ │ │ │ ├── 10.0.0.txt │ │ │ ├── 10.1.0.txt │ │ │ ├── 10.2.0.txt │ │ │ ├── 10.3.0.txt │ │ │ ├── 10.4.0.txt │ │ │ ├── 11.0.0.txt │ │ │ ├── 11.1.0.txt │ │ │ ├── 11.2.0.txt │ │ │ ├── 11.3.0.txt │ │ │ ├── 12.0.0.txt │ │ │ ├── 12.1.0.txt │ │ │ ├── 12.2.0.txt │ │ │ ├── 12.2.1.txt │ │ │ ├── 15.1.1.txt │ │ │ ├── 17.2.1.txt │ │ │ ├── 18.0.1.txt │ │ │ ├── 18.1.1.txt │ │ │ ├── 18.1.3.txt │ │ │ ├── 18.2.0.txt │ │ │ ├── 18.2.1.txt │ │ │ ├── 18.3.0.txt │ │ │ ├── 19.1.0.txt │ │ │ ├── 20.0.2.txt │ │ │ ├── 20.1.0.txt │ │ │ ├── 22.0.0.txt │ │ │ ├── 22.1.0.txt │ │ │ ├── 23.2.0.txt │ │ │ ├── 24.0.0.txt │ │ │ ├── 24.4.0.txt │ │ │ ├── 25.0.0.txt │ │ │ ├── 26.0.0.txt │ │ │ ├── 26.1.1.txt │ │ │ ├── 26.1.2.txt │ │ │ ├── 27.0.0.txt │ │ │ ├── 27.0.1.txt │ │ │ ├── 28.0.0.txt │ │ │ ├── 28.1.1.txt │ │ │ ├── 28.2.0.txt │ │ │ ├── 29.2.1.txt │ │ │ ├── 30.0.1.txt │ │ │ ├── 30.1.0.txt │ │ │ ├── 31.2.1.txt │ │ │ ├── 32.1.0.txt │ │ │ ├── 32.2.0.txt │ │ │ ├── 34.1.0.txt │ │ │ ├── 36.1.2.txt │ │ │ ├── 37.0.0.txt │ │ │ ├── 37.0.1.txt │ │ │ ├── 37.3.0.txt │ │ │ ├── 37.4.0.txt │ │ │ ├── 4.1.2.txt │ │ │ ├── 42.1.0.txt │ │ │ ├── 44.2.0.txt │ │ │ ├── 45.1.0.txt │ │ │ ├── 45.1.2.txt │ │ │ ├── 46.0.1.txt │ │ │ ├── 49.1.0.txt │ │ │ ├── 49.1.2.txt │ │ │ ├── 49.1.3.txt │ │ │ ├── 49.4.0.txt │ │ │ ├── 5.3.1.txt │ │ │ ├── 50.1.4.txt │ │ │ ├── 50.1.5.txt │ │ │ ├── 51.2.0.txt │ │ │ ├── 53.0.0.txt │ │ │ ├── 53.1.0.txt │ │ │ ├── 53.1.1.txt │ │ │ ├── 6.0.0.txt │ │ │ ├── 6.0.2.txt │ │ │ ├── 7.1.0.txt │ │ │ ├── 7.1.1.txt │ │ │ ├── 8.0.1.txt │ │ │ ├── 8.0.2.txt │ │ │ ├── 8.2.0.txt │ │ │ ├── 8.3.0.txt │ │ │ ├── 8.3.5.txt │ │ │ ├── 8.4.0.txt │ │ │ ├── 8.4.1.txt │ │ │ ├── 8.4.3.txt │ │ │ ├── 8.5.0.txt │ │ │ ├── 8.5.2.txt │ │ │ ├── 8.6.0.txt │ │ │ ├── 9.0.0.txt │ │ │ ├── 9.1.0.txt │ │ │ ├── 9.2.0.txt │ │ │ ├── 9.3.0.txt │ │ │ └── 9.4.0.txt │ │ ├── full_description.txt │ │ ├── short_description.txt │ │ └── title.txt │ ├── sv/ │ │ ├── changelogs/ │ │ │ ├── 10.0.0.txt │ │ │ ├── 10.1.0.txt │ │ │ ├── 10.2.0.txt │ │ │ ├── 10.3.0.txt │ │ │ ├── 10.4.0.txt │ │ │ ├── 11.0.0.txt │ │ │ ├── 11.1.0.txt │ │ │ ├── 11.2.0.txt │ │ │ ├── 11.3.0.txt │ │ │ ├── 12.0.0.txt │ │ │ ├── 12.1.0.txt │ │ │ ├── 12.2.0.txt │ │ │ ├── 12.2.1.txt │ │ │ ├── 13.0.0.txt │ │ │ ├── 13.0.1.txt │ │ │ ├── 14.0.0.txt │ │ │ ├── 15.0.0.txt │ │ │ ├── 15.1.0.txt │ │ │ ├── 15.1.1.txt │ │ │ ├── 16.0.0.txt │ │ │ ├── 16.0.1.txt │ │ │ ├── 16.1.0.txt │ │ │ ├── 17.0.0.txt │ │ │ ├── 17.1.0.txt │ │ │ ├── 17.2.0.txt │ │ │ ├── 17.2.1.txt │ │ │ ├── 17.2.2.txt │ │ │ ├── 17.2.3.txt │ │ │ ├── 17.3.0.txt │ │ │ ├── 17.4.0.txt │ │ │ ├── 18.0.0.txt │ │ │ ├── 18.0.1.txt │ │ │ ├── 18.1.0.txt │ │ │ ├── 18.1.1.txt │ │ │ ├── 18.1.2.txt │ │ │ ├── 18.1.3.txt │ │ │ ├── 18.2.0.txt │ │ │ ├── 18.2.1.txt │ │ │ ├── 18.3.0.txt │ │ │ ├── 19.0.0.txt │ │ │ ├── 19.1.0.txt │ │ │ ├── 20.0.0.txt │ │ │ ├── 20.0.1.txt │ │ │ ├── 20.0.2.txt │ │ │ ├── 20.1.0.txt │ │ │ ├── 21.0.0.txt │ │ │ ├── 21.1.0.txt │ │ │ ├── 21.2.0.txt │ │ │ ├── 22.0.0.txt │ │ │ ├── 22.1.0.txt │ │ │ ├── 23.0.0.txt │ │ │ ├── 23.1.0.txt │ │ │ ├── 23.2.0.txt │ │ │ ├── 24.0.0.txt │ │ │ ├── 24.1.0.txt │ │ │ ├── 24.2.0.txt │ │ │ ├── 24.3.0.txt │ │ │ ├── 24.4.0.txt │ │ │ ├── 25.0.0.txt │ │ │ ├── 26.0.0.txt │ │ │ ├── 26.0.1.txt │ │ │ ├── 26.1.0.txt │ │ │ ├── 26.1.1.txt │ │ │ ├── 26.1.2.txt │ │ │ ├── 27.0.0.txt │ │ │ ├── 27.0.1.txt │ │ │ ├── 28.0.0.txt │ │ │ ├── 28.1.0.txt │ │ │ ├── 28.1.1.txt │ │ │ ├── 28.2.0.txt │ │ │ ├── 29.0.0.txt │ │ │ ├── 29.1.0.txt │ │ │ ├── 29.2.0.txt │ │ │ ├── 29.2.1.txt │ │ │ ├── 3.2.0.txt │ │ │ ├── 3.2.2.txt │ │ │ ├── 30.0.0.txt │ │ │ ├── 30.0.1.txt │ │ │ ├── 30.1.0.txt │ │ │ ├── 30.2.0.txt │ │ │ ├── 30.3.0.txt │ │ │ ├── 31.0.0.txt │ │ │ ├── 31.1.0.txt │ │ │ ├── 31.2.0.txt │ │ │ ├── 31.2.1.txt │ │ │ ├── 32.0.0.txt │ │ │ ├── 32.1.0.txt │ │ │ ├── 32.2.0.txt │ │ │ ├── 32.3.0.txt │ │ │ ├── 33.0.0.txt │ │ │ ├── 34.0.0.txt │ │ │ ├── 34.1.0.txt │ │ │ ├── 35.0.0.txt │ │ │ ├── 35.1.0.txt │ │ │ ├── 35.2.0.txt │ │ │ ├── 36.0.0.txt │ │ │ ├── 36.1.0.txt │ │ │ ├── 36.1.1.txt │ │ │ ├── 36.1.2.txt │ │ │ ├── 36.2.0.txt │ │ │ ├── 37.0.0.txt │ │ │ ├── 37.0.1.txt │ │ │ ├── 37.1.0.txt │ │ │ ├── 37.2.0.txt │ │ │ ├── 37.3.0.txt │ │ │ ├── 37.4.0.txt │ │ │ ├── 38.0.0.txt │ │ │ ├── 39.0.0.txt │ │ │ ├── 39.1.0.txt │ │ │ ├── 4.0.0.txt │ │ │ ├── 4.1.0.txt │ │ │ ├── 4.1.1.txt │ │ │ ├── 4.1.2.txt │ │ │ ├── 40.0.0.txt │ │ │ ├── 40.0.1.txt │ │ │ ├── 40.1.0.txt │ │ │ ├── 41.0.0.txt │ │ │ ├── 42.0.0.txt │ │ │ ├── 42.1.0.txt │ │ │ ├── 42.2.0.txt │ │ │ ├── 42.3.0.txt │ │ │ ├── 43.0.0.txt │ │ │ ├── 43.0.2.txt │ │ │ ├── 43.1.0.txt │ │ │ ├── 44.0.0.txt │ │ │ ├── 44.1.0.txt │ │ │ ├── 44.2.0.txt │ │ │ ├── 446.txt │ │ │ ├── 447.txt │ │ │ ├── 448.txt │ │ │ ├── 449.txt │ │ │ ├── 45.0.0.txt │ │ │ ├── 45.1.0.txt │ │ │ ├── 45.1.1.txt │ │ │ ├── 45.1.2.txt │ │ │ ├── 46.0.0.txt │ │ │ ├── 46.0.1.txt │ │ │ ├── 46.1.0.txt │ │ │ ├── 46.1.1.txt │ │ │ ├── 46.2.0.txt │ │ │ ├── 47.0.0.txt │ │ │ ├── 47.1.0.txt │ │ │ ├── 47.2.0.txt │ │ │ ├── 48.0.0.txt │ │ │ ├── 48.0.1.txt │ │ │ ├── 48.1.0.txt │ │ │ ├── 48.2.0.txt │ │ │ ├── 49.0.0.txt │ │ │ ├── 49.0.1.txt │ │ │ ├── 49.1.0.txt │ │ │ ├── 49.1.2.txt │ │ │ ├── 49.1.3.txt │ │ │ ├── 49.2.0.txt │ │ │ ├── 49.3.1.txt │ │ │ ├── 49.4.0.txt │ │ │ ├── 5.0.0.txt │ │ │ ├── 5.1.0.txt │ │ │ ├── 5.2.0.txt │ │ │ ├── 5.2.1.txt │ │ │ ├── 5.3.0.txt │ │ │ ├── 5.3.1.txt │ │ │ ├── 5.4.0.txt │ │ │ ├── 5.4.1.txt │ │ │ ├── 5.4.2.txt │ │ │ ├── 50.0.0.txt │ │ │ ├── 50.1.0.txt │ │ │ ├── 50.1.1.txt │ │ │ ├── 50.1.3.txt │ │ │ ├── 50.1.4.txt │ │ │ ├── 50.1.5.txt │ │ │ ├── 50.2.0.txt │ │ │ ├── 50.2.1.txt │ │ │ ├── 50.2.2.txt │ │ │ ├── 51.0.0.txt │ │ │ ├── 51.1.0.txt │ │ │ ├── 51.2.0.txt │ │ │ ├── 52.0.0.txt │ │ │ ├── 52.1.0.txt │ │ │ ├── 52.2.0.txt │ │ │ ├── 53.0.0.txt │ │ │ ├── 53.1.0.txt │ │ │ ├── 53.1.1.txt │ │ │ ├── 53.1.2.txt │ │ │ ├── 53.2.0.txt │ │ │ ├── 53.2.1.txt │ │ │ ├── 53.2.2.txt │ │ │ ├── 53.2.3.txt │ │ │ ├── 54.0.0.txt │ │ │ ├── 54.1.0.txt │ │ │ ├── 54.2.0.txt │ │ │ ├── 55.0.0.txt │ │ │ ├── 55.0.1.txt │ │ │ ├── 55.0.2.txt │ │ │ ├── 55.1.0.txt │ │ │ ├── 55.2.0.txt │ │ │ ├── 56.0.0.txt │ │ │ ├── 56.1.0.txt │ │ │ ├── 56.2.0.txt │ │ │ ├── 56.3.0.txt │ │ │ ├── 57.0.0.txt │ │ │ ├── 57.1.0.txt │ │ │ ├── 57.1.1.txt │ │ │ ├── 57.2.0.txt │ │ │ ├── 58.0.0.txt │ │ │ ├── 59.0.0.txt │ │ │ ├── 59.0.1.txt │ │ │ ├── 59.0.2.txt │ │ │ ├── 59.0.3.txt │ │ │ ├── 59.0.4.txt │ │ │ ├── 59.1.0.txt │ │ │ ├── 59.2.0.txt │ │ │ ├── 59.3.0.txt │ │ │ ├── 59.4.0.txt │ │ │ ├── 59.4.1.txt │ │ │ ├── 59.4.2.txt │ │ │ ├── 59.4.3.txt │ │ │ ├── 59.6.0.txt │ │ │ ├── 59.7.0.txt │ │ │ ├── 6.0.0.txt │ │ │ ├── 6.0.1.txt │ │ │ ├── 6.0.2.txt │ │ │ ├── 6.1.0.txt │ │ │ ├── 6.2.0.txt │ │ │ ├── 6.2.1.txt │ │ │ ├── 6.3.0.txt │ │ │ ├── 6.3.1.txt │ │ │ ├── 6.4.0.txt │ │ │ ├── 60.0.0.txt │ │ │ ├── 60.1.0.txt │ │ │ ├── 60.2.0.txt │ │ │ ├── 60.3.0.txt │ │ │ ├── 60.4.0.txt │ │ │ ├── 60.4.1.txt │ │ │ ├── 60.4.2.txt │ │ │ ├── 61.0.0.txt │ │ │ ├── 61.0.1.txt │ │ │ ├── 61.1.0.txt │ │ │ ├── 62.0.0.txt │ │ │ ├── 62.1.0.txt │ │ │ ├── 63.0.0.txt │ │ │ ├── 63.1.0.txt │ │ │ ├── 63.2.0.txt │ │ │ ├── 63.2.1.txt │ │ │ ├── 63.2.2.txt │ │ │ ├── 63.2.3.txt │ │ │ ├── 63.2.5.txt │ │ │ ├── 63.3.0.txt │ │ │ ├── 64.0.0.txt │ │ │ ├── 64.1.0.txt │ │ │ ├── 64.2.0.txt │ │ │ ├── 64.3.0.txt │ │ │ ├── 64.3.1.txt │ │ │ ├── 65.0.0.txt │ │ │ ├── 65.1.0.txt │ │ │ ├── 65.2.0.txt │ │ │ ├── 65.2.1.txt │ │ │ ├── 65.3.0.txt │ │ │ ├── 66.0.0.txt │ │ │ ├── 66.1.0.txt │ │ │ ├── 66.1.1.txt │ │ │ ├── 66.1.2.txt │ │ │ ├── 66.1.3.txt │ │ │ ├── 66.1.4.txt │ │ │ ├── 66.1.5.txt │ │ │ ├── 66.1.6.txt │ │ │ ├── 67.0.0.txt │ │ │ ├── 67.0.1.txt │ │ │ ├── 67.0.2.txt │ │ │ ├── 67.0.3.txt │ │ │ ├── 67.1.0.txt │ │ │ ├── 67.2.0.txt │ │ │ ├── 68.0.0.txt │ │ │ ├── 68.1.0.txt │ │ │ ├── 68.1.1.txt │ │ │ ├── 68.1.2.txt │ │ │ ├── 7.0.0.txt │ │ │ ├── 7.1.0.txt │ │ │ ├── 7.1.1.txt │ │ │ ├── 7.1.2.txt │ │ │ ├── 7.2.0.txt │ │ │ ├── 7.3.0.txt │ │ │ ├── 7.4.0.txt │ │ │ ├── 8.0.0.txt │ │ │ ├── 8.0.1.txt │ │ │ ├── 8.0.2.txt │ │ │ ├── 8.1.0.txt │ │ │ ├── 8.2.0.txt │ │ │ ├── 8.3.0.txt │ │ │ ├── 8.3.1.txt │ │ │ ├── 8.3.2.txt │ │ │ ├── 8.3.3.txt │ │ │ ├── 8.3.5.txt │ │ │ ├── 8.3.6.txt │ │ │ ├── 8.4.0.txt │ │ │ ├── 8.4.1.txt │ │ │ ├── 8.4.2.txt │ │ │ ├── 8.4.3.txt │ │ │ ├── 8.4.4.txt │ │ │ ├── 8.5.0.txt │ │ │ ├── 8.5.1.txt │ │ │ ├── 8.5.2.txt │ │ │ ├── 8.6.0.txt │ │ │ ├── 9.0.0.txt │ │ │ ├── 9.1.0.txt │ │ │ ├── 9.2.0.txt │ │ │ ├── 9.3.0.txt │ │ │ └── 9.4.0.txt │ │ ├── full_description.txt │ │ ├── short_description.txt │ │ └── title.txt │ └── zh-CN/ │ ├── full_description.txt │ ├── short_description.txt │ └── title.txt ├── gradle/ │ ├── libs.versions.toml │ └── wrapper/ │ └── gradle-wrapper.properties ├── gradle.properties ├── gradlew ├── gradlew.bat └── settings.gradle.kts ================================================ FILE CONTENTS ================================================ ================================================ FILE: .clang-format ================================================ --- Language: Cpp # BasedOnStyle: LLVM AccessModifierOffset: -4 AlignAfterOpenBracket: DontAlign AlignConsecutiveAssignments: false AlignConsecutiveDeclarations: false AlignEscapedNewlinesLeft: true AlignOperands: true AlignTrailingComments: true AllowAllParametersOfDeclarationOnNextLine: true AllowShortBlocksOnASingleLine: false AllowShortCaseLabelsOnASingleLine: false AllowShortFunctionsOnASingleLine: None AllowShortIfStatementsOnASingleLine: false AllowShortLoopsOnASingleLine: false AlwaysBreakAfterDefinitionReturnType: None AlwaysBreakAfterReturnType: None AlwaysBreakBeforeMultilineStrings: false AlwaysBreakTemplateDeclarations: false BinPackArguments: true BinPackParameters: true BraceWrapping: AfterClass: false AfterControlStatement: false AfterEnum: false AfterFunction: true AfterNamespace: false AfterObjCDeclaration: false AfterStruct: false AfterUnion: false BeforeCatch: false BeforeElse: false IndentBraces: false BreakBeforeBinaryOperators: NonAssignment BreakBeforeBraces: Mozilla BreakBeforeTernaryOperators: true BreakConstructorInitializersBeforeComma: false ColumnLimit: 100 CommentPragmas: '^ IWYU pragma:' ConstructorInitializerAllOnOneLineOrOnePerLine: false ConstructorInitializerIndentWidth: 4 ContinuationIndentWidth: 8 Cpp11BracedListStyle: true DerivePointerAlignment: false DisableFormat: false ExperimentalAutoDetectBinPacking: false ForEachMacros: [ foreach, Q_FOREACH, BOOST_FOREACH ] IncludeCategories: - Regex: '^"(llvm|llvm-c|clang|clang-c)/' Priority: 2 - Regex: '^(<|"(gtest|isl|json)/)' Priority: 3 - Regex: '.*' Priority: 1 IndentCaseLabels: true IndentWidth: 4 IndentWrappedFunctionNames: false IndentPPDirectives: BeforeHash KeepEmptyLinesAtTheStartOfBlocks: true MacroBlockBegin: '' MacroBlockEnd: '' MaxEmptyLinesToKeep: 2 NamespaceIndentation: None ObjCBlockIndentWidth: 4 ObjCSpaceAfterProperty: false ObjCSpaceBeforeProtocolList: true PenaltyBreakBeforeFirstCallParameter: 19 PenaltyBreakComment: 300 PenaltyBreakFirstLessLess: 120 PenaltyBreakString: 1000 PenaltyExcessCharacter: 1000000 PenaltyReturnTypeOnItsOwnLine: 60 PointerAlignment: Right ReflowComments: false SortIncludes: false SpaceAfterCStyleCast: false SpaceBeforeAssignmentOperators: true SpaceBeforeParens: ControlStatements SpaceInEmptyParentheses: false SpacesBeforeTrailingComments: 1 SpacesInAngles: false SpacesInContainerLiterals: true SpacesInCStyleCastParentheses: false SpacesInParentheses: false SpacesInSquareBrackets: false Standard: Cpp11 UseTab: Never TabWidth: 4 ... ================================================ FILE: .gitignore ================================================ /.idea local.properties .DS_Store *~ *.iml *.orig *.rej java_* .gradle /local.properties build /captures .externalNativeBuild /app/release /app/.cxx gradle/wrapper/gradle-wrapper.jar /distribution/* /distribution.video/* ================================================ FILE: .gitmodules ================================================ [submodule "libbaresip-android"] path = libbaresip-android url = https://github.com/juha-h/libbaresip-android.git branch = master ================================================ FILE: LICENSE ================================================ BSD 3-Clause License Copyright (c) 2018, TutPro Inc. All rights reserved. Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: * Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. * Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. * Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. ================================================ FILE: PrivacyPolicy.txt ================================================ baresip Privacy Policy Last updated: Sept 25, 2022 baresip does not collect any personal, app usage, or any other information nor share any information with anyone. Information you submit in baresip app is stored privately and securely on your Android device and is deleted when you uninstall the app. You can be optionally choose in Settings that baresip uses Android contacts as references to SIP and tel URIs. If chosen, baresip does not store or upload Android contacts anywhere nor share them with anyone. Use https://github.com/juha-h/baresip-studio/discussions page for questions regarding baresip app's privacy policy. ================================================ FILE: README.md ================================================ This is a bare-bones Android Studio project implementing baresip based SIP User Agent for Android. Its development is motivated by need for a secure, privacy focused SIP user agent for Android that does not depend on third party push notification services. Currently the application supports voice calling and messaging, voice conference calls, UDP, TCP, TLS, and WSS signaling transports, voicemail Message Waiting Indication, call transfers (REFER), AMR, Codec2, G.722, G.722.1, G.729, Opus, and PCMU/PCMA voice codecs, as well as ZRTP and (DTLS) SRTP media encapsulation. Minimum supported Android version is 9 (API level 28). If you need video calling and have a device that supports Camera2 API, you can instead of this application install its sister application baresip+ from video branch. After cloning the project, generate static libraries and include files to distribution directory using master branch of libbaresip-android. Then in Android Studio (tested with Android Studio Panda 2 | 2025.3.2): - Open an existing Android Studio project - File -> Invalidate Caches ... -> Invalidate & Restart - Build -> Generate Signed Bundle / APK ... Ready to be installed baresip app is available from F-Droid, Play Store, and from GitHub. Signing certificate SHA-256 fingerprint of the GitHub APKs is FE:CC:79:C2:0A:B7:25:B9:B7:8B:B1:6A:75:BA:9A:04:09:28:22:CF:52:BA:32:E4:A4:37:17:0A:68:02:06:02. Use `keytool -printcert -jarfile app-release.apk` to verify. Language translations are managed via baresip Weblate project. Copyright (c) 2018 TutPro Inc. Distributed under BSD-3-Clause license. ================================================ FILE: app/build.gradle.kts ================================================ import com.android.build.api.dsl.ApplicationExtension plugins { alias(libs.plugins.compose.compiler) alias(libs.plugins.kotlin.serialization) id("com.android.application") } configure { compileSdk = 36 ndkVersion = "29.0.14206865" defaultConfig { applicationId = "com.tutpro.baresip" minSdk = 28 versionCode = 492 versionName = "78.1.1" @Suppress("UnstableApiUsage") externalNativeBuild { cmake { cFlags += "-DHAVE_INTTYPES_H -lstdc++" arguments.addAll(listOf("-DANDROID_STL=c++_shared")) } } ndk { // noinspection ChromeOsAbiSupport abiFilters.addAll(listOf("armeabi-v7a", "arm64-v8a")) } vectorDrawables.useSupportLibrary = true } buildTypes { debug { ndk { abiFilters.add("x86_64") } } release { isMinifyEnabled = true isShrinkResources = true proguardFiles( getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro" ) } } buildFeatures { viewBinding = true buildConfig = true compose = true } externalNativeBuild { cmake { path = file("src/main/cpp/CMakeLists.txt") version = "3.31.6" } } namespace = "com.tutpro.baresip" } composeCompiler { reportsDestination = layout.buildDirectory.dir("compose_compiler") } dependencies { implementation(libs.androidx.foundation.android) implementation(libs.androidx.runtime.livedata) implementation(libs.androidx.compose.material3) implementation(platform(libs.androidx.compose.bom)) implementation(libs.androidx.activity.compose) implementation(libs.androidx.ui) implementation(libs.kotlin.stdlib.jdk8) implementation(libs.material) implementation(libs.androidx.preference.ktx) implementation(libs.androidx.exifinterface) implementation(libs.androidx.core.ktx) implementation(libs.kotlinx.coroutines.android) implementation(libs.kotlinx.serialization.json) implementation(libs.androidx.activity.ktx) implementation(libs.androidx.fragment.ktx) implementation(libs.androidx.media) implementation(libs.coil.compose) implementation(libs.androidx.navigation.compose) implementation(libs.androidx.navigation.runtime.android) implementation(libs.androidx.lifecycle.process) implementation(libs.androidx.lifecycle.viewmodel.ktx) implementation(libs.androidx.compose.material.icons.core) implementation(libs.androidx.compose.material.icons.extended) implementation(libs.androidx.compose.runtime) } ================================================ FILE: app/proguard-rules.pro ================================================ # Add project specific ProGuard rules here. # By default, the flags in this file are appended to flags specified # in /Users/gfan/dev/sdk_current/tools/proguard/proguard-android.txt # You can edit the include path and order by changing the proguardFiles # directive in build.gradle. # # For more details, see # http://developer.android.com/guide/developing/tools/proguard.html # Add any project specific keep options here: -keepattributes LineNumberTable,SourceFile -dontobfuscate # If your project uses WebView with JS, uncomment the following # and specify the fully qualified class name to the JavaScript interface # class: #-keepclassmembers class fqcn.of.javascript.interface.for.webview { # public *; #} ================================================ FILE: app/src/main/AndroidManifest.xml ================================================ ================================================ FILE: app/src/main/assets/accounts ================================================ ================================================ FILE: app/src/main/assets/config.static ================================================ poll_method epoll call_local_timeout 120 call_max_calls 4 call_hold_other_calls yes filter_registrar udp,tcp,tls,ws,wss audio_player aaudio,nil audio_source aaudio,nil audio_alert aaudio,nil audio_level no ausrc_format s16 auplay_format s16 auenc_format s16 audec_format s16 audio_buffer 20-160 audio_buffer_mode adaptive audio_silence -35.0 audio_telev_pt 101 audio_jitter_buffer_type adaptive audio_jitter_buffer_ms 100-200 audio_jitter_buffer_size 50 rtp_stats yes rtp_timeout 60 rtp_rxmode thread module aaudio.so module stun.so module turn.so module ice.so module srtp.so module dtls_srtp.so module gzrtp.so module uuid.so module_app account.so module_app debug_cmd.so module_app mwi.so opus_samplerate 16000 opus_stereo no opus_sprop_stereo no opus_cbr no opus_inbandfec yes opus_application voip dtls_srtp_use_ec prime256v1 ================================================ FILE: app/src/main/assets/contacts ================================================ "The Test Call" ================================================ FILE: app/src/main/cpp/CMakeLists.txt ================================================ cmake_minimum_required(VERSION 3.18...4.0) project(baresip) add_link_options("LINKER:--build-id=none") set(distribution_DIR ${CMAKE_SOURCE_DIR}/../../../../distribution) add_library(lib_crypto STATIC IMPORTED) set_target_properties(lib_crypto PROPERTIES IMPORTED_LOCATION ${distribution_DIR}/openssl/lib/${ANDROID_ABI}/libcrypto.a) add_library(lib_ssl STATIC IMPORTED) set_target_properties(lib_ssl PROPERTIES IMPORTED_LOCATION ${distribution_DIR}/openssl/lib/${ANDROID_ABI}/libssl.a) add_library(lib_re STATIC IMPORTED) set_target_properties(lib_re PROPERTIES IMPORTED_LOCATION ${distribution_DIR}/re/lib/${ANDROID_ABI}/libre.a) add_library(lib_opus STATIC IMPORTED) set_target_properties(lib_opus PROPERTIES IMPORTED_LOCATION ${distribution_DIR}/opus/lib/${ANDROID_ABI}/libopus.a) add_library(lib_g722 STATIC IMPORTED) set_target_properties(lib_g722 PROPERTIES IMPORTED_LOCATION ${distribution_DIR}/g722/lib/${ANDROID_ABI}/libg722.a) add_library(lib_g722_1 STATIC IMPORTED) set_target_properties(lib_g722_1 PROPERTIES IMPORTED_LOCATION ${distribution_DIR}/g7221/lib/${ANDROID_ABI}/libg722_1.a) add_library(lib_g729 STATIC IMPORTED) set_target_properties(lib_g729 PROPERTIES IMPORTED_LOCATION ${distribution_DIR}/g729/lib/${ANDROID_ABI}/libbcg729.a) add_library(lib_codec2 STATIC IMPORTED) set_target_properties(lib_codec2 PROPERTIES IMPORTED_LOCATION ${distribution_DIR}/codec2/lib/${ANDROID_ABI}/libcodec2.a) add_library(lib_amrnb STATIC IMPORTED) set_target_properties(lib_amrnb PROPERTIES IMPORTED_LOCATION ${distribution_DIR}/amr/lib/${ANDROID_ABI}/libamrnb.a) add_library(lib_amrwb STATIC IMPORTED) set_target_properties(lib_amrwb PROPERTIES IMPORTED_LOCATION ${distribution_DIR}/amr/lib/${ANDROID_ABI}/libamrwb.a) add_library(lib_amrwbenc STATIC IMPORTED) set_target_properties(lib_amrwbenc PROPERTIES IMPORTED_LOCATION ${distribution_DIR}/amr/lib/${ANDROID_ABI}/libamrwbenc.a) add_library(lib_zrtpcppcore STATIC IMPORTED) set_target_properties(lib_zrtpcppcore PROPERTIES IMPORTED_LOCATION ${distribution_DIR}/gzrtp/lib/${ANDROID_ABI}/libzrtpcppcore.a) add_library(lib_sndfile STATIC IMPORTED) set_target_properties(lib_sndfile PROPERTIES IMPORTED_LOCATION ${distribution_DIR}/sndfile/lib/${ANDROID_ABI}/libsndfile.a) add_library(lib_baresip STATIC IMPORTED) set_target_properties(lib_baresip PROPERTIES IMPORTED_LOCATION ${distribution_DIR}/baresip/lib/${ANDROID_ABI}/libbaresip.a) add_library(baresip SHARED ${CMAKE_SOURCE_DIR}/baresip.c) target_include_directories(baresip PRIVATE ${distribution_DIR}/openssl/include ${distribution_DIR}/re/include ${distribution_DIR}/baresip/include) add_definitions(-DHAVE_PTHREAD) target_link_libraries( baresip android aaudio lib_baresip lib_re lib_ssl lib_crypto lib_opus lib_g722 lib_g722_1 lib_g729 lib_codec2 lib_amrnb lib_amrwb lib_amrwbenc lib_zrtpcppcore lib_sndfile z log) ================================================ FILE: app/src/main/cpp/baresip.c ================================================ #include #include #include #include #include #include #include #include "logger.h" enum { ASYNC_WORKERS = 4 }; typedef struct baresip_context { JavaVM *javaVM; jclass mainActivityClz; jobject mainActivityObj; } BaresipContext; BaresipContext g_ctx; static int vprintf_null(const char *p, size_t size, void *arg) { (void)p; (void)size; (void)arg; return 0; } static void net_debug_log() { char debug_buf[2048]; int l; l = re_snprintf(&(debug_buf[0]), 2047, "%H", net_debug, baresip_network()); if (l != -1) { debug_buf[l] = '\0'; LOGD("%s\n", debug_buf); } } static void net_dns_debug_log() { char debug_buf[2048]; int l; LOGD("net_dns_debug_log\n"); l = re_snprintf(&(debug_buf[0]), 2047, "%H", net_dns_debug, baresip_network()); if (l != -1) { debug_buf[l] = '\0'; LOGD("%s\n", debug_buf); } } static void ua_debug_log(struct ua *ua) { char debug_buf[2048]; int l; l = re_snprintf(&(debug_buf[0]), 2047, "%H", ua_debug, ua); if (l != -1) { debug_buf[l] = '\0'; LOGD("%s\n", debug_buf); } } static void account_debug_log(struct account *acc) { char debug_buf[2048]; int l; l = re_snprintf(&(debug_buf[0]), 2047, "%H", account_debug, acc); if (l != -1) { debug_buf[l] = '\0'; LOGD("%s\n", debug_buf); } } #if 0 static void ua_print_status_log(struct ua *ua) { char debug_buf[2048]; int l; l = re_snprintf(&(debug_buf[0]), 2047, "%H", ua_print_status, ua); if (l != -1) { debug_buf[l] = '\0'; LOGD("%s\n", debug_buf); } } #endif static struct re_printf pf_null = {vprintf_null, 0}; static void signal_handler(int sig) { static bool term = false; if (term) { module_app_unload(); mod_close(); exit(0); } term = true; LOGI("terminated by signal (%d)\n", sig); ua_stop_all(false); } static void ua_exit_handler(void *arg) { (void)arg; LOGD("ua exited -- stopping main runloop\n"); re_cancel(); } static const char *bevent_reg_str(enum bevent_ev ev) { switch (ev) { case BEVENT_REGISTERING: return "registering"; case BEVENT_REGISTER_OK: case BEVENT_FALLBACK_OK: return "registered"; case BEVENT_REGISTER_FAIL: case BEVENT_FALLBACK_FAIL: return "registering failed"; case BEVENT_UNREGISTERING: return "unregistering"; default: return "?"; } } static const char *translate_errorcode(uint16_t scode) { switch (scode) { case 404: return ""; /* ignore */ case 486: case 603: return "busy"; case 487: return ""; /* ignore */ default: return "error"; } } static void event_handler(enum bevent_ev ev, struct bevent *event, void *arg) { (void)arg; const char *prm = bevent_get_text(event); struct call *call = bevent_get_call(event); struct ua *ua = bevent_get_ua(event); const struct sip_msg *msg = bevent_get_msg(event); struct account *acc = ua_account(bevent_get_ua(event)); const char *tone; char event_buf[256]; enum sdp_dir ardir; int len, err; struct pl module, module_event, data; LOGD("ua event (%s) %s\n", bevent_str(ev), prm); switch (ev) { case BEVENT_CREATE: len = re_snprintf(event_buf, sizeof event_buf, "create", ""); break; case BEVENT_REGISTERING: case BEVENT_UNREGISTERING: case BEVENT_REGISTER_OK: case BEVENT_FALLBACK_OK: len = re_snprintf(event_buf, sizeof event_buf, "%s", bevent_reg_str(ev)); break; case BEVENT_REGISTER_FAIL: case BEVENT_FALLBACK_FAIL: len = re_snprintf(event_buf, sizeof event_buf, "registering failed,%s", prm); break; case BEVENT_SIPSESS_CONN: ua = uag_find_msg(msg); // There is no call yet and call is thus used to hold SIP message call = (struct call *)msg; len = re_snprintf(event_buf, sizeof event_buf, "%s,%r,%ld", prm, &msg->from.auri, (long)event); break; case BEVENT_CALL_INCOMING: len = re_snprintf(event_buf, sizeof event_buf, "call incoming,%s", prm); break; case BEVENT_CALL_OUTGOING: len = re_snprintf(event_buf, sizeof event_buf, "call outgoing", ""); break; case BEVENT_CALL_ANSWERED: len = re_snprintf(event_buf, sizeof event_buf, "call answered", ""); break; case BEVENT_CALL_REDIRECT: len = re_snprintf(event_buf, sizeof event_buf, "call redirect,%s", prm + 4); break; case BEVENT_CALL_LOCAL_SDP: if (strcmp(prm, "offer") == 0) return; len = re_snprintf(event_buf, sizeof event_buf, "call %sed", prm); break; case BEVENT_CALL_RINGING: len = re_snprintf(event_buf, sizeof event_buf, "call ringing", ""); break; case BEVENT_CALL_PROGRESS: ardir = sdp_media_rdir(stream_sdpmedia(audio_strm(call_audio(call)))); len = re_snprintf(event_buf, sizeof event_buf, "call progress,%d", ardir); break; case BEVENT_CALL_ESTABLISHED: len = re_snprintf(event_buf, sizeof event_buf, "call established", ""); break; case BEVENT_CALL_REMOTE_SDP: ardir = sdp_media_rdir(stream_sdpmedia(audio_strm(call_audio(call)))); len = re_snprintf(event_buf, sizeof event_buf, "call update,%d,%ld", ardir, (long)event); break; case BEVENT_CALL_MENC: if (prm[0] == '0') len = re_snprintf(event_buf, sizeof event_buf, "call secure", ""); else if (prm[0] == '1') len = re_snprintf(event_buf, sizeof event_buf, "call verify,%s", prm + 2); else if (prm[0] == '2') len = re_snprintf(event_buf, sizeof event_buf, "call verified,%s", prm + 2); else len = re_snprintf(event_buf, sizeof event_buf, "unknown menc event", ""); break; case BEVENT_CALL_TRANSFER: len = re_snprintf(event_buf, sizeof event_buf, "call transfer,%s", prm); break; case BEVENT_CALL_TRANSFER_FAILED: call_hold(call, false); len = re_snprintf(event_buf, sizeof event_buf, "transfer failed,%s", prm); break; case BEVENT_CALL_CLOSED: tone = call_scode(call) ? translate_errorcode(call_scode(call)) : ""; len = re_snprintf(event_buf, sizeof event_buf, "call closed,%s,%s", prm, tone); break; case BEVENT_MWI_NOTIFY: len = re_snprintf(event_buf, sizeof event_buf, "mwi notify,%s", prm); break; case BEVENT_MODULE: err = re_regex(prm, strlen(prm), "[^,]*,[^,]*,[~]*", &module, &module_event, &data); if (err) return; if (!pl_strcmp(&module_event, "dump")) { len = re_snprintf(event_buf, sizeof event_buf, "sndfile dump,%r", &data); break; } if (!pl_strcmp(&module_event, "recorder sessionid")) { len = re_snprintf(event_buf, sizeof event_buf, "recorder sessionid,%r", &data); break; } default: return; } if (len == -1) { LOGE("failed to print event to buffer\n"); return; } JavaVM *javaVM = g_ctx.javaVM; JNIEnv *env; jint res = (*javaVM)->GetEnv(javaVM, (void**)&env, JNI_VERSION_1_6); if (res != JNI_OK) { LOGD("failed to get javaVM environment, ErrorCode = %d\n", res); res = (*javaVM)->AttachCurrentThread(javaVM, &env, NULL); if (JNI_OK != res) { LOGE("failed to AttachCurrentThread, ErrorCode = %d\n", res); return; } } jmethodID methodId = (*env)->GetMethodID(env, g_ctx.mainActivityClz, "uaEvent", "(Ljava/lang/String;JJ)V"); jstring jEvent = (*env)->NewStringUTF(env, event_buf); LOGD("sending ua/call %ld/%ld event %s\n", (long)ua, (long)call, event_buf); (*env)->CallVoidMethod(env, g_ctx.mainActivityObj, methodId, jEvent, (jlong)ua, (jlong)call); (*env)->DeleteLocalRef(env, jEvent); } static void message_handler( struct ua *ua, const struct pl *peer, const struct pl *ctype, struct mbuf *body, void *arg) { (void)arg; char ctype_buf[128]; char peer_buf[256]; size_t size; if (snprintf(peer_buf, 256, "%.*s", (int)peer->l, peer->p) >= 256) { LOGE("message peer is too long (max 255 characters)\n"); return; } JavaVM *javaVM = g_ctx.javaVM; JNIEnv *env; jint res = (*javaVM)->GetEnv(javaVM, (void **)&env, JNI_VERSION_1_6); if (res != JNI_OK) { res = (*javaVM)->AttachCurrentThread(javaVM, &env, NULL); if (JNI_OK != res) { LOGE("failed to AttachCurrentThread, ErrorCode = %d\n", res); return; } } jmethodID methodId = (*env)->GetMethodID(env, g_ctx.mainActivityClz, "messageEvent", "(JLjava/lang/String;Ljava/lang/String;[B)V"); jstring jPeer = (*env)->NewStringUTF(env, peer_buf); pl_strcpy(ctype, ctype_buf, 256); jstring jCtype = (*env)->NewStringUTF(env, ctype_buf); jbyteArray jMsg; size = mbuf_get_left(body); jMsg = (*env)->NewByteArray(env, (jsize)size); if ((*env)->GetArrayLength(env, jMsg) != size) { (*env)->DeleteLocalRef(env, jMsg); jMsg = (*env)->NewByteArray(env, (jsize)size); } void *temp = (*env)->GetPrimitiveArrayCritical(env, (jarray)jMsg, 0); memcpy(temp, mbuf_buf(body), size); (*env)->ReleasePrimitiveArrayCritical(env, jMsg, temp, 0); LOGD("sending message %ld/%s/%s/%.*s\n", (long)ua, peer_buf, ctype_buf, (int)size, mbuf_buf(body)); (*env)->CallVoidMethod(env, g_ctx.mainActivityObj, methodId, (jlong)ua, jPeer, jCtype, jMsg); (*env)->DeleteLocalRef(env, jCtype); (*env)->DeleteLocalRef(env, jPeer); (*env)->DeleteLocalRef(env, jMsg); } static void send_resp_handler(int err, const struct sip_msg *msg, void *arg) { (void)arg; char reason_buf[64]; if (err) { LOGD("send_response_handler received error %d\n", err); return; } pl_strcpy(&(msg->reason), reason_buf, 64); LOGD("send_response_handler received response '%u %s' at %s\n", msg->scode, reason_buf, (char *)arg); JavaVM *javaVM = g_ctx.javaVM; JNIEnv *env; jint res = (*javaVM)->GetEnv(javaVM, (void **)&env, JNI_VERSION_1_6); if (res != JNI_OK) { res = (*javaVM)->AttachCurrentThread(javaVM, &env, NULL); if (JNI_OK != res) { LOGE("failed to AttachCurrentThread, ErrorCode = %d\n", res); return; } } jmethodID methodId = (*env)->GetMethodID(env, g_ctx.mainActivityClz, "messageResponse", "(ILjava/lang/String;Ljava/lang/String;)V"); jstring javaReason = (*env)->NewStringUTF(env, reason_buf); jstring javaTime = (*env)->NewStringUTF(env, (char *)arg); (*env)->CallVoidMethod(env, g_ctx.mainActivityObj, methodId, msg->scode, javaReason, javaTime); (*env)->DeleteLocalRef(env, javaReason); (*env)->DeleteLocalRef(env, javaTime); } enum { ID_UA_STOP_ALL }; static struct mqueue *mq; static void mqueue_handler(int id, void *data, void *arg) { (void)arg; if (id == ID_UA_STOP_ALL) { LOGD("calling ua_stop_all with force %u\n", (unsigned)(uintptr_t)data); ua_stop_all((bool)(uintptr_t)data); } } #include static int pfd[2]; static pthread_t loggingThread; static void *loggingFunction(void *arg) { (void)arg; ssize_t readSize; char buf[128]; while ((readSize = read(pfd[0], buf, sizeof buf - 1)) > 0) { if (buf[readSize - 1] == '\n') { --readSize; } buf[readSize] = 0; LOGD("%s", buf); } return 0; } static int runLoggingThread() { setvbuf(stdout, 0, _IOLBF, 0); setvbuf(stderr, 0, _IONBF, 0); pipe(pfd); dup2(pfd[1], 1); dup2(pfd[1], 2); int ret = pthread_create(&loggingThread, NULL, loggingFunction, NULL); if (ret != 0) { LOGE("failed to create logging thread: %d", ret); return ret; } pthread_detach(loggingThread); return 0; } JNIEXPORT jint JNICALL JNI_OnLoad(JavaVM *vm, void *reserved) { (void)reserved; LOGD("at JNI_OnLoad\n"); memset(&g_ctx, 0, sizeof(g_ctx)); g_ctx.javaVM = vm; g_ctx.mainActivityClz = NULL; g_ctx.mainActivityObj = NULL; return JNI_VERSION_1_6; } JNIEXPORT void JNICALL Java_com_tutpro_baresip_BaresipService_baresipStart( JNIEnv *env, jobject instance, jstring jPath, jstring jAddrs, jint jLogLevel, jstring jSoftware) { LOGI("starting baresip\n"); int err; char start_error[64] = ""; JavaVM *javaVM = g_ctx.javaVM; jclass clz = (*env)->GetObjectClass(env, instance); g_ctx.mainActivityClz = (*env)->NewGlobalRef(env, clz); g_ctx.mainActivityObj = (*env)->NewGlobalRef(env, instance); const char *path = (*env)->GetStringUTFChars(env, jPath, 0); const char *addrs = (*env)->GetStringUTFChars(env, jAddrs, 0); const char *software = (*env)->GetStringUTFChars(env, jSoftware, 0); runLoggingThread(); err = libre_init(); if (err) { LOGE("failed to init libre"); goto out; } if (re_thread_check(true) == 0) { LOGI("attaching to re thread\n"); jint res = (*javaVM)->AttachCurrentThread(javaVM, &env, NULL); if (JNI_OK != res) { LOGE("failed to AttachCurrentThread: %d\n", res); goto out; } } else { LOGE("not on re thread\n"); goto out; } conf_path_set(path); log_level_set((enum log_level)jLogLevel); err = conf_configure(); if (err) { LOGW("conf_configure() failed: (%d)\n", err); strcpy(start_error, "conf_configure"); goto out; } re_thread_async_init(ASYNC_WORKERS); err = baresip_init(conf_config()); if (err) { LOGW("baresip_init() failed (%d)\n", err); strcpy(start_error, "baresip_init"); goto out; } // Turn off DNS client cache (should be OK with async workers, but it not) dnsc_cache_max(net_dnsc(baresip_network()), 0); if (strlen(addrs) > 0) { char *addr_list = (char *)malloc(strlen(addrs) + 1); struct sa temp_sa; char buf[256]; net_flush_addresses(baresip_network()); strcpy(addr_list, addrs); char *ptr = strtok(addr_list, ";"); while (ptr != NULL) { if (0 == sa_set_str(&temp_sa, ptr, 0)) { sa_ntop(&temp_sa, buf, 256); ptr = strtok(NULL, ";"); net_add_address_ifname(baresip_network(), &temp_sa, ptr); } else { LOGE("invalid ip address (%s)\n", ptr); ptr = strtok(NULL, ";"); } *(ptr - 1) = ';'; ptr = strtok(NULL, ";"); } free(addr_list); } // net_debug_log(); err = ua_init(software, true, true, true); if (err) { LOGE("ua_init() failed (%d)\n", err); strcpy(start_error, "ua_init"); goto out; } uag_set_exit_handler(ua_exit_handler, NULL); err = bevent_register(event_handler, NULL); if (err) { LOGE("bevent_register() failed (%d)\n", err); strcpy(start_error, "bevent_register"); goto out; } err = message_listen(baresip_message(), message_handler, NULL); if (err) { LOGE("message_listen() failed (%d)\n", err); strcpy(start_error, "message_listen"); goto out; } err = conf_modules(); if (err) { LOGW("conf_modules() failed (%d)\n", err); strcpy(start_error, "conf_modules"); goto out; } err = mqueue_alloc(&mq, mqueue_handler, NULL); if (err) { LOGW("mqueue_alloc failed (%d)\n", err); strcpy(start_error, "mqueue_alloc"); goto out; } // no need to call re_leave/enter since main is not running yet jmethodID startedId = (*env)->GetMethodID(env, g_ctx.mainActivityClz, "started", "()V"); (*env)->CallVoidMethod(env, g_ctx.mainActivityObj, startedId); LOGI("running main loop ...\n"); err = re_main(signal_handler); out: if (err) { LOGE("stopping UAs due to error: (%d)\n", err); ua_stop_all(true); } else { LOGI("main loop exit\n"); } mq = mem_deref(mq); LOGD("closing ..."); ua_close(); module_app_unload(); conf_close(); baresip_close(); bevent_unregister(event_handler); LOGD("unloading modules ..."); mod_close(); LOGD("closing re thread\n"); re_thread_async_close(); LOGD("closing libre\n"); libre_close(); // tmr_debug(); // mem_debug(); LOGD("sending stopped event"); jstring javaError = (*env)->NewStringUTF(env, start_error); jmethodID stoppedId = (*env)->GetMethodID(env, g_ctx.mainActivityClz, "stopped", "(Ljava/lang/String;)V"); (*env)->CallVoidMethod(env, g_ctx.mainActivityObj, stoppedId, javaError); (*env)->DeleteLocalRef(env, javaError); (*env)->ReleaseStringUTFChars(env, jPath, path); (*env)->ReleaseStringUTFChars(env, jAddrs, addrs); (*env)->ReleaseStringUTFChars(env, jSoftware, software); } JNIEXPORT void JNICALL Java_com_tutpro_baresip_BaresipService_baresipStop( JNIEnv *env, jobject obj, jboolean force) { (void)env; (void)obj; LOGD("ua_stop_all upon baresipStop"); mqueue_push(mq, ID_UA_STOP_ALL, (void *)((long)force)); } JNIEXPORT jstring JNICALL Java_com_tutpro_baresip_Api_account_1display_1name( JNIEnv *env, jobject obj, jlong acc) { (void)obj; if (acc) { const char *dn = account_display_name((struct account *)acc); if (dn) return (*env)->NewStringUTF(env, dn); else return (*env)->NewStringUTF(env, ""); } else return (*env)->NewStringUTF(env, ""); } JNIEXPORT jint JNICALL Java_com_tutpro_baresip_Api_account_1set_1display_1name( JNIEnv *env, jobject obj, jlong acc, jstring jDn) { (void)obj; const char *dn = (*env)->GetStringUTFChars(env, jDn, 0); int res; if (strlen(dn) > 0) res = account_set_display_name((struct account *)acc, dn); else res = account_set_display_name((struct account *)acc, NULL); (*env)->ReleaseStringUTFChars(env, jDn, dn); return res; } JNIEXPORT jstring JNICALL Java_com_tutpro_baresip_Api_account_1aor( JNIEnv *env, jobject obj, jlong acc) { (void)obj; if (acc) return (*env)->NewStringUTF(env, account_aor((struct account *)acc)); else return (*env)->NewStringUTF(env, ""); } JNIEXPORT jstring JNICALL Java_com_tutpro_baresip_Api_account_1luri( JNIEnv *env, jobject obj, jlong acc) { (void)obj; const struct uri *uri = account_luri((struct account *)acc); char uri_buf[512]; int l; l = re_snprintf(&(uri_buf[0]), 511, "%H", uri_encode, uri); if (l != -1) uri_buf[l] = '\0'; else uri_buf[0] = '\0'; return (*env)->NewStringUTF(env, uri_buf); } JNIEXPORT jstring JNICALL Java_com_tutpro_baresip_Api_account_1auth_1user( JNIEnv *env, jobject obj, jlong acc) { (void)obj; if (acc) { const char *au = account_auth_user((struct account *)acc); if (au) return (*env)->NewStringUTF(env, au); else return (*env)->NewStringUTF(env, ""); } else return (*env)->NewStringUTF(env, ""); } JNIEXPORT jint JNICALL Java_com_tutpro_baresip_Api_account_1set_1auth_1user( JNIEnv *env, jobject obj, jlong acc, jstring jUser) { (void)obj; const char *user = (*env)->GetStringUTFChars(env, jUser, 0); int res; if (strlen(user) > 0) res = account_set_auth_user((struct account *)acc, user); else res = account_set_auth_user((struct account *)acc, NULL); (*env)->ReleaseStringUTFChars(env, jUser, user); return res; } JNIEXPORT jstring JNICALL Java_com_tutpro_baresip_Api_account_1auth_1pass( JNIEnv *env, jobject obj, jlong acc) { (void)obj; if (acc) { const char *ap = account_auth_pass((struct account *)acc); if (ap) return (*env)->NewStringUTF(env, ap); else return (*env)->NewStringUTF(env, ""); } else return (*env)->NewStringUTF(env, ""); } JNIEXPORT jint JNICALL Java_com_tutpro_baresip_Api_account_1set_1auth_1pass( JNIEnv *env, jobject obj, jlong acc, jstring jPass) { (void)obj; const char *pass = (*env)->GetStringUTFChars(env, jPass, 0); int res; if (strlen(pass) > 0) res = account_set_auth_pass((struct account *)acc, pass); else res = account_set_auth_pass((struct account *)acc, NULL); (*env)->ReleaseStringUTFChars(env, jPass, pass); return res; } JNIEXPORT jstring JNICALL Java_com_tutpro_baresip_Api_account_1outbound( JNIEnv *env, jobject obj, jlong acc, jint jIx) { (void)obj; const uint16_t native_ix = jIx; const char *outbound; if (acc) { outbound = account_outbound((struct account *)acc, native_ix); if (outbound) return (*env)->NewStringUTF(env, outbound); else return (*env)->NewStringUTF(env, ""); } else { return (*env)->NewStringUTF(env, ""); } } JNIEXPORT jint JNICALL Java_com_tutpro_baresip_Api_account_1set_1outbound( JNIEnv *env, jobject obj, jlong acc, jstring jOb, jint jIx) { (void)obj; const char *ob = (*env)->GetStringUTFChars(env, jOb, 0); const uint16_t ix = jIx; int res; if (strlen(ob) > 0) res = account_set_outbound((struct account *)acc, ob, ix); else res = account_set_outbound((struct account *)acc, NULL, ix); (*env)->ReleaseStringUTFChars(env, jOb, ob); return res; } JNIEXPORT jstring JNICALL Java_com_tutpro_baresip_Api_account_1audio_1codec( JNIEnv *env, jobject obj, jlong acc, jint ix) { (void)obj; const struct list *codecl; char codec_buf[32]; int len; struct le *le; codec_buf[0] = '\0'; if (acc) { codecl = account_aucodecl((struct account *)acc); if (!list_isempty(codecl)) { int i = -1; for (le = list_head(codecl); le != NULL; le = le->next) { i++; if (i == ix) { const struct aucodec *ac = le->data; len = re_snprintf( codec_buf, sizeof codec_buf, "%s/%u/%u", ac->name, ac->srate, ac->ch); if (len == -1) { LOGE("failed to print audio codec to buffer\n"); codec_buf[0] = '\0'; } break; } } } } return (*env)->NewStringUTF(env, codec_buf); } JNIEXPORT jint JNICALL Java_com_tutpro_baresip_Api_account_1set_1audio_1codecs( JNIEnv *env, jobject obj, jlong acc, jstring jCodecs) { (void)obj; const char *codecs = (*env)->GetStringUTFChars(env, jCodecs, 0); int res = account_set_audio_codecs((struct account *)acc, codecs); (*env)->ReleaseStringUTFChars(env, jCodecs, codecs); return res; } JNIEXPORT jint JNICALL Java_com_tutpro_baresip_Api_account_1set_1video_1codecs( JNIEnv *env, jobject obj, jlong acc, jstring jCodecs) { (void)obj; const char *codecs = (*env)->GetStringUTFChars(env, jCodecs, 0); int res = account_set_video_codecs((struct account *)acc, codecs); (*env)->ReleaseStringUTFChars(env, jCodecs, codecs); return res; } JNIEXPORT jint JNICALL Java_com_tutpro_baresip_Api_account_1regint( JNIEnv *env, jobject obj, jlong acc) { (void)env; (void)obj; if (acc) return (jint)account_regint((struct account *)acc); else return 0; } JNIEXPORT jint JNICALL Java_com_tutpro_baresip_Api_account_1set_1regint( JNIEnv *env, jobject obj, jlong acc, jint jRegint) { (void)env; (void)obj; const uint32_t regint = (uint32_t)jRegint; return account_set_regint((struct account *)acc, regint); } JNIEXPORT jboolean JNICALL Java_com_tutpro_baresip_Api_account_1check_1origin( JNIEnv *env, jobject obj, jlong acc) { (void)env; (void)obj; return account_check_origin((struct account *)acc); } JNIEXPORT void JNICALL Java_com_tutpro_baresip_Api_account_1set_1check_1origin( JNIEnv *env, jobject obj, jlong acc, jboolean value) { (void)env; (void)obj; account_set_check_origin((struct account *)acc, value); } JNIEXPORT jstring JNICALL Java_com_tutpro_baresip_Api_account_1mediaenc( JNIEnv *env, jobject obj, jlong acc) { (void)obj; if (acc) { const char *mediaenc = account_mediaenc((struct account *)acc); if (mediaenc) return (*env)->NewStringUTF(env, mediaenc); } return (*env)->NewStringUTF(env, ""); } JNIEXPORT jint JNICALL Java_com_tutpro_baresip_Api_account_1set_1mediaenc( JNIEnv *env, jobject obj, jlong acc, jstring jMencid) { (void)obj; const char *mencid = (*env)->GetStringUTFChars(env, jMencid, 0); int res; if (strlen(mencid) > 0) res = account_set_mediaenc((struct account *)acc, mencid); else res = account_set_mediaenc((struct account *)acc, NULL); (*env)->ReleaseStringUTFChars(env, jMencid, mencid); return res; } JNIEXPORT jstring JNICALL Java_com_tutpro_baresip_Api_account_1medianat( JNIEnv *env, jobject obj, jlong acc) { (void)obj; if (acc) { const char *medianat = account_medianat((struct account *)acc); if (medianat) return (*env)->NewStringUTF(env, medianat); } return (*env)->NewStringUTF(env, ""); } JNIEXPORT jint JNICALL Java_com_tutpro_baresip_Api_account_1set_1medianat( JNIEnv *env, jobject obj, jlong acc, jstring jMedNat) { (void)obj; const char *mednat = (*env)->GetStringUTFChars(env, jMedNat, 0); int res; if (strlen(mednat) > 0) res = account_set_medianat((struct account *)acc, mednat); else res = account_set_medianat((struct account *)acc, NULL); (*env)->ReleaseStringUTFChars(env, jMedNat, mednat); return res; } JNIEXPORT jint JNICALL Java_com_tutpro_baresip_Api_account_1set_1sipnat( JNIEnv *env, jobject obj, jlong acc, jstring jSipNat) { (void)obj; const char *sipnat = (*env)->GetStringUTFChars(env, jSipNat, 0); int res; if (strlen(sipnat) > 0) res = account_set_sipnat((struct account *)acc, sipnat); else res = account_set_sipnat((struct account *)acc, NULL); (*env)->ReleaseStringUTFChars(env, jSipNat, sipnat); return res; } JNIEXPORT jstring JNICALL Java_com_tutpro_baresip_Api_account_1stun_1uri( JNIEnv *env, jobject obj, jlong acc) { (void)obj; if (acc) { const struct stun_uri *stun_uri = account_stun_uri((struct account *)acc); if (stun_uri) { char uri_str[256]; if (stun_uri->port != 0) { if (stun_uri->proto == IPPROTO_TCP) sprintf(uri_str, "%s:%s:%d?transport=tcp", stunuri_scheme_name(stun_uri->scheme), stun_uri->host, stun_uri->port); else sprintf(uri_str, "%s:%s:%d", stunuri_scheme_name(stun_uri->scheme), stun_uri->host, stun_uri->port); } else { if (stun_uri->proto == IPPROTO_TCP) sprintf(uri_str, "%s:%s?transport=tcp", stunuri_scheme_name(stun_uri->scheme), stun_uri->host); else sprintf(uri_str, "%s:%s", stunuri_scheme_name(stun_uri->scheme), stun_uri->host); } return (*env)->NewStringUTF(env, uri_str); } } return (*env)->NewStringUTF(env, ""); } JNIEXPORT jint JNICALL Java_com_tutpro_baresip_Api_account_1set_1stun_1uri( JNIEnv *env, jobject obj, jlong acc, jstring jUri) { (void)obj; const char *uri = (*env)->GetStringUTFChars(env, jUri, 0); int res; if (strlen(uri) > 0) res = account_set_stun_uri((struct account *)acc, uri); else res = account_set_stun_uri((struct account *)acc, NULL); (*env)->ReleaseStringUTFChars(env, jUri, uri); return res; } JNIEXPORT jstring JNICALL Java_com_tutpro_baresip_Api_account_1stun_1user( JNIEnv *env, jobject obj, jlong acc) { (void)obj; if (acc) { const char *stun_user = account_stun_user((struct account *)acc); if (stun_user) return (*env)->NewStringUTF(env, stun_user); } return (*env)->NewStringUTF(env, ""); } JNIEXPORT jint JNICALL Java_com_tutpro_baresip_Api_account_1set_1stun_1user( JNIEnv *env, jobject obj, jlong acc, jstring jUser) { (void)obj; const char *user = (*env)->GetStringUTFChars(env, jUser, 0); int res; if (strlen(user) > 0) res = account_set_stun_user((struct account *)acc, user); else res = account_set_stun_user((struct account *)acc, NULL); (*env)->ReleaseStringUTFChars(env, jUser, user); return res; } JNIEXPORT jstring JNICALL Java_com_tutpro_baresip_Api_account_1stun_1pass( JNIEnv *env, jobject obj, jlong acc) { (void)obj; if (acc) { const char *stun_pass = account_stun_pass((struct account *)acc); if (stun_pass) return (*env)->NewStringUTF(env, stun_pass); } return (*env)->NewStringUTF(env, ""); } JNIEXPORT jint JNICALL Java_com_tutpro_baresip_Api_account_1set_1stun_1pass( JNIEnv *env, jobject obj, jlong acc, jstring jPass) { (void)obj; const char *pass = (*env)->GetStringUTFChars(env, jPass, 0); int res; if (strlen(pass) > 0) res = account_set_stun_pass((struct account *)acc, pass); else res = account_set_stun_pass((struct account *)acc, NULL); (*env)->ReleaseStringUTFChars(env, jPass, pass); return res; } JNIEXPORT jint JNICALL Java_com_tutpro_baresip_Api_account_1set_1mwi( JNIEnv *env, jobject obj, jlong acc, jboolean value) { (void)env; (void)obj; return account_set_mwi((struct account *)acc, value); } JNIEXPORT jstring JNICALL Java_com_tutpro_baresip_Api_account_1vm_1uri( JNIEnv *env, jobject obj, jlong acc) { (void)obj; char uri_buf[256]; if (acc) { struct pl pl; const struct sip_addr *addr = account_laddr((struct account *)acc); int err = msg_param_decode(&(addr->params), "vm_uri", &pl); if (err) { return (*env)->NewStringUTF(env, ""); } else { pl_strcpy(&pl, uri_buf, 256); return (*env)->NewStringUTF(env, uri_buf); } } else return (*env)->NewStringUTF(env, ""); } JNIEXPORT jint JNICALL Java_com_tutpro_baresip_Api_account_1answermode( JNIEnv *env, jobject obj, jlong acc) { (void)env; (void)obj; return account_answermode((struct account *)acc); } JNIEXPORT jint JNICALL Java_com_tutpro_baresip_Api_account_1set_1answermode( JNIEnv *env, jobject obj, jlong acc, jint jMode) { (void)env; (void)obj; const uint32_t mode = (uint32_t)jMode; return account_set_answermode((struct account *)acc, mode); } JNIEXPORT jboolean JNICALL Java_com_tutpro_baresip_Api_account_1sip_1autoredirect( JNIEnv *env, jobject obj, jlong acc) { (void)env; (void)obj; return account_sip_autoredirect((struct account *)acc); } JNIEXPORT void JNICALL Java_com_tutpro_baresip_Api_account_1set_1sip_1autoredirect( JNIEnv *env, jobject obj, jlong acc, jboolean allow) { (void)env; (void)obj; return account_set_sip_autoredirect((struct account *)acc, allow); } JNIEXPORT jboolean JNICALL Java_com_tutpro_baresip_Api_account_1rtcp_1mux( JNIEnv *env, jobject obj, jlong acc) { (void)env; (void)obj; return account_rtcp_mux((struct account *)acc); } JNIEXPORT jint JNICALL Java_com_tutpro_baresip_Api_account_1set_1rtcp_1mux( JNIEnv *env, jobject obj, jlong acc, jboolean value) { (void)env; (void)obj; return account_set_rtcp_mux((struct account *)acc, value); } JNIEXPORT jint JNICALL Java_com_tutpro_baresip_Api_account_1rel100_1mode( JNIEnv *env, jobject obj, jlong acc) { (void)env; (void)obj; return account_rel100_mode((struct account *)acc); } JNIEXPORT jint JNICALL Java_com_tutpro_baresip_Api_account_1set_1rel100_1mode( JNIEnv *env, jobject obj, jlong acc, jint jMode) { (void)env; (void)obj; const uint32_t mode = (uint32_t)jMode; return account_set_rel100_mode((struct account *)acc, mode); } JNIEXPORT jint JNICALL Java_com_tutpro_baresip_Api_account_1dtmfmode( JNIEnv *env, jobject obj, jlong acc) { (void)env; (void)obj; return account_dtmfmode((struct account *)acc); } JNIEXPORT jint JNICALL Java_com_tutpro_baresip_Api_account_1set_1dtmfmode( JNIEnv *env, jobject obj, jlong acc, jint jMode) { (void)env; (void)obj; const uint32_t mode = (uint32_t)jMode; return account_set_dtmfmode((struct account *)acc, mode); } JNIEXPORT jstring JNICALL Java_com_tutpro_baresip_Api_account_1extra( JNIEnv *env, jobject obj, jlong acc) { (void)obj; if (acc) { const char *extra = account_extra((struct account *)acc); if (extra) return (*env)->NewStringUTF(env, extra); } return (*env)->NewStringUTF(env, ""); } JNIEXPORT void JNICALL Java_com_tutpro_baresip_Api_account_1debug( JNIEnv *env, jobject obj, jlong acc) { (void)env; (void)obj; account_debug_log((struct account *)acc); } JNIEXPORT jlong JNICALL Java_com_tutpro_baresip_Api_ua_1alloc( JNIEnv *env, jobject obj, jstring jUri) { (void)obj; const char *uri = (*env)->GetStringUTFChars(env, jUri, 0); struct ua *ua; LOGD("allocating UA '%s'\n", uri); re_thread_enter(); int res = ua_alloc(&ua, uri); re_thread_leave(); if (res == 0) { LOGD("allocated ua '%ld'\n", (long)ua); } else { LOGE("failed to allocate ua '%s'\n", uri); } (*env)->ReleaseStringUTFChars(env, jUri, uri); return (jlong)ua; } JNIEXPORT jint JNICALL Java_com_tutpro_baresip_Api_ua_1register(JNIEnv *env, jobject obj, jlong ua) { (void)env; (void)obj; LOGD("registering UA '%ld'\n", (long)ua); re_thread_enter(); int res = ua_register((struct ua *)ua); re_thread_leave(); return res; } JNIEXPORT void JNICALL Java_com_tutpro_baresip_Api_ua_1unregister( JNIEnv *env, jobject obj, jlong ua) { (void)env; (void)obj; re_thread_enter(); ua_unregister((struct ua *)ua); re_thread_leave(); } JNIEXPORT jboolean JNICALL Java_com_tutpro_baresip_Api_ua_1isregistered( JNIEnv *env, jobject obj, jlong ua) { (void)env; (void)obj; return ua_isregistered((struct ua *)ua) ? true : false; } JNIEXPORT void JNICALL Java_com_tutpro_baresip_Api_ua_1destroy(JNIEnv *env, jobject obj, jlong ua) { (void)env; (void)obj; LOGD("destroying ua %ld\n", (long)ua); re_thread_enter(); (void)ua_destroy((struct ua *)ua); re_thread_leave(); } JNIEXPORT jlong JNICALL Java_com_tutpro_baresip_Api_ua_1account(JNIEnv *env, jobject obj, jlong ua) { (void)env; (void)obj; struct account *acc = 0; if (ua) acc = ua_account((struct ua *)ua); return (jlong)acc; } JNIEXPORT jlong JNICALL Java_com_tutpro_baresip_Api_ua_1update_1account( JNIEnv *env, jobject obj, jlong ua) { (void)env; (void)obj; LOGD("updating account of ua %ld\n", (long)ua); return ua_update_account((struct ua *)ua); } JNIEXPORT void JNICALL Java_com_tutpro_baresip_Api_ua_1hangup( JNIEnv *env, jobject obj, jlong ua, jlong call, jint code, jstring reason) { (void)obj; const uint16_t native_code = code; const char *native_reason = (*env)->GetStringUTFChars(env, reason, 0); LOGD("hanging up call %ld/%ld\n", (long)ua, (long)call); re_thread_enter(); if (strlen(native_reason) == 0) ua_hangup((struct ua *)ua, (struct call *)call, native_code, NULL); else ua_hangup((struct ua *)ua, (struct call *)call, native_code, native_reason); re_thread_leave(); (*env)->ReleaseStringUTFChars(env, reason, native_reason); } JNIEXPORT void JNICALL Java_com_tutpro_baresip_Api_ua_1accept( JNIEnv *env, jobject obj, jlong ua, jlong msg) { (void)env; (void)obj; int err; char *from_uri; pl_strdup(&from_uri, &((struct sip_msg *)msg)->from.auri); LOGD("accepting incoming call for ua %ld from %s\n", (long)ua, from_uri); mem_deref(from_uri); re_thread_enter(); err = ua_accept((struct ua *)ua, (struct sip_msg *)msg); re_thread_leave(); if (err) LOGW("accepting incoming call for ua %ld failed with error %d\n", (long)ua, err); } JNIEXPORT jlong JNICALL Java_com_tutpro_baresip_Api_ua_1call_1alloc( JNIEnv *env, jobject obj, jlong ua, jlong xCall, jint vidMode) { (void)env; (void)obj; struct call *call = NULL; int err; LOGD("allocating new call for ua %ld xcall %ld\n", (long)ua, (long)xCall); re_thread_enter(); err = ua_call_alloc(&call, (struct ua *)ua, (enum vidmode)vidMode, NULL, (struct call *)xCall, call_localuri((struct call *)xCall), true); re_thread_leave(); if (err) LOGW("call allocation for ua %ld failed with error %d\n", (long)ua, err); return (jlong)call; } JNIEXPORT void JNICALL Java_com_tutpro_baresip_Api_ua_1answer( JNIEnv *env, jobject obj, jlong ua, jlong call, jint vidMode) { (void)env; (void)obj; LOGD("answering ua/call %ld/%ld with video mode %d\n", (long)ua, (long)call, vidMode); re_thread_enter(); ua_answer((struct ua *)ua, (struct call *)call, (enum vidmode)vidMode); re_thread_leave(); } JNIEXPORT void JNICALL Java_com_tutpro_baresip_Api_ua_1add_1custom_1header( JNIEnv *env, jobject obj, jlong ua, jstring jname, jstring jbody) { (void)obj; const char *name = (*env)->GetStringUTFChars(env, jname, 0); const char *body = (*env)->GetStringUTFChars(env, jbody, 0); LOGD("adding header to %ld with name/body %s/%s\n", (long)ua, name, body); re_thread_enter(); struct pl pl_name, pl_body; pl_set_str(&pl_name, name); pl_set_str(&pl_body, body); int err = ua_add_custom_hdr((struct ua *)ua, &pl_name, &pl_body); re_thread_leave(); if (err) LOGW("adding custom header to ua %ld failed with error %d\n", (long)ua, err); (*env)->ReleaseStringUTFChars(env, jname, name); (*env)->ReleaseStringUTFChars(env, jbody, body); } JNIEXPORT void JNICALL Java_com_tutpro_baresip_Api_ua_1debug(JNIEnv *env, jobject obj, jlong ua) { (void)env; (void)obj; ua_debug_log((struct ua *)ua); } JNIEXPORT void JNICALL Java_com_tutpro_baresip_Api_sip_1treply( JNIEnv *env, jobject obj, jlong msg, jint code, jstring reason) { (void)obj; const uint16_t native_code = code; const char *native_reason = (*env)->GetStringUTFChars(env, reason, 0); LOGD("replying with %d/%s\n", native_code, native_reason); re_thread_enter(); (void)sip_treply(NULL, uag_sip(), (const struct sip_msg *)(struct msg *)msg, native_code, native_reason); re_thread_leave(); (*env)->ReleaseStringUTFChars(env, reason, native_reason); } JNIEXPORT void JNICALL Java_com_tutpro_baresip_Api_bevent_1stop( JNIEnv *env, jobject obj, jlong event) { (void)env; (void)obj; re_thread_enter(); bevent_stop((struct bevent *)event); re_thread_leave(); } JNIEXPORT void JNICALL Java_com_tutpro_baresip_Api_calls_1mute( JNIEnv *env, jobject obj, jboolean mute) { (void)env; (void)obj; struct le *ua_le; struct le *call_le; LOGD("muting calls %d\n", mute); re_thread_enter(); for (ua_le = list_head(uag_list()); ua_le != NULL; ua_le = ua_le->next) { const struct ua *ua = ua_le->data; for (call_le = list_head(ua_calls(ua)); call_le != NULL; call_le = call_le->next) { const struct call *call = call_le->data; audio_mute(call_audio(call), mute); } } re_thread_leave(); } JNIEXPORT jint JNICALL Java_com_tutpro_baresip_Api_call_1connect( JNIEnv *env, jobject obj, jlong call, jstring jPeer) { (void)obj; const char *native_peer = (*env)->GetStringUTFChars(env, jPeer, 0); LOGD("connecting call %ld to %s\n", (long)call, native_peer); re_thread_enter(); struct pl pl; pl_set_str(&pl, native_peer); int err = call_connect((struct call *)call, &pl); re_thread_leave(); if (err) LOGW("call_connect error: %d\n", err); (*env)->ReleaseStringUTFChars(env, jPeer, native_peer); return err; } JNIEXPORT void JNICALL Java_com_tutpro_baresip_Api_call_1notify_1sipfrag( JNIEnv *env, jobject obj, jlong call, jint code, jstring reason) { (void)obj; const uint16_t native_code = code; const char *native_reason = (*env)->GetStringUTFChars(env, reason, 0); LOGD("notifying call %ld/%s\n", (long)call, native_reason); re_thread_enter(); (void)call_notify_sipfrag((struct call *)call, native_code, native_reason); re_thread_leave(); (*env)->ReleaseStringUTFChars(env, reason, native_reason); } JNIEXPORT void JNICALL Java_com_tutpro_baresip_Api_call_1start_1audio( JNIEnv *env, jobject obj, jlong call) { (void)env; (void)obj; LOGD("starting audio of call %ld\n", (long)call); re_thread_enter(); struct audio *a = call_audio((struct call *)call); if (!audio_started(a)) audio_update(a); re_thread_leave(); } JNIEXPORT jboolean JNICALL Java_com_tutpro_baresip_Api_call_1hold( JNIEnv *env, jobject obj, jlong call, jboolean hold) { (void)env; (void)obj; int err; if (hold) { LOGD("holding call %ld\n", (long)call); re_thread_enter(); err = call_hold((struct call *)call, true); re_thread_leave(); } else { LOGD("resuming call %ld\n", (long)call); re_thread_enter(); err = call_hold((struct call *)call, false); re_thread_leave(); } if (err) LOGW("call_hold error: %d\n", err); return err == 0; } JNIEXPORT jboolean JNICALL Java_com_tutpro_baresip_Api_call_1ismuted( JNIEnv *env, jobject obj, jlong call) { (void)env; (void)obj; return audio_ismuted(call_audio((struct call *)call)); } JNIEXPORT jboolean JNICALL Java_com_tutpro_baresip_Api_call_1supported( JNIEnv *env, jobject obj, jlong call, jint tags) { (void)env; (void)obj; return call_supported((struct call *)call, tags); } JNIEXPORT jint JNICALL Java_com_tutpro_baresip_Api_call_1transfer( JNIEnv *env, jobject obj, jlong call, jstring jPeer) { (void)obj; const char *native_peer = (*env)->GetStringUTFChars(env, jPeer, 0); LOGD("transfering call %ld to %s\n", (long)call, native_peer); re_thread_enter(); int err = call_transfer((struct call *)call, native_peer); re_thread_leave(); if (err) LOGW("call_transfer error: %d\n", err); (*env)->ReleaseStringUTFChars(env, jPeer, native_peer); return err; } JNIEXPORT jint JNICALL Java_com_tutpro_baresip_Api_call_1send_1digit( JNIEnv *env, jobject obj, jlong call, jchar digit) { (void)env; (void)obj; const uint16_t native_digit = digit; LOGD("sending DTMF digit '%c' to call %ld\n", (char)native_digit, (long)call); re_thread_enter(); int res = call_send_digit((struct call *)call, (char)native_digit); if (!res) res = call_send_digit((struct call *)call, KEYCODE_REL); re_thread_leave(); return res; } JNIEXPORT jstring JNICALL Java_com_tutpro_baresip_Api_call_1audio_1codecs( JNIEnv *env, jobject obj, jlong call) { (void)obj; const struct aucodec *tx = audio_codec(call_audio((struct call *)call), true); const struct aucodec *rx = audio_codec(call_audio((struct call *)call), false); char codec_buf[256]; char *start = &(codec_buf[0]); unsigned int left = sizeof codec_buf; int len = -1; if (tx && rx) len = re_snprintf(start, left, "%s/%u/%u,%s/%u/%u", tx->name, tx->srate, tx->ch, rx->name, rx->srate, rx->ch); else { LOGE("failed to get audio codecs of call %ld\n", (long)call); len = re_snprintf(start, left, "%s/%u/%u,%s/%u/%u", '?', 0, 0, '?', 0, 0); } return (*env)->NewStringUTF(env, codec_buf); } JNIEXPORT jint JNICALL Java_com_tutpro_baresip_Api_call_1duration( JNIEnv *env, jobject obj, jlong call) { (void)env; (void)obj; return (jint)call_duration((struct call *)call); } JNIEXPORT jstring JNICALL Java_com_tutpro_baresip_Api_call_1stats( JNIEnv *env, jobject obj, jlong call, jstring jStream) { (void)obj; const char *native_stream = (*env)->GetStringUTFChars(env, jStream, 0); const struct stream *s; if (strcmp(native_stream, "audio") == 0) s = audio_strm(call_audio((struct call *)call)); else s = video_strm(call_video((struct call *)call)); const struct rtcp_stats *stats = stream_rtcp_stats(s); char stats_buf[256]; int len; if (stats) { const double tx_rate = 1.0 * stream_metric_get_tx_bitrate(s) / 1000.0; const double rx_rate = 1.0 * stream_metric_get_rx_bitrate(s) / 1000.0; const double tx_avg_rate = 1.0 * stream_metric_get_tx_avg_bitrate(s) / 1000.0; const double rx_avg_rate = 1.0 * stream_metric_get_rx_avg_bitrate(s) / 1000.0; len = re_snprintf(&(stats_buf[0]), 256, "%.1f/%.1f,%.1f/%.1f,%u/%u,%d/%d,%.1f/%.1f", tx_rate, rx_rate, tx_avg_rate, rx_avg_rate, stats->tx.sent, stats->rx.sent, stats->tx.lost, stats->rx.lost, 1.0 * stats->tx.jit / 1000, 1.0 * stats->rx.jit / 1000); if (len == -1) { LOGE("failed to get stats of call %ld %s stream\n", (long)call, native_stream); stats_buf[0] = '\0'; } } (*env)->ReleaseStringUTFChars(env, jStream, native_stream); return (*env)->NewStringUTF(env, stats_buf); } JNIEXPORT jint JNICALL Java_com_tutpro_baresip_Api_call_1state( JNIEnv *env, jobject obj, jlong call) { (void)env; (void)obj; return call_state((struct call *)call); } JNIEXPORT jboolean JNICALL Java_com_tutpro_baresip_Api_call_1replaces( JNIEnv *env, jobject obj, jlong call) { (void)env; (void)obj; return call_supported((struct call *)call, REPLACES) ? true : false; } JNIEXPORT jboolean JNICALL Java_com_tutpro_baresip_Api_call_1replace_1transfer( JNIEnv *env, jobject obj, jlong xferCall, jlong call) { (void)env; (void)obj; re_thread_enter(); int res = call_replace_transfer((struct call *)xferCall, (struct call *)call); re_thread_leave(); return res == 0 ? true : false; } JNIEXPORT jstring JNICALL Java_com_tutpro_baresip_Api_call_1peer_1uri( JNIEnv *env, jobject obj, jlong call) { (void)obj; const char *uri = call_peeruri((struct call *)call); if (uri) return (*env)->NewStringUTF(env, uri); return (*env)->NewStringUTF(env, ""); } JNIEXPORT jstring JNICALL Java_com_tutpro_baresip_Api_call_1diverter_1uri( JNIEnv *env, jobject obj, jlong call) { (void)obj; const char *uri = call_diverteruri((struct call *)call); if (uri) return (*env)->NewStringUTF(env, uri); return (*env)->NewStringUTF(env, ""); } JNIEXPORT jint JNICALL Java_com_tutpro_baresip_Api_message_1send( JNIEnv *env, jobject obj, jlong ua, jstring jPeer, jstring jMsg, jstring jTime) { (void)obj; const char *native_peer = (*env)->GetStringUTFChars(env, jPeer, 0); const char *native_msg = (*env)->GetStringUTFChars(env, jMsg, 0); const char *native_time = (*env)->GetStringUTFChars(env, jTime, 0); LOGD("sending message from ua %ld to %s at %s\n", (long)ua, native_peer, native_time); re_thread_enter(); int err = message_send( (struct ua *)ua, native_peer, native_msg, send_resp_handler, (void *)native_time); re_thread_leave(); if (err) { LOGW("message_send failed with error %d\n", err); } (*env)->ReleaseStringUTFChars(env, jPeer, native_peer); (*env)->ReleaseStringUTFChars(env, jMsg, native_msg); return err; } JNIEXPORT void JNICALL Java_com_tutpro_baresip_Api_call_1destroy( JNIEnv *env, jobject obj, jlong call) { (void)env; (void)obj; re_thread_enter(); mem_deref((struct call *)call); re_thread_leave(); } JNIEXPORT jint JNICALL Java_com_tutpro_baresip_Api_cmd_1exec( JNIEnv *env, jobject obj, jstring javaCmd) { (void)obj; const char *native_cmd = (*env)->GetStringUTFChars(env, javaCmd, 0); LOGD("processing command '%s'\n", native_cmd); re_thread_enter(); int res = cmd_process_long(baresip_commands(), native_cmd, strlen(native_cmd), &pf_null, NULL); re_thread_leave(); (*env)->ReleaseStringUTFChars(env, javaCmd, native_cmd); return res; } JNIEXPORT jstring JNICALL Java_com_tutpro_baresip_Api_audio_1codecs(JNIEnv *env, jobject obj) { (void)obj; struct list *aucodecl = baresip_aucodecl(); 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 aucodec *ac = le->data; if (start == &(codec_buf[0])) len = re_snprintf(start, left, "%s/%u/%u", ac->name, ac->srate, ac->ch); else len = re_snprintf(start, left, ",%s/%u/%u", ac->name, ac->srate, ac->ch); 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_Api_video_1codecs(JNIEnv *env, jobject obj) { (void)obj; struct list *vidcodecl = baresip_vidcodecl(); struct le *le; char codec_buf[256]; char *start = &(codec_buf[0]); unsigned int left = sizeof codec_buf; int len; for (le = list_head(vidcodecl); le != NULL; le = le->next) { const struct vidcodec *vc = le->data; if (start == &(codec_buf[0])) len = re_snprintf(start, left, "%s", vc->name); else len = re_snprintf(start, left, ",%s", vc->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 void JNICALL Java_com_tutpro_baresip_Api_log_1level_1set( JNIEnv *env, jobject obj, jint level) { (void)env; (void)obj; const enum log_level native_level = (enum log_level)level; LOGD("setting log level to '%u'\n", native_level); log_level_set(native_level); } JNIEXPORT jint JNICALL Java_com_tutpro_baresip_Api_net_1use_1nameserver( JNIEnv *env, jobject obj, jstring javaServers) { (void)obj; const char *native_servers = (*env)->GetStringUTFChars(env, javaServers, 0); char servers[256]; char *server; struct sa nsv[NET_MAX_NS]; uint32_t count = 0; char *comma; int res; int err; LOGD("setting dns servers '%s'\n", native_servers); if (str_len(native_servers) > 255) { LOGW("net_use_nameserver: too long servers list (%s)\n", native_servers); return 1; } str_ncpy(servers, native_servers, 256); (*env)->ReleaseStringUTFChars(env, javaServers, native_servers); server = &(servers[0]); while ((count < NET_MAX_NS) && ((comma = strchr(server, ',')) != NULL)) { *comma = '\0'; err = sa_decode(&(nsv[count]), server, str_len(server)); if (err) { LOGW("net_use_nameserver: could not decode '%s' (%u)\n", server, err); return err; } server = ++comma; count++; } if ((count < NET_MAX_NS) && (str_len(server) > 0)) { err = sa_decode(&(nsv[count]), server, str_len(server)); if (err) { LOGW("net_use_nameserver: could not decode `%s' (%u)\n", server, err); return err; } count++; } res = net_use_nameserver(baresip_network(), nsv, count); return res; } JNIEXPORT jint JNICALL Java_com_tutpro_baresip_Api_net_1add_1address_1ifname( JNIEnv *env, jobject obj, jstring jAddr, jstring jIfName) { (void)obj; const char *addr = (*env)->GetStringUTFChars(env, jAddr, 0); const char *name = (*env)->GetStringUTFChars(env, jIfName, 0); int res; struct sa temp_sa; char buf[256]; LOGD("adding address/ifname '%s/%s'\n", addr, name); if (0 == sa_set_str(&temp_sa, addr, 0)) { sa_ntop(&temp_sa, buf, 256); re_thread_enter(); res = net_add_address_ifname(baresip_network(), &temp_sa, name); re_thread_leave(); } else { LOGE("invalid ip address %s\n", addr); res = EAFNOSUPPORT; } (*env)->ReleaseStringUTFChars(env, jAddr, addr); (*env)->ReleaseStringUTFChars(env, jIfName, name); return res; } JNIEXPORT jint JNICALL Java_com_tutpro_baresip_Api_net_1rm_1address( JNIEnv *env, jobject obj, jstring jIp) { (void)obj; const char *native_ip = (*env)->GetStringUTFChars(env, jIp, 0); int res; struct sa temp_sa; char buf[256]; LOGD("removing address '%s'\n", native_ip); if (str_len(native_ip) == 0) { (*env)->ReleaseStringUTFChars(env, jIp, native_ip); return 0; } if (0 == sa_set_str(&temp_sa, native_ip, 0)) { sa_ntop(&temp_sa, buf, 256); re_thread_enter(); res = net_rm_address(baresip_network(), &temp_sa); re_thread_leave(); } else { LOGE("invalid ip address %s\n", native_ip); res = EAFNOSUPPORT; } (*env)->ReleaseStringUTFChars(env, jIp, native_ip); return res; } JNIEXPORT void JNICALL Java_com_tutpro_baresip_Api_uag_1reset_1transp( JNIEnv *env, jobject obj, jboolean reg, jboolean reinvite) { (void)env; (void)obj; LOGD("resetting transports (%d, %d)\n", reg, reinvite); re_thread_enter(); (void)uag_reset_transp(reg, reinvite); re_thread_leave(); } JNIEXPORT void JNICALL Java_com_tutpro_baresip_Api_uag_1enable_1sip_1trace( JNIEnv *env, jobject obj, jboolean enable) { (void)env; (void)obj; LOGD("enabling sip trace (%d)\n", enable); uag_enable_sip_trace(enable); } JNIEXPORT void JNICALL Java_com_tutpro_baresip_Api_config_1verify_1server_1set( JNIEnv *env, jobject obj, jboolean verify) { (void)env; (void)obj; struct config *conf = conf_config(); LOGD("setting verify_server (%d)\n", verify); conf->sip.verify_server = verify; } JNIEXPORT void JNICALL Java_com_tutpro_baresip_Api_net_1debug(JNIEnv *env, jobject obj) { (void)env; (void)obj; re_thread_enter(); net_debug_log(); re_thread_leave(); } JNIEXPORT void JNICALL Java_com_tutpro_baresip_Api_net_1dns_1debug(JNIEnv *env, jobject obj) { (void)env; (void)obj; re_thread_enter(); net_dns_debug_log(); re_thread_leave(); } JNIEXPORT jint JNICALL Java_com_tutpro_baresip_Api_module_1load( JNIEnv *env, jobject obj, jstring javaModule) { (void)obj; const char *native_module = (*env)->GetStringUTFChars(env, javaModule, 0); int result = module_load(".", native_module); (*env)->ReleaseStringUTFChars(env, javaModule, native_module); return result; } JNIEXPORT void JNICALL Java_com_tutpro_baresip_Api_module_1unload( JNIEnv *env, jobject obj, jstring javaModule) { (void)obj; const char *native_module = (*env)->GetStringUTFChars(env, javaModule, 0); module_unload(native_module); LOGD("unloaded module %s\n", native_module); (*env)->ReleaseStringUTFChars(env, javaModule, native_module); } AAudioStream *AAudio_stream = NULL; JNIEXPORT jint JNICALL Java_com_tutpro_baresip_Api_AAudio_1open_1stream(JNIEnv *env, jobject obj) { AAudioStreamBuilder *builder = NULL; jint sessionId = -1; if (AAudio_createStreamBuilder(&builder) != AAUDIO_OK) { return -1; } AAudioStreamBuilder_setDirection(builder, AAUDIO_DIRECTION_INPUT); AAudioStreamBuilder_setSharingMode(builder, AAUDIO_SHARING_MODE_SHARED); AAudioStreamBuilder_setPerformanceMode(builder, AAUDIO_PERFORMANCE_MODE_LOW_LATENCY); AAudioStreamBuilder_setUsage(builder,AAUDIO_USAGE_VOICE_COMMUNICATION); AAudioStreamBuilder_setInputPreset(builder, AAUDIO_INPUT_PRESET_VOICE_COMMUNICATION); AAudioStreamBuilder_setFormat(builder, AAUDIO_FORMAT_PCM_I16); AAudioStreamBuilder_setSampleRate(builder, 16000); AAudioStreamBuilder_setChannelCount(builder, 1); AAudioStreamBuilder_setSessionId(builder, AAUDIO_SESSION_ID_ALLOCATE); if (AAudioStreamBuilder_openStream(builder, &AAudio_stream) == AAUDIO_OK) sessionId = AAudioStream_getSessionId(AAudio_stream); else { LOGE("Failed to open AAudio stream\n"); AAudio_stream = NULL; } AAudioStreamBuilder_delete(builder); return sessionId; } JNIEXPORT void JNICALL Java_com_tutpro_baresip_Api_AAudio_1close_1stream(JNIEnv *env, jobject obj) { if (AAudio_stream != NULL) { AAudioStream_close(AAudio_stream); AAudio_stream = NULL; } } ================================================ FILE: app/src/main/cpp/logger.h ================================================ #include #include #ifndef BARESIP_LOGGER_H #define BARESIP_LOGGER_H #define LOG_TAG "Baresip Lib" #define LOGD(...) \ if (log_level_get() < LEVEL_INFO) \ ((void)__android_log_print(ANDROID_LOG_DEBUG, LOG_TAG, __VA_ARGS__)) #define LOGI(...) \ if (log_level_get() < LEVEL_WARN) \ ((void)__android_log_print(ANDROID_LOG_INFO, LOG_TAG, __VA_ARGS__)) #define LOGW(...) \ if (log_level_get() < LEVEL_ERROR) \ ((void)__android_log_print(ANDROID_LOG_WARN, LOG_TAG, __VA_ARGS__)) #define LOGE(...) \ if (log_level_get() <= LEVEL_ERROR) \ ((void)__android_log_print(ANDROID_LOG_ERROR, LOG_TAG, __VA_ARGS__)) #endif //BARESIP_LOGGER_H ================================================ FILE: app/src/main/kotlin/com/tutpro/baresip/AboutScreen.kt ================================================ package com.tutpro.baresip import androidx.compose.foundation.background import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.WindowInsets import androidx.compose.foundation.layout.asPaddingValues import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.imePadding import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.statusBars import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.verticalScroll import androidx.compose.material.icons.Icons import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.Icon import androidx.compose.material3.IconButton import androidx.compose.material.icons.automirrored.filled.ArrowBack import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Scaffold import androidx.compose.material3.Text import androidx.compose.material3.TopAppBar import androidx.compose.material3.TopAppBarDefaults import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp import androidx.compose.ui.text.AnnotatedString import androidx.compose.ui.text.SpanStyle import androidx.compose.ui.text.TextLinkStyles import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.fromHtml import androidx.navigation.NavController import androidx.navigation.NavGraphBuilder import androidx.navigation.compose.composable fun NavGraphBuilder.aboutScreenRoute(navController: NavController) { composable("about") { AboutScreen(onBack = { navController.navigateUp() }) } } @OptIn(ExperimentalMaterial3Api::class) @Composable private fun AboutScreen(onBack: () -> Unit) { Scaffold( modifier = Modifier.fillMaxSize().imePadding(), containerColor = MaterialTheme.colorScheme.background, topBar = { Column( modifier = Modifier .fillMaxWidth() .background(MaterialTheme.colorScheme.background) .padding( top = WindowInsets.statusBars.asPaddingValues().calculateTopPadding() ) ) { TopAppBar( title = { Text( text = stringResource(R.string.about_title), fontWeight = FontWeight.Bold ) }, navigationIcon = { IconButton(onClick = onBack) { Icon( imageVector = Icons.AutoMirrored.Filled.ArrowBack, contentDescription = null, ) } }, colors = TopAppBarDefaults.topAppBarColors( containerColor = MaterialTheme.colorScheme.primary, titleContentColor = MaterialTheme.colorScheme.onPrimary, navigationIconContentColor = MaterialTheme.colorScheme.onPrimary, ), windowInsets = WindowInsets(0, 0, 0, 0) ) } } ) { contentPadding -> Text( text = AnnotatedString.fromHtml( htmlString = stringResource(R.string.about_text, BuildConfig.VERSION_NAME), linkStyles = TextLinkStyles(SpanStyle(color = MaterialTheme.colorScheme.error)) ), color = MaterialTheme.colorScheme.onBackground, modifier = Modifier .padding(horizontal = 16.dp, vertical = 16.dp) .padding(contentPadding) .verticalScroll(rememberScrollState()) .fillMaxSize() ) } } ================================================ FILE: app/src/main/kotlin/com/tutpro/baresip/Account.kt ================================================ package com.tutpro.baresip import android.content.Context import java.net.URLDecoder import java.net.URLEncoder import java.util.Locale class Account(val accp: Long) { var nickName = "" var displayName = Api.account_display_name(accp) val aor = Api.account_aor(accp) val luri = Api.account_luri(accp) var authUser = Api.account_auth_user(accp) var authPass = Api.account_auth_pass(accp) var outbound = ArrayList() var mediaNat = Api.account_medianat(accp) var stunServer = Api.account_stun_uri(accp) var stunUser = Api.account_stun_user(accp) var stunPass = Api.account_stun_pass(accp) var audioCodec = ArrayList() var videoCodec = ArrayList() var regint = Api.account_regint(accp) var checkOrigin = Api.account_check_origin(accp) var configuredRegInt = REGISTRATION_INTERVAL var mediaEnc = Api.account_mediaenc(accp) var rtcpMux = Api.account_rtcp_mux(accp) var rel100Mode = Api.account_rel100_mode(accp) var dtmfMode = Api.account_dtmfmode(accp) var answerMode = Api.account_answermode(accp) var autoRedirect = Api.account_sip_autoredirect(accp) var blockUnknown = false var vmUri = Api.account_vm_uri(accp) var vmNew = 0 var vmOld = 0 var missedCalls = false var unreadMessages = false var callHistory = true var countryCode = "" var telProvider = Utils.aorDomain(aor) var resumeUri = "" var numericKeypad = false var customParams = "" init { if (authPass == "") authPass = NO_AUTH_PASS var i = 0 while (true) { val ob = Api.account_outbound(accp, i) if (ob != "") { outbound.add(ob) i++ } else { break } } i = 0 while (true) { val ac = Api.account_audio_codec(accp, i) if (ac != "") { audioCodec.add(ac) i++ } else { break } } val extra = Api.account_extra(accp) if (Utils.paramExists(extra, "nickname")) nickName = Utils.paramValue(extra, "nickname") if (Utils.paramExists(extra, "regint")) configuredRegInt = Utils.paramValue(extra, "regint").toInt() callHistory = Utils.paramValue(extra, "call_history") == "" blockUnknown= Utils.paramExists(extra, "block_unknown") if (Utils.paramExists(extra, "country_code")) countryCode = Utils.paramValue(extra, "country_code") if (Utils.paramExists(extra, "tel_provider")) telProvider = URLDecoder.decode(Utils.paramValue(extra, "tel_provider"), "UTF-8") numericKeypad = Utils.paramExists(extra, "numeric_keypad") customParams = extra.substringAfter("last=empty").substringAfter(";") } fun print() : String { var res = if (displayName != "") "\"${displayName}\" " else "" res = "$res<$luri>" if (authUser != "") res += ";auth_user=\"${authUser}\"" if ((authPass != "") && !BaresipService.aorPasswords.containsKey(aor)) res += ";auth_pass=\"${authPass}\"" if (outbound.isNotEmpty()) { res += ";outbound=\"${outbound[0]}\"" if (outbound.size > 1) res += ";outbound2=\"${outbound[1]}\"" res = "$res;sipnat=outbound" } if (!checkOrigin) res += ";check_origin=no" if (mediaNat != "") res += ";medianat=${mediaNat}" if (stunServer != "") res += ";stunserver=\"${stunServer}\"" if (stunUser != "") res += ";stunuser=\"${stunUser}\"" if (stunPass != "") res += ";stunpass=\"${stunPass}\"" if (audioCodec.isNotEmpty()) { var first = true res = "$res;audio_codecs=" for (c in audioCodec) if (first) { res += c first = false } else { res = "$res,$c" } } if (mediaEnc != "") res += ";mediaenc=${mediaEnc}" if (rtcpMux) res += ";rtcp_mux=yes" res += if (rel100Mode == Api.REL100_ENABLED) ";100rel=yes" else ";100rel=no" if (dtmfMode == Api.DTMFMODE_SIP_INFO) res += ";dtmfmode=info" else if (dtmfMode == Api.DTMFMODE_AUTO) res += ";dtmfmode=auto" res = if (vmUri == "") "$res;mwi=no" else "$res;mwi=yes;vm_uri=\"$vmUri\"" if (answerMode == Api.ANSWERMODE_AUTO) res += ";answermode=auto" if (autoRedirect) res += ";sip_autoredirect=yes" res += ";ptime=20;regint=${regint};regq=0.5;pubint=0;inreq_allowed=yes;call_transfer=yes" var extra = "" if (nickName != "") extra += ";nickname=${nickName}" if (!callHistory) extra += ";call_history=no" if (blockUnknown) extra += ";block_unknown=yes" if (telProvider != "") extra += ";tel_provider=${URLEncoder.encode(telProvider, "UTF-8")}" if (countryCode != "") extra += ";country_code=$countryCode" if (configuredRegInt != REGISTRATION_INTERVAL) extra += ";regint=$configuredRegInt" if (numericKeypad) extra += ";numeric_keypad=yes" extra += ";last=empty" if (customParams != "") extra += ";$customParams" res += ";extra=\"" + extra.substringAfter(";") + "\"" if (customParams != "") res += ";$customParams" return res } fun vmMessages(cxt: Context) : String { val new = if (vmNew > 0) { if (vmNew == 1) cxt.getString(R.string.one_new_message) else "$vmNew ${cxt.getString(R.string.new_messages)}" } else "" val old = if (vmOld > 0) { if (vmOld == 1) cxt.getString(R.string.one_old_message) else "$vmOld ${cxt.getString(R.string.old_messages)}" } else "" var msg = cxt.getString(R.string.you_have) if (new != "") { msg = "$msg $new" if (old != "") msg = "$msg ${cxt.getString(R.string.and)} $old" } else { msg = if (old != "") "$msg $old" else cxt.getString(R.string.no_messages) } return "$msg." } fun host() : String { return aor.split("@")[1] } fun text(): String { return if (nickName != "") nickName else aor.split(":")[1].substringBefore(";") } private fun removeAudioCodecsStartingWith(prefix: String) { val newCodecs = ArrayList() for (acSpec in audioCodec) if (!acSpec.lowercase(Locale.ROOT).startsWith(prefix)) newCodecs.add(acSpec) audioCodec = newCodecs } fun removeAudioCodecs(codecModule: String) { when (codecModule) { "g711" -> removeAudioCodecsStartingWith("pcm") "g722" -> removeAudioCodecsStartingWith("g722/") else -> removeAudioCodecsStartingWith(codecModule) } } companion object { fun accounts(): ArrayList { val res = ArrayList() for (ua in BaresipService.uas.value) { res.add(ua.account) } return res } fun saveAccounts() { var accounts = "" for (a in accounts()) accounts = accounts + a.print() + "\n" Utils.putFileContents( BaresipService.filesPath + "/accounts", accounts.toByteArray(Charsets.UTF_8) ) Log.d(TAG, "Saved accounts '${accounts}' to '${BaresipService.filesPath}/accounts'") } fun ofAor(aor: String): Account? { for (ua in BaresipService.uas.value) if (ua.account.aor == aor) return ua.account return null } fun checkDisplayName(dn: String): Boolean { if (dn == "") return true val dnRegex = Regex("^([* .!%_`'~]|[+]|[-a-zA-Z0-9]){1,64}$") return dnRegex.matches(dn) } fun checkAuthUser(au: String): Boolean { if (au == "") return true val ud = au.split("@") return when (ud.size) { 1 -> Utils.checkUriUser(au) 2 -> Utils.checkUriUser(ud[0]) && Utils.checkDomain(ud[1]) else -> false } } fun checkAuthPass(ap: String): Boolean { return (ap.isNotEmpty()) && (ap.length <= 64) && Regex("^[ -~]*$").matches(ap) && !ap.contains('"') } fun uniqueNickName(nickName: String): Boolean { for (ua in BaresipService.uas.value) if (ua.account.nickName == nickName) return false return true } } } ================================================ FILE: app/src/main/kotlin/com/tutpro/baresip/AccountScreen.kt ================================================ package com.tutpro.baresip import android.content.Context import androidx.compose.foundation.background import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.WindowInsets import androidx.compose.foundation.layout.asPaddingValues import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.imePadding import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.statusBars import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.text.KeyboardOptions import androidx.compose.foundation.verticalScroll import androidx.compose.material.icons.Icons import androidx.compose.material.icons.automirrored.filled.ArrowBack import androidx.compose.material.icons.filled.ArrowDropDown import androidx.compose.material.icons.filled.Check import androidx.compose.material.icons.filled.Visibility import androidx.compose.material.icons.filled.VisibilityOff import androidx.compose.material3.CircularProgressIndicator import androidx.compose.material3.DropdownMenu import androidx.compose.material3.DropdownMenuItem import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.HorizontalDivider import androidx.compose.material3.Icon import androidx.compose.material3.IconButton import androidx.compose.material3.MaterialTheme import androidx.compose.material3.OutlinedTextField import androidx.compose.material3.OutlinedTextFieldDefaults import androidx.compose.material3.Scaffold import androidx.compose.material3.Switch import androidx.compose.material3.Text import androidx.compose.material3.TopAppBar import androidx.compose.material3.TopAppBarDefaults import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.collectAsState import androidx.compose.runtime.derivedStateOf import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalSoftwareKeyboardController import androidx.compose.ui.platform.SoftwareKeyboardController import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.TextStyle import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.input.KeyboardCapitalization import androidx.compose.ui.text.input.KeyboardType import androidx.compose.ui.text.input.PasswordVisualTransformation import androidx.compose.ui.text.input.VisualTransformation import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import androidx.lifecycle.viewmodel.compose.viewModel import androidx.navigation.NavController import androidx.navigation.NavGraphBuilder import androidx.navigation.NavType import androidx.navigation.compose.composable import androidx.navigation.navArgument import com.tutpro.baresip.CustomElements.AlertDialog import com.tutpro.baresip.CustomElements.verticalScrollbar import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Job import kotlinx.coroutines.launch import org.xmlpull.v1.XmlPullParser import org.xmlpull.v1.XmlPullParserFactory import java.io.File import java.io.StringReader import java.net.URL import java.util.Locale import javax.net.ssl.HttpsURLConnection fun NavGraphBuilder.accountScreenRoute(navController: NavController) { composable( route = "account/{aor}/{kind}", arguments = listOf( navArgument("aor") { type = NavType.StringType }, navArgument("kind") { type = NavType.StringType } ) ) { backStackEntry -> val ctx = LocalContext.current val viewModel = viewModel() val aor = backStackEntry.arguments?.getString("aor")!! val kind = backStackEntry.arguments?.getString("kind")!! val ua = UserAgent.ofAor(aor)!! AccountScreen( viewModel = viewModel, navController = navController, onBack = { navController.navigateUp() }, checkOnClick = { val ok = checkOnClick(ctx, viewModel, ua) if (ok) { if (reRegister) ua.reRegister() navController.navigateUp() } }, aor = aor, kind = kind ) } } private var keyboardController: SoftwareKeyboardController? = null @OptIn(ExperimentalMaterial3Api::class) @Composable private fun AccountScreen( viewModel: AccountViewModel, navController: NavController, onBack: () -> Unit, checkOnClick: () -> Unit, aor: String, kind: String ) { val ua = UserAgent.ofAor(aor)!! val acc = ua.account var isAccountAvailable by remember { mutableStateOf(false) } var isAccountLoaded by remember { mutableStateOf(false) } LaunchedEffect(kind, acc) { if (kind == "new") initAccountFromNetwork(acc) { isAccountAvailable = true } else isAccountAvailable = true } if (isAccountAvailable) LaunchedEffect(acc) { viewModel.loadAccount(acc) isAccountLoaded = true } Scaffold( modifier = Modifier.fillMaxSize().imePadding(), containerColor = MaterialTheme.colorScheme.background, topBar = { Column( modifier = Modifier .fillMaxWidth() .background(MaterialTheme.colorScheme.background) .padding( top = WindowInsets.statusBars.asPaddingValues().calculateTopPadding() ) ) { TopAppBar( title = { Text( text = acc.text(), fontWeight = FontWeight.Bold ) }, colors = TopAppBarDefaults.topAppBarColors( containerColor = MaterialTheme.colorScheme.primary, navigationIconContentColor = MaterialTheme.colorScheme.onPrimary, titleContentColor = MaterialTheme.colorScheme.onPrimary, actionIconContentColor = MaterialTheme.colorScheme.onPrimary ), navigationIcon = { IconButton(onClick = onBack) { Icon( imageVector = Icons.AutoMirrored.Filled.ArrowBack, contentDescription = null, ) } }, windowInsets = WindowInsets(0, 0, 0, 0), actions = { IconButton(onClick = checkOnClick) { Icon( imageVector = Icons.Filled.Check, contentDescription = "Check" ) } }, ) } } ) { contentPadding -> if (isAccountLoaded) AccountContent(viewModel, navController, contentPadding, ua) else Box( modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center ) { CircularProgressIndicator() } } } private val password = mutableStateOf("") private val showPasswordDialog = mutableStateOf(false) private val alertTitle = mutableStateOf("") private val alertMessage = mutableStateOf("") private val showAlert = mutableStateOf(false) private var reRegister = false @Composable private fun AccountContent( viewModel: AccountViewModel, navController: NavController, contentPadding: PaddingValues, ua: UserAgent ) { val ctx = LocalContext.current val aor = ua.account.aor val mediaNat by viewModel.mediaNat.collectAsState() val showStun by remember { derivedStateOf { mediaNat.isNotEmpty() } } @Composable fun AoR() { Row( Modifier .fillMaxWidth() .padding(top = 8.dp, end = 10.dp), verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.Start ) { OutlinedTextField( value = ua.account.luri, enabled = false, onValueChange = {}, modifier = Modifier.fillMaxWidth(), textStyle = TextStyle(fontSize = 18.sp), label = { Text(text = stringResource(R.string.sip_uri), fontWeight = FontWeight.Bold) }, colors = OutlinedTextFieldDefaults.colors( disabledTextColor = MaterialTheme.colorScheme.onSurface, disabledBorderColor = MaterialTheme.colorScheme.outline, disabledLeadingIconColor = MaterialTheme.colorScheme.onSurfaceVariant, disabledTrailingIconColor = MaterialTheme.colorScheme.onSurfaceVariant, disabledLabelColor = MaterialTheme.colorScheme.onSurfaceVariant, ) ) } } @Composable fun Nickname() { val nickNameTitle = stringResource(R.string.nickname) val nickNameHelp = stringResource(R.string.account_nickname_help) Row( Modifier.fillMaxWidth().padding(top = 8.dp, end = 10.dp), verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.Start ) { val nickName by viewModel.nickName.collectAsState() OutlinedTextField( value = nickName, placeholder = { Text(nickNameTitle) }, onValueChange = { viewModel.nickName.value = it }, modifier = Modifier .fillMaxWidth() .clickable { alertTitle.value = nickNameTitle alertMessage.value = nickNameHelp showAlert.value = true }, singleLine = true, textStyle = TextStyle(fontSize = 18.sp), label = { Text(nickNameTitle) }, keyboardOptions = KeyboardOptions( capitalization = KeyboardCapitalization.Words, keyboardType = KeyboardType.Text), ) } } @Composable fun DisplayName() { val displayNameTitle = stringResource(R.string.display_name) val displayNameHelp = stringResource(R.string.display_name_help) Row( Modifier.fillMaxWidth().padding(top = 8.dp, end = 10.dp), verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.Start ) { val displayName by viewModel.displayName.collectAsState() OutlinedTextField( value = displayName, placeholder = { Text(displayNameTitle) }, onValueChange = { viewModel.displayName.value = it }, modifier = Modifier .fillMaxWidth() .clickable { alertTitle.value = displayNameTitle alertMessage.value = displayNameHelp showAlert.value = true }, singleLine = true, textStyle = TextStyle(fontSize = 18.sp), label = { Text(displayNameTitle) }, keyboardOptions = KeyboardOptions( capitalization = KeyboardCapitalization.Sentences, keyboardType = KeyboardType.Text ), ) } } @Composable fun AuthUser() { val authenticationUserNameTitle = stringResource(R.string.authentication_username) val authenticationUserNameHelp = stringResource(R.string.authentication_username_help) Row( Modifier.fillMaxWidth().padding(top = 8.dp, end = 10.dp), verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.Start ) { val authUser by viewModel.authUser.collectAsState() OutlinedTextField( value = authUser, placeholder = { Text(authenticationUserNameTitle) }, onValueChange = { viewModel.authUser.value = it }, modifier = Modifier .fillMaxWidth() .clickable { alertTitle.value = authenticationUserNameTitle alertMessage.value = authenticationUserNameHelp showAlert.value = true }, textStyle = TextStyle(fontSize = 18.sp), label = { Text(authenticationUserNameTitle) }, keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Text) ) } } @Composable fun AuthPass() { val authenticationPasswordTitle = stringResource(R.string.authentication_password) val authenticationPasswordHelp = stringResource(R.string.authentication_password_help) val showPassword = remember { mutableStateOf(false) } Row( Modifier.fillMaxWidth().padding(top = 8.dp, end = 10.dp), verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.Start ) { val authPass by viewModel.authPass.collectAsState() OutlinedTextField( value = authPass, placeholder = { Text(authenticationPasswordTitle) }, onValueChange = { viewModel.authPass.value = it }, singleLine = true, visualTransformation = if (showPassword.value) VisualTransformation.None else PasswordVisualTransformation(), trailingIcon = { IconButton(onClick = { showPassword.value = !showPassword.value }) { Icon( if (showPassword.value) Icons.Filled.Visibility else Icons.Filled.VisibilityOff, contentDescription = "Visibility", tint = MaterialTheme.colorScheme.onSurfaceVariant ) } }, modifier = Modifier .fillMaxWidth() .clickable { alertTitle.value = authenticationPasswordTitle alertMessage.value = authenticationPasswordHelp showAlert.value = true }, textStyle = TextStyle(fontSize = 18.sp), label = { Text(authenticationPasswordTitle) }, keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Text) ) } } @Composable fun AskPassword(ctx: Context, navController: NavController, ua: UserAgent) { CustomElements.PasswordDialog( ctx = ctx, showPasswordDialog = showPasswordDialog, password = password, keyboardController = keyboardController, title = stringResource(R.string.authentication_password), okAction = { if (password.value != "") { BaresipService.aorPasswords[ua.account.aor] = password.value Api.account_set_auth_pass(ua.account.accp, password.value) password.value = "" ua.reRegister() navController.navigateUp() } }, cancelAction = { ua.reRegister() navController.navigateUp() } ) } @Composable fun Outbound() { val outboundProxiesTitle = stringResource(R.string.outbound_proxies) val outboundProxiesHelp = stringResource(R.string.outbound_proxies_help) Text(text = stringResource(R.string.outbound_proxies), fontSize = 18.sp, modifier = Modifier .padding(top = 8.dp) .clickable { alertTitle.value = outboundProxiesTitle alertMessage.value = outboundProxiesHelp showAlert.value = true } ) Row( Modifier.fillMaxWidth().padding(top = 8.dp, end = 10.dp), verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.Start ) { val outbound1 by viewModel.outbound1.collectAsState() val sipUriOfProxyServerTitle = stringResource(R.string.sip_uri_of_proxy_server) OutlinedTextField( value = outbound1, placeholder = { Text(sipUriOfProxyServerTitle) }, onValueChange = { viewModel.outbound1.value = it }, modifier = Modifier.fillMaxWidth(), singleLine = true, textStyle = TextStyle(fontSize = 18.sp), label = { Text(sipUriOfProxyServerTitle) }, keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Text) ) } Row( Modifier.fillMaxWidth().padding(top = 8.dp, end = 10.dp), verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.Start ) { val outbound2 by viewModel.outbound2.collectAsState() val anotherProxyServerTitle = stringResource(R.string.sip_uri_of_another_proxy_server) OutlinedTextField( value = outbound2, placeholder = { Text(anotherProxyServerTitle) }, onValueChange = { viewModel.outbound2.value = it }, modifier = Modifier.fillMaxWidth(), singleLine = true, textStyle = TextStyle(fontSize = 18.sp), label = { Text(anotherProxyServerTitle) }, keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Text) ) } } @Composable fun Register() { val registerTitle = stringResource(R.string.register) val registerHelp = stringResource(R.string.register_help) Row( Modifier.fillMaxWidth().padding(end = 10.dp), verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.Start ) { Text(text = registerTitle, modifier = Modifier .weight(1f) .clickable { alertTitle.value = registerTitle alertMessage.value = registerHelp showAlert.value = true }, fontSize = 18.sp, ) val register by viewModel.register.collectAsState() Switch( checked = register, onCheckedChange = { viewModel.register.value = it } ) } } @Composable fun RegInt() { val regIntTitle = stringResource(R.string.reg_int) val regIntHelp = stringResource(R.string.reg_int_help) Row( Modifier.fillMaxWidth().padding(end = 10.dp), verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.Start ) { val regInt by viewModel.regInt.collectAsState() OutlinedTextField( value = regInt, placeholder = { Text(regIntTitle) }, onValueChange = { viewModel.regInt.value = it }, modifier = Modifier .fillMaxWidth() .clickable { alertTitle.value = regIntTitle alertMessage.value = regIntHelp showAlert.value = true }, singleLine = true, textStyle = TextStyle(fontSize = 18.sp), label = { Text(regIntTitle) }, keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number) ) } } @Composable fun CheckOrigin() { val checkOriginTitle = stringResource(R.string.check_origin) val checkOriginHelp = stringResource(R.string.check_origin_help) Row( Modifier.fillMaxWidth().padding(end = 10.dp), verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.Start ) { Text(text = checkOriginTitle, modifier = Modifier .weight(1f) .clickable { alertTitle.value = checkOriginTitle alertMessage.value = checkOriginHelp showAlert.value = true }, fontSize = 18.sp, ) val checkOrigin by viewModel.checkOrigin.collectAsState() Switch( checked = checkOrigin, onCheckedChange = { viewModel.checkOrigin.value = it } ) } } @Composable fun BlockUnknown() { val blockUnknownTitle = stringResource(R.string.block_unknown) val blockUnknownHelp = stringResource(R.string.block_unknown_help) val block by viewModel.blockUnknown.collectAsState() Row( Modifier.fillMaxWidth().padding(end = 10.dp), verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.Start ) { Text(text = blockUnknownTitle, modifier = Modifier .weight(1f) .clickable { alertTitle.value = blockUnknownTitle alertMessage.value = blockUnknownHelp showAlert.value = true }, fontSize = 18.sp ) Switch( checked = block, onCheckedChange = { viewModel.blockUnknown.value = it } ) } } @Composable fun AudioCodecs(navController: NavController, aor: String) { Row( Modifier.fillMaxWidth(), verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.Start ) { Text( text = stringResource(R.string.audio_codecs), modifier = Modifier .weight(1f) .clickable { val route = "codecs/$aor/audio" navController.navigate(route) }, fontSize = 18.sp, fontWeight = FontWeight.Bold ) } } @Composable fun MediaEnc() { val mediaEncryptionTitle = stringResource(R.string.media_encryption) val mediaEncryptionHelp = stringResource(R.string.media_encryption_help) Row( Modifier.fillMaxWidth().padding(top = 2.dp, end = 10.dp), verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.Start ) { Text(text = mediaEncryptionTitle, modifier = Modifier .weight(1f) .clickable { alertTitle.value = mediaEncryptionTitle alertMessage.value = mediaEncryptionHelp showAlert.value = true }, fontSize = 18.sp ) val isDropDownExpanded = remember { mutableStateOf(false) } val mediaEnc by viewModel.mediaEnc.collectAsState() Box { Row( horizontalArrangement = Arrangement.Center, verticalAlignment = Alignment.CenterVertically, modifier = Modifier.clickable { isDropDownExpanded.value = true } ) { Text(text = mediaEncMap[mediaEnc]!!) Icon( imageVector = Icons.Filled.ArrowDropDown, contentDescription = null, modifier = Modifier.size(36.dp) ) } DropdownMenu( expanded = isDropDownExpanded.value, onDismissRequest = { isDropDownExpanded.value = false }) { var index = 0 mediaEncMap.forEach { DropdownMenuItem(text = { Text(text = it.value) }, onClick = { isDropDownExpanded.value = false viewModel.mediaEnc.value = it.key } ) if (index < 4) HorizontalDivider(thickness = 1.dp) index++ } } } } } @Composable fun MediaNat() { val mediaNatTitle = stringResource(R.string.media_nat) val mediaNatHelp = stringResource(R.string.media_nat_help) Row( Modifier.fillMaxWidth().padding(end = 10.dp), verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.Start ) { Text(mediaNatTitle, modifier = Modifier .weight(1f) .clickable { alertTitle.value = mediaNatTitle alertMessage.value = mediaNatHelp showAlert.value = true }, fontSize = 18.sp ) val isDropDownExpanded = remember { mutableStateOf(false) } val mediaNat by viewModel.mediaNat.collectAsState() Box { Row( horizontalArrangement = Arrangement.Center, verticalAlignment = Alignment.CenterVertically, modifier = Modifier.clickable { isDropDownExpanded.value = true } ) { Text(text = mediaNatMap[mediaNat]!!) Icon( imageVector = Icons.Filled.ArrowDropDown, contentDescription = null, modifier = Modifier.size(36.dp) ) } DropdownMenu( expanded = isDropDownExpanded.value, onDismissRequest = { isDropDownExpanded.value = false } ) { var index = 0 mediaNatMap.forEach { DropdownMenuItem(text = { Text(text = it.value) }, onClick = { isDropDownExpanded.value = false viewModel.mediaNat.value = it.key } ) if (index < 3) HorizontalDivider(thickness = 1.dp) index++ } } } } } @Composable fun StunServer() { val stunServerTitle = stringResource(R.string.stun_server) val stunServerHelp = stringResource(R.string.stun_server_help) Row( Modifier.fillMaxWidth().padding(end = 10.dp), verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.Start ) { val stunServer by viewModel.stunServer.collectAsState() OutlinedTextField( value = stunServer, placeholder = { Text(stunServerTitle) }, onValueChange = { viewModel.stunServer.value = it }, modifier = Modifier .fillMaxWidth() .clickable { alertTitle.value = stunServerTitle alertMessage.value = stunServerHelp showAlert.value = true }, textStyle = TextStyle(fontSize = 18.sp), label = { Text(stunServerTitle) }, keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Text) ) } } @Composable fun StunUser() { val stunUsernameTitle = stringResource(R.string.stun_username) val stunUsernameHelp = stringResource(R.string.stun_username_help) Row( Modifier.fillMaxWidth().padding(top = 8.dp, end = 10.dp), verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.Start ) { val stunUser by viewModel.stunUser.collectAsState() OutlinedTextField( value = stunUser, placeholder = { Text(stunUsernameTitle) }, onValueChange = { viewModel.stunUser.value = it }, modifier = Modifier .fillMaxWidth() .clickable { alertTitle.value = stunUsernameTitle alertMessage.value = stunUsernameHelp showAlert.value = true }, textStyle = TextStyle(fontSize = 18.sp), label = { Text(stunUsernameTitle) }, keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Text) ) } } @Composable fun StunPass() { val stunPasswordTitle = stringResource(R.string.stun_password) val stunPasswordHelp = stringResource(R.string.stun_password_help) val showPassword = remember { mutableStateOf(false) } Row( Modifier.fillMaxWidth().padding(end = 10.dp), verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.Start ) { val stunPass by viewModel.stunPass.collectAsState() OutlinedTextField( value = stunPass, placeholder = { Text(stunPasswordTitle) }, onValueChange = { viewModel.stunPass.value = it }, singleLine = true, visualTransformation = if (showPassword.value) VisualTransformation.None else PasswordVisualTransformation(), trailingIcon = { IconButton(onClick = { showPassword.value = !showPassword.value }) { Icon( if (showPassword.value) Icons.Filled.Visibility else Icons.Filled.VisibilityOff, contentDescription = "Visibility", tint = MaterialTheme.colorScheme.onSurfaceVariant ) } }, modifier = Modifier .fillMaxWidth() .clickable { alertTitle.value = stunPasswordTitle alertMessage.value = stunPasswordHelp showAlert.value = true }, textStyle = TextStyle(fontSize = 18.sp), label = { Text(stunPasswordTitle) }, keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Text) ) } } @Composable fun RtcpMux() { val rtcpMuxTitle = stringResource(R.string.rtcp_mux) val rtcpMuxHelp = stringResource(R.string.rtcp_mux_help) Row( Modifier.fillMaxWidth().padding(end = 10.dp), verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.Start ) { val rtcpMux by viewModel.rtcpMux.collectAsState() Text(text = rtcpMuxTitle, modifier = Modifier .weight(1f) .clickable { alertTitle.value = rtcpMuxTitle alertMessage.value = rtcpMuxHelp showAlert.value = true }, fontSize = 18.sp ) Switch( checked = rtcpMux, onCheckedChange = { viewModel.rtcpMux.value = it } ) } } @Composable fun Rel100() { val rel100Title = stringResource(R.string.rel_100) val rel100Help = stringResource(R.string.rel_100_help) Row( Modifier.fillMaxWidth().padding(end = 10.dp), verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.Start ) { val rel100 by viewModel.rel100.collectAsState() Text( text = rel100Title, modifier = Modifier .weight(1f) .clickable { alertTitle.value = rel100Title alertMessage.value = rel100Help showAlert.value = true }, fontSize = 18.sp ) Switch( checked = rel100, onCheckedChange = { viewModel.rel100.value = it } ) } } @Composable fun Dtmf() { val dtmfModeTitle = stringResource(R.string.dtmf_mode) val dtmfModeHelp = stringResource(R.string.dtmf_mode_help) val dtmfInbandText = stringResource(R.string.dtmf_inband) val dtmfInfoText = stringResource(R.string.dtmf_info) val dtmfAutoText = stringResource(R.string.dtmf_auto) val dtmfMode by viewModel.dtmfMode.collectAsState() Row( Modifier.fillMaxWidth().padding(end = 10.dp), verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.Start ) { val dtmfModeMap = mapOf(Api.DTMFMODE_RTP_EVENT to dtmfInbandText, Api.DTMFMODE_SIP_INFO to dtmfInfoText, Api.DTMFMODE_AUTO to dtmfAutoText) Text(text = dtmfModeTitle, modifier = Modifier .weight(1f) .clickable { alertTitle.value = dtmfModeTitle alertMessage.value = dtmfModeHelp showAlert.value = true }, fontSize = 18.sp ) val isDropDownExpanded = remember { mutableStateOf(false) } Box { Row( horizontalArrangement = Arrangement.Center, verticalAlignment = Alignment.CenterVertically, modifier = Modifier.clickable { isDropDownExpanded.value = true } ) { Text(text = dtmfModeMap[dtmfMode]!!) Icon( imageVector = Icons.Filled.ArrowDropDown, contentDescription = null, modifier = Modifier.size(36.dp) ) } DropdownMenu( expanded = isDropDownExpanded.value, onDismissRequest = { isDropDownExpanded.value = false }) { var index = 0 dtmfModeMap.forEach { DropdownMenuItem(text = { Text(text = it.value) }, onClick = { isDropDownExpanded.value = false viewModel.dtmfMode.value = it.key } ) if (index < 2) HorizontalDivider(thickness = 1.dp) index++ } } } } } val manualText = stringResource(R.string.manual) val autoText = stringResource(R.string.auto) @Composable fun Answer() { val answerModeTitle = stringResource(R.string.answer_mode) val answerModeHelp = stringResource(R.string.answer_mode_help) val answerMode by viewModel.answerMode.collectAsState() Row( Modifier.fillMaxWidth().padding(end = 10.dp), verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.Start ) { val answerModeMap = mapOf(Api.ANSWERMODE_MANUAL to manualText, Api.ANSWERMODE_AUTO to autoText) Text(text = answerModeTitle, modifier = Modifier .weight(1f) .clickable { alertTitle.value = answerModeTitle alertMessage.value = answerModeHelp showAlert.value = true }, fontSize = 18.sp ) val isDropDownExpanded = remember { mutableStateOf(false) } Box { Row( horizontalArrangement = Arrangement.Center, verticalAlignment = Alignment.CenterVertically, modifier = Modifier.clickable { isDropDownExpanded.value = true } ) { Text(text = answerModeMap[answerMode]!!) Icon( imageVector = Icons.Filled.ArrowDropDown, contentDescription = null, modifier = Modifier.size(36.dp) ) } DropdownMenu( expanded = isDropDownExpanded.value, onDismissRequest = { isDropDownExpanded.value = false } ) { var index = 0 answerModeMap.forEach { DropdownMenuItem(text = { Text(text = it.value) }, onClick = { isDropDownExpanded.value = false viewModel.answerMode.value = it.key }) if (index < 1) HorizontalDivider(thickness = 1.dp) index++ } } } } } @Composable fun Redirect() { val redirectModeTitle = stringResource(R.string.redirect_mode) val redirectModeHelp = stringResource(R.string.redirect_mode_help) val autoRedirect by viewModel.autoRedirect.collectAsState() Row( Modifier.fillMaxWidth().padding(end = 10.dp), verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.Start ) { val redirectModeMap = mapOf(false to manualText, true to autoText) Text(text = redirectModeTitle, modifier = Modifier .weight(1f) .clickable { alertTitle.value = redirectModeTitle alertMessage.value = redirectModeHelp showAlert.value = true }, fontSize = 18.sp ) val isDropDownExpanded = remember { mutableStateOf(false) } Box { Row( horizontalArrangement = Arrangement.Center, verticalAlignment = Alignment.CenterVertically, modifier = Modifier.clickable { isDropDownExpanded.value = true } ) { Text(text = redirectModeMap[autoRedirect]!!) Icon( imageVector = Icons.Filled.ArrowDropDown, contentDescription = null, modifier = Modifier.size(36.dp) ) } DropdownMenu( expanded = isDropDownExpanded.value, onDismissRequest = { isDropDownExpanded.value = false } ) { var index = 0 redirectModeMap.forEach { DropdownMenuItem( text = { Text(text = it.value) }, onClick = { isDropDownExpanded.value = false viewModel.autoRedirect.value = it.key } ) if (index < 1) HorizontalDivider(thickness = 1.dp) index++ } } } } } @Composable fun Voicemail() { val voicemailUriTitle = stringResource(R.string.voicemail_uri) val voicemailUriHelp = stringResource(R.string.voicemain_uri_help) val vmUri by viewModel.vmUri.collectAsState() Row( Modifier.fillMaxWidth().padding(end = 10.dp), verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.Start ) { OutlinedTextField( value = vmUri, placeholder = { Text(voicemailUriTitle) }, onValueChange = { viewModel.vmUri.value = it }, modifier = Modifier .fillMaxWidth() .clickable { alertTitle.value = voicemailUriTitle alertMessage.value = voicemailUriHelp showAlert.value = true }, singleLine = true, textStyle = TextStyle(fontSize = 18.sp), label = { Text(voicemailUriTitle) }, keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Text) ) } } @Composable fun CountryCode() { val countryCodeTitle = stringResource(R.string.country_code) val countryCodeHelp = stringResource(R.string.country_code_help) val countryCode by viewModel.countryCode.collectAsState() Row( Modifier.fillMaxWidth().padding(top = 8.dp, end = 10.dp), verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.Start ) { OutlinedTextField( value = countryCode, placeholder = { Text(countryCodeTitle) }, onValueChange = { viewModel.countryCode.value = it }, modifier = Modifier .fillMaxWidth() .clickable { alertTitle.value = countryCodeTitle alertMessage.value = countryCodeHelp showAlert.value = true }, singleLine = true, textStyle = TextStyle(fontSize = 18.sp), label = { Text(countryCodeTitle) }, keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Text) ) } } @Composable fun TelProvider() { val telephonyProviderTitle = stringResource(R.string.telephony_provider) val telephonyProviderHelp = stringResource(R.string.telephony_provider_help) val telProvider by viewModel.telProvider.collectAsState() Row( Modifier.fillMaxWidth().padding(top = 8.dp, end = 10.dp), verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.Start ) { OutlinedTextField( value = telProvider, placeholder = { Text(telephonyProviderTitle) }, onValueChange = { viewModel.telProvider.value = it }, modifier = Modifier .fillMaxWidth() .clickable { alertTitle.value = telephonyProviderTitle alertMessage.value = telephonyProviderHelp showAlert.value = true }, singleLine = true, textStyle = TextStyle(fontSize = 18.sp), label = { Text(telephonyProviderTitle) }, keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Text) ) } } @Composable fun NumericKeypad() { val numericKeypadTitle = stringResource(R.string.numeric_keypad) val numericKeypadHelp = stringResource(R.string.numeric_keypad_help) val numericKeypad by viewModel.numericKeypad.collectAsState() Row( Modifier.fillMaxWidth().padding(end = 10.dp), verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.Start ) { Text(text = numericKeypadTitle, modifier = Modifier .weight(1f) .clickable { alertTitle.value = numericKeypadTitle alertMessage.value = numericKeypadHelp showAlert.value = true }, fontSize = 18.sp ) Switch( checked = numericKeypad, onCheckedChange = { viewModel.numericKeypad.value = it } ) } } @Composable fun DefaultAccount() { val defaultAccountTitle = stringResource(R.string.default_account) val defaultAccountHelp = stringResource(R.string.default_account_help) val defaultAccount by viewModel.defaultAccount.collectAsState() Row( Modifier.fillMaxWidth().padding(end = 10.dp), verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.Start ) { Text(text = defaultAccountTitle, modifier = Modifier .weight(1f) .clickable { alertTitle.value = defaultAccountTitle alertMessage.value = defaultAccountHelp showAlert.value = true }, fontSize = 18.sp ) Switch( checked = defaultAccount, onCheckedChange = { viewModel.defaultAccount.value = it } ) } } @Composable fun CustomParams() { val customParamsTitle = stringResource(R.string.custom_parameters) val customParamsHelp = stringResource(R.string.custom_parameters_help) val customParams by viewModel.customParams.collectAsState() Row( Modifier.fillMaxWidth().padding(top = 8.dp, end = 10.dp), verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.Start ) { OutlinedTextField( value = customParams, placeholder = { Text(customParamsTitle) }, onValueChange = { viewModel.customParams.value = it }, modifier = Modifier .fillMaxWidth() .clickable { alertTitle.value = customParamsTitle alertMessage.value = customParamsHelp showAlert.value = true }, singleLine = true, textStyle = TextStyle(fontSize = 18.sp), label = { Text(customParamsTitle) }, keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Text) ) } } if (showAlert.value) { AlertDialog( showDialog = showAlert, title = alertTitle.value, message = alertMessage.value, lastButtonText = stringResource(R.string.ok), ) } keyboardController = LocalSoftwareKeyboardController.current val scrollState = rememberScrollState() Column( modifier = Modifier .fillMaxWidth() .padding(contentPadding) .padding(start = 16.dp, end = 4.dp, top = 8.dp, bottom = 16.dp) .verticalScrollbar(scrollState) .verticalScroll(state = scrollState), verticalArrangement = Arrangement.spacedBy(8.dp), ) { AoR() Nickname() DisplayName() AuthUser() AuthPass() if (showPasswordDialog.value) AskPassword(ctx, navController, ua) Outbound() Register() if (viewModel.register.collectAsState().value) { RegInt() CheckOrigin() } BlockUnknown() AudioCodecs(navController, aor) MediaEnc() MediaNat() if (showStun) { StunServer() StunUser() StunPass() } RtcpMux() Rel100() Dtmf() Answer() Redirect() Voicemail() CountryCode() TelProvider() NumericKeypad() DefaultAccount() CustomParams() } } private fun checkOnClick(ctx: Context, viewModel: AccountViewModel, ua: UserAgent): Boolean { val acc = ua.account val noticeTitle = ctx.getString(R.string.notice) val nn = viewModel.nickName.value.trim() if (nn != acc.nickName) { if (Account.checkDisplayName(nn)) { if (nn == "" || Account.uniqueNickName(nn)) { acc.nickName = nn Log.d(TAG, "New nickname is ${acc.nickName}") } else { alertTitle.value = noticeTitle alertMessage.value = String.format(ctx.getString(R.string.non_unique_account_nickname), nn) showAlert.value = true return false } } else { alertTitle.value = noticeTitle alertMessage.value = String.format(ctx.getString(R.string.invalid_account_nickname), nn) showAlert.value = true return false } } val dn = viewModel.displayName.value.trim() if (dn != acc.displayName) { if (Account.checkDisplayName(dn)) { if (Api.account_set_display_name(acc.accp, dn) == 0) { acc.displayName = Api.account_display_name(acc.accp) Log.d(TAG, "New display name is ${acc.displayName}") } else { Log.e(TAG, "Setting of display name failed") } } else { alertTitle.value = noticeTitle alertMessage.value = String.format(ctx.getString(R.string.invalid_display_name), dn) showAlert.value = true return false } } val au = viewModel.authUser.value.trim() if (au != acc.authUser) { if (Account.checkAuthUser(au)) { if (Api.account_set_auth_user(acc.accp, au) == 0) { acc.authUser = Api.account_auth_user(acc.accp) Log.d(TAG, "New auth user is ${acc.authUser}") if (acc.regint > 0) reRegister = true } else { Log.e(TAG, "Setting of auth user failed") } } else { alertTitle.value = noticeTitle alertMessage.value = String.format(ctx.getString(R.string.invalid_authentication_username), au) showAlert.value = true return false } } val ap = viewModel.authPass.value.trim() if (ap != "") { if (ap != acc.authPass) { if (Account.checkAuthPass(ap)) { if (Api.account_set_auth_pass(acc.accp, ap) == 0) { acc.authPass = Api.account_auth_pass(acc.accp) if (acc.regint > 0) reRegister = true } else Log.e(TAG, "Setting of auth pass failed") BaresipService.aorPasswords.remove(acc.aor) } else { alertTitle.value = noticeTitle alertMessage.value = String.format(ctx.getString(R.string.invalid_authentication_password), ap) showAlert.value = true return false } } else BaresipService.aorPasswords.remove(acc.aor) } else { // ap == "" if (acc.authPass != NO_AUTH_PASS && acc.authPass != BaresipService.aorPasswords[acc.aor]) if (Api.account_set_auth_pass(acc.accp, "") == 0) { acc.authPass = NO_AUTH_PASS BaresipService.aorPasswords[acc.aor] = NO_AUTH_PASS } } val ob = ArrayList() var ob1 = viewModel.outbound1.value.trim().replace(" ", "") if (ob1 != "") { if (!ob1.startsWith("sip:")) ob1 = "sip:$ob1" if (checkOutboundUri(ob1)) { ob.add(ob1) } else { alertTitle.value = noticeTitle alertMessage.value = String.format(ctx.getString(R.string.invalid_proxy_server_uri), ob1) showAlert.value = true return false } } var ob2 = viewModel.outbound2.value.trim().replace(" ", "") if (ob2 != "") { if (!ob2.startsWith("sip:")) ob2 = "sip:$ob2" if (checkOutboundUri(ob2)) ob.add(ob2) else { alertTitle.value = noticeTitle alertMessage.value = String.format(ctx.getString(R.string.invalid_proxy_server_uri), ob2) showAlert.value = true return false } } if (ob != acc.outbound) { for (i in 0..1) { val uri = if (ob.size > i) ob[i] else "" if (Api.account_set_outbound(acc.accp, uri, i) != 0) Log.e(TAG, "Setting of outbound proxy $i uri '$uri' failed") } Log.d(TAG, "New outbound proxies are $ob") acc.outbound = ob if (ob.isEmpty()) Api.account_set_sipnat(acc.accp, "") else Api.account_set_sipnat(acc.accp, "outbound") if (acc.regint > 0) reRegister = true } val regInt = try { viewModel.regInt.value.trim().toInt() } catch (_: NumberFormatException) { 0 } if (regInt !in 60..3600) { alertTitle.value = noticeTitle alertMessage.value = String.format(ctx.getString(R.string.invalid_reg_int), "$regInt") showAlert.value = true return false } val reReg = (viewModel.register.value != acc.regint > 0) || (viewModel.register.value && regInt != acc.configuredRegInt) if (reReg) { if (Api.account_set_regint(acc.accp, if (viewModel.register.value) regInt else 0) != 0) { Log.e(TAG, "Setting of regint failed") } else { acc.regint = Api.account_regint(acc.accp) acc.configuredRegInt = regInt Log.d(TAG, "New regint is ${acc.regint}") reRegister = true } } else { if (regInt != acc.configuredRegInt) acc.configuredRegInt = regInt } val newCheckOrigin = viewModel.checkOrigin.value if (newCheckOrigin != acc.checkOrigin) { Api.account_set_check_origin(ua.account.accp, newCheckOrigin) acc.checkOrigin = Api.account_check_origin(ua.account.accp) Log.d(TAG, "New checkOrigin is ${acc.checkOrigin}") } val newMediaNat = viewModel.mediaNat.value if (newMediaNat != acc.mediaNat) { if (Api.account_set_medianat(acc.accp, newMediaNat) == 0) { acc.mediaNat = Api.account_medianat(acc.accp) Log.d(TAG, "New medianat is ${acc.mediaNat}") } else Log.e(TAG, "Setting of medianat $newMediaNat failed") } var newStunServer = viewModel.stunServer.value.trim() if (newMediaNat != "") { if ((newMediaNat == "stun" || newMediaNat == "ice") && newStunServer == "") newStunServer = ctx.getString(R.string.stun_server_default) if (!Utils.checkStunUri(newStunServer) || (newMediaNat == "turn" && newStunServer.substringBefore(":") !in setOf("turn", "turns"))) { alertTitle.value = ctx.getString(R.string.notice) alertMessage.value = String.format(ctx.getString(R.string.invalid_stun_server), newStunServer) showAlert.value = true return false } } if (acc.stunServer != newStunServer) { if (Api.account_set_stun_uri(acc.accp, newStunServer) == 0) { acc.stunServer = Api.account_stun_uri(acc.accp) Log.d(TAG, "New STUN/TURN server URI is '${acc.stunServer}'") } else { Log.e(TAG, "Setting of STUN/TURN URI server $newStunServer failed") } } val newStunUser = viewModel.stunUser.value.trim() if (acc.stunUser != newStunUser) { if (Account.checkAuthUser(newStunUser)) { if (Api.account_set_stun_user(acc.accp, newStunUser) == 0) { acc.stunUser = Api.account_stun_user(acc.accp) Log.d(TAG, "New STUN/TURN user is ${acc.stunUser}") } else Log.e(TAG, "Setting of STUN/TURN user $newStunUser failed") } else { alertTitle.value = noticeTitle alertMessage.value = String.format(ctx.getString(R.string.invalid_stun_username), newStunUser) showAlert.value = true return false } } val newStunPass = viewModel.stunPass.value.trim() if (acc.stunPass != newStunPass) { if (newStunPass.isEmpty() || Account.checkAuthPass(newStunPass)) { if (Api.account_set_stun_pass(acc.accp, newStunPass) == 0) acc.stunPass = Api.account_stun_pass(acc.accp) else Log.e(TAG, "Setting of stun pass failed") } else { alertTitle.value = noticeTitle alertMessage.value = String.format(ctx.getString(R.string.invalid_stun_password), newStunPass) showAlert.value = true return false } } val newRtcpMux = viewModel.rtcpMux.value if (newRtcpMux != acc.rtcpMux) if (Api.account_set_rtcp_mux(acc.accp, newRtcpMux) == 0) { acc.rtcpMux = Api.account_rtcp_mux(acc.accp) Log.d(TAG, "New rtcpMux is ${acc.rtcpMux}") } else { Log.e(TAG, "Setting of account_rtc_mux $newRtcpMux failed") } val new100Rel = viewModel.rel100.value if (new100Rel != (acc.rel100Mode == Api.REL100_ENABLED)) { val mode = if (new100Rel) Api.REL100_ENABLED else Api.REL100_DISABLED if (Api.account_set_rel100_mode(acc.accp, mode) == 0) { acc.rel100Mode = Api.account_rel100_mode(acc.accp) Api.ua_update_account(ua.uap) Log.d(TAG, "New rel100Mode is ${acc.rel100Mode}") } else { Log.e(TAG, "Setting of account_rel100Mode $mode failed") } } val newDtmfMode = viewModel.dtmfMode.value if (newDtmfMode != acc.dtmfMode) { if (Api.account_set_dtmfmode(acc.accp, newDtmfMode) == 0) { acc.dtmfMode = Api.account_dtmfmode(acc.accp) Log.d(TAG, "New dtmfmode is ${acc.dtmfMode}") } else { Log.e(TAG, "Setting of dtmfmode $newDtmfMode failed") } } val newAnswerMode = viewModel.answerMode.value if (newAnswerMode != acc.answerMode) { if (Api.account_set_answermode(acc.accp, newAnswerMode) == 0) { acc.answerMode = Api.account_answermode(acc.accp) Log.d(TAG, "New answermode is ${acc.answerMode}") } else { Log.e(TAG, "Setting of answermode $newAnswerMode failed") } } val newAutoRedirect = viewModel.autoRedirect.value if (newAutoRedirect != acc.autoRedirect) { Api.account_set_sip_autoredirect(acc.accp, newAutoRedirect) acc.autoRedirect = newAutoRedirect Log.d(TAG, "New autoRedirect is ${acc.autoRedirect}") } val newBlockUnknown = viewModel.blockUnknown.value if (newBlockUnknown != acc.blockUnknown) { acc.blockUnknown = newBlockUnknown Log.d(TAG, "New blockUnknown is ${acc.blockUnknown}") } var newVmUri = viewModel.vmUri.value.trim() if (newVmUri != acc.vmUri) { if (newVmUri != "") { if (!newVmUri.startsWith("sip:")) newVmUri = "sip:$newVmUri" if (!newVmUri.contains("@")) newVmUri = "$newVmUri@${acc.host()}" if (!Utils.checkUri(newVmUri)) { alertTitle.value = noticeTitle alertMessage.value = String.format(ctx.getString(R.string.invalid_sip_or_tel_uri), newVmUri) showAlert.value = true return false } Api.account_set_mwi(acc.accp, true) } else Api.account_set_mwi(acc.accp, false) acc.vmUri = newVmUri Log.d(TAG, "New voicemail URI is ${acc.vmUri}") } val newCountryCode = viewModel.countryCode.value.trim() if (newCountryCode != acc.countryCode) { if (newCountryCode != "" && !Utils.checkCountryCode(newCountryCode)) { alertTitle.value = ctx.getString(R.string.notice) alertMessage.value = String.format(ctx.getString(R.string.invalid_country_code), newCountryCode) showAlert.value = true return false } acc.countryCode = newCountryCode Log.d(TAG, "New country code is ${acc.countryCode}") } val hostPart = viewModel.telProvider.value.trim() if (hostPart != acc.telProvider) { if (hostPart != "" && !Utils.checkHostPortParams(hostPart)) { alertTitle.value = noticeTitle alertMessage.value = String.format(ctx.getString(R.string.invalid_sip_uri_hostpart), hostPart) showAlert.value = true return false } acc.telProvider = hostPart Log.d(TAG, "New tel provider is ${acc.telProvider}") } val newNumericKeypad = viewModel.numericKeypad.value if (newNumericKeypad != acc.numericKeypad) { acc.numericKeypad = newNumericKeypad Log.d(TAG, "New numericKeyboard is ${acc.numericKeypad}") } val newCustomParams = viewModel.customParams.value if (newCustomParams != acc.customParams) { acc.customParams = newCustomParams Log.d(TAG, "New customParams is ${acc.customParams}") } if (viewModel.defaultAccount.value) ua.makeDefault() Api.account_debug(acc.accp) Account.saveAccounts() if (acc.authUser != "" && BaresipService.aorPasswords[acc.aor] == NO_AUTH_PASS) { showPasswordDialog.value = true return false } else return true } private fun initAccountFromNetwork(acc: Account, onConfigLoaded: () -> Unit) { val scope = CoroutineScope(Job() + Dispatchers.Main) scope.launch(Dispatchers.IO) { val url = "https://${Utils.uriHostPart(acc.aor)}/baresip/account_config.xml" val urlConnection = URL(url).openConnection() as HttpsURLConnection urlConnection.connectTimeout = 5000 urlConnection.readTimeout = 3000 val caFile = File(BaresipService.filesPath + "/ca_certs.crt") val template = try { if (caFile.exists()) Utils.readUrlWithCustomCAs(urlConnection, caFile) else urlConnection.inputStream.bufferedReader().use { it.readText() } } catch (e: java.lang.Exception) { Log.d(TAG, "Failed to get account template from network: ${e.message}") null } if (template != null) { Log.d(TAG, "Got account template '$template'") val parserFactory: XmlPullParserFactory = XmlPullParserFactory.newInstance() val parser: XmlPullParser = parserFactory.newPullParser() parser.setInput(StringReader(template)) var tag: String? var text = "" var event = parser.eventType val audioCodecs = ArrayList(Api.audio_codecs().split(",")) val videoCodecs = ArrayList(Api.video_codecs().split(",")) while (event != XmlPullParser.END_DOCUMENT) { tag = parser.name when (event) { XmlPullParser.TEXT -> text = parser.text XmlPullParser.START_TAG -> { if (tag == "audio-codecs") acc.audioCodec.clear() if (tag == "video-codecs") acc.videoCodec.clear() } XmlPullParser.END_TAG -> when (tag) { "outbound-proxy-1" -> if (text.isNotEmpty()) acc.outbound.add(text) "outbound-proxy-2" -> if (text.isNotEmpty()) acc.outbound.add(text) "registration-interval" -> acc.configuredRegInt = try { text.toInt() } catch (_: NumberFormatException) { 900 } "register" -> { acc.regint = if (text == "yes") acc.configuredRegInt else 0 if (acc.regint > 0) acc.checkOrigin = true } "audio-codec" -> if (text in audioCodecs) acc.audioCodec.add(text) "video-codec" -> if (text in videoCodecs) acc.videoCodec.add(text) "media-encoding" -> { val enc = text.lowercase(Locale.ROOT) if (enc in mediaEncMap.keys && enc.isNotEmpty()) acc.mediaEnc = enc } "media-nat" -> { val nat = text.lowercase(Locale.ROOT) if (nat in mediaNatMap.keys && nat.isNotEmpty()) acc.mediaNat = nat } "stun-turn-server" -> if (text.isNotEmpty()) acc.stunServer = text "rtcp-mux" -> acc.rtcpMux = text == "yes" "100rel-mode" -> acc.rel100Mode = if (text == "yes") Api.REL100_ENABLED else Api.REL100_DISABLED "dtmf-mode" -> if (text in arrayOf("rtp-event", "sip-info", "auto")) acc.dtmfMode = when (text) { "rtp-event" -> Api.DTMFMODE_RTP_EVENT "sip-info" -> Api.DTMFMODE_SIP_INFO else -> Api.DTMFMODE_AUTO } "answer-mode" -> if (text in arrayOf("manual", "auto")) acc.answerMode = if (text == "manual") Api.ANSWERMODE_MANUAL else Api.ANSWERMODE_AUTO "redirect-mode" -> acc.autoRedirect = text == "yes" "voicemail-uri" -> if (text.isNotEmpty()) acc.vmUri = text "country-code" -> acc.countryCode = text "tel-provider" -> acc.telProvider = text } } event = parser.next() } } onConfigLoaded() } } private fun checkOutboundUri(uri: String): Boolean { if (!uri.startsWith("sip:")) return false return Utils.checkHostPortParams(uri.substring(4)) } ================================================ FILE: app/src/main/kotlin/com/tutpro/baresip/AccountViewModel.kt ================================================ package com.tutpro.baresip import androidx.lifecycle.ViewModel import kotlinx.coroutines.flow.MutableStateFlow class AccountViewModel: ViewModel() { val nickName = MutableStateFlow("") val displayName = MutableStateFlow("") val authUser = MutableStateFlow("") val authPass = MutableStateFlow("") val outbound1 = MutableStateFlow("") val outbound2 = MutableStateFlow("") val register = MutableStateFlow(false) val regInt = MutableStateFlow("") val checkOrigin = MutableStateFlow(true) val mediaEnc = MutableStateFlow("") val mediaNat = MutableStateFlow("") val stunServer = MutableStateFlow("") val stunUser = MutableStateFlow("") val stunPass = MutableStateFlow("") val rtcpMux = MutableStateFlow(false) val rel100 = MutableStateFlow(false) val dtmfMode = MutableStateFlow(0) val answerMode = MutableStateFlow(0) val autoRedirect = MutableStateFlow(false) val blockUnknown = MutableStateFlow(false) val vmUri = MutableStateFlow("") val countryCode = MutableStateFlow("") val telProvider = MutableStateFlow("") val numericKeypad = MutableStateFlow(false) val defaultAccount = MutableStateFlow(false) val customParams = MutableStateFlow("") private var isLoaded = false fun loadAccount(acc: Account) { if (isLoaded) return else isLoaded = true nickName.value = acc.nickName displayName.value = acc.displayName authUser.value = acc.authUser authPass.value = if (BaresipService.aorPasswords[acc.aor] == null && acc.authPass != NO_AUTH_PASS) acc.authPass else "" outbound1.value = if (acc.outbound.isNotEmpty()) acc.outbound[0] else "" outbound2.value = if (acc.outbound.size > 1) acc.outbound[1] else "" register.value = acc.regint > 0 regInt.value = acc.configuredRegInt.toString() checkOrigin.value = acc.checkOrigin mediaEnc.value = acc.mediaEnc mediaNat.value = acc.mediaNat stunServer.value = acc.stunServer stunUser.value = acc.stunUser stunPass.value = acc.stunPass rtcpMux.value = acc.rtcpMux rel100.value = acc.rel100Mode == Api.REL100_ENABLED dtmfMode.value = acc.dtmfMode answerMode.value = acc.answerMode autoRedirect.value = acc.autoRedirect blockUnknown.value = acc.blockUnknown vmUri.value = acc.vmUri countryCode.value = acc.countryCode telProvider.value = acc.telProvider numericKeypad.value = acc.numericKeypad defaultAccount.value = UserAgent.findAorIndex(acc.aor)!! == 0 customParams.value = acc.customParams } } ================================================ FILE: app/src/main/kotlin/com/tutpro/baresip/AccountsScreen.kt ================================================ package com.tutpro.baresip import android.content.Context import androidx.compose.foundation.background import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.WindowInsets import androidx.compose.foundation.layout.asPaddingValues import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.imePadding import androidx.compose.foundation.layout.navigationBarsPadding import androidx.compose.foundation.layout.offset import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.statusBars import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.items import androidx.compose.foundation.lazy.rememberLazyListState import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.text.KeyboardOptions import androidx.compose.foundation.verticalScroll import androidx.compose.material.icons.Icons import androidx.compose.material.icons.automirrored.filled.ArrowBack import androidx.compose.material.icons.filled.Add import androidx.compose.material.icons.filled.Delete import androidx.compose.material.icons.outlined.Clear import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.Icon import androidx.compose.material3.IconButton import androidx.compose.material3.MaterialTheme import androidx.compose.material3.OutlinedTextField import androidx.compose.material3.Scaffold import androidx.compose.material3.SmallFloatingActionButton import androidx.compose.material3.Text import androidx.compose.material3.TopAppBar import androidx.compose.material3.TopAppBarDefaults import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalFocusManager import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.TextStyle import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.input.KeyboardType import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import androidx.navigation.NavController import androidx.navigation.NavGraphBuilder import androidx.navigation.compose.composable import com.tutpro.baresip.CustomElements.AlertDialog import com.tutpro.baresip.CustomElements.verticalScrollbar fun NavGraphBuilder.accountsScreenRoute(navController: NavController) { composable("accounts") { AccountsScreen(navController) } } @OptIn(ExperimentalMaterial3Api::class) @Composable fun AccountsScreen(navController: NavController) { Scaffold( modifier = Modifier.fillMaxSize().imePadding(), containerColor = MaterialTheme.colorScheme.background, topBar = { Column( modifier = Modifier .fillMaxWidth() .background(MaterialTheme.colorScheme.background) .padding( top = WindowInsets.statusBars.asPaddingValues().calculateTopPadding() ) ) { TopAppBar( title = { Text( text = stringResource(R.string.accounts), fontWeight = FontWeight.Bold ) }, colors = TopAppBarDefaults.topAppBarColors( containerColor = MaterialTheme.colorScheme.primary, navigationIconContentColor = MaterialTheme.colorScheme.onPrimary, titleContentColor = MaterialTheme.colorScheme.onPrimary, ), navigationIcon = { IconButton(onClick = { navController.navigateUp() }) { Icon( imageVector = Icons.AutoMirrored.Filled.ArrowBack, contentDescription = null, ) } }, windowInsets = WindowInsets(0, 0, 0, 0) ) } }, bottomBar = { NewAccount(navController) }, content = { contentPadding -> AccountsContent(contentPadding, navController) }, ) } @Composable fun AccountsContent(contentPadding: PaddingValues, navController: NavController) { val showDialog = remember { mutableStateOf(false) } val message = remember { mutableStateOf("") } val lastAction = remember { mutableStateOf({}) } AlertDialog( showDialog = showDialog, title = stringResource(R.string.confirmation), message = message.value, firstButtonText = stringResource(R.string.cancel), lastButtonText = stringResource(R.string.delete), onLastClicked = lastAction.value, ) val showAccounts = remember { mutableStateOf(true) } if (showAccounts.value && BaresipService.uas.value.isNotEmpty()) { val lazyListState = rememberLazyListState() LazyColumn( state = lazyListState, modifier = Modifier .fillMaxWidth() .padding(contentPadding) .padding(start = 16.dp, end = 4.dp, top = 8.dp, bottom = 8.dp) .verticalScrollbar(state = lazyListState), verticalArrangement = Arrangement.spacedBy(4.dp), ) { items(BaresipService.uas.value) { ua -> val account = ua.account val aor = account.aor val text = account.text() Row( verticalAlignment = Alignment.CenterVertically, ) { Text( text = text, fontSize = 20.sp, color = MaterialTheme.colorScheme.onBackground, modifier = Modifier .weight(1f) .padding(start = 10.dp) .clickable { navController.navigate("account/$aor/old") } ) val deleteAccountMessage = stringResource(R.string.delete_account) SmallFloatingActionButton( modifier = Modifier.padding(end = 8.dp), onClick = { message.value = String.format(deleteAccountMessage, text) lastAction.value = { CallHistoryNew.clear(aor) Message.clearMessagesOfAor(aor) ua.remove() Api.ua_destroy(ua.uap) Account.saveAccounts() showAccounts.value = false showAccounts.value = true } showDialog.value = true }, containerColor = MaterialTheme.colorScheme.errorContainer, contentColor = MaterialTheme.colorScheme.onErrorContainer ) { Icon( Icons.Filled.Delete, contentDescription = stringResource(R.string.delete) ) } } } } } } @Composable fun NewAccount(navController: NavController) { val alertTitle = remember { mutableStateOf("") } val alertMessage = remember { mutableStateOf("") } val showAlert = remember { mutableStateOf(false) } if (showAlert.value) AlertDialog( showDialog = showAlert, title = alertTitle.value, message = alertMessage.value, lastButtonText = stringResource(R.string.ok), ) fun createNew(ctx: Context, newAor: String): Account? { val aor = if (newAor.startsWith("sip:")) newAor else "sip:$newAor" if (!Utils.checkAor(aor)) { alertTitle.value = ctx.getString(R.string.notice) alertMessage.value = String.format(ctx.getString(R.string.invalid_aor), aor.split(":")[1]) showAlert.value = true return null } if (Account.ofAor(aor) != null) { alertTitle.value = ctx.getString(R.string.notice) alertMessage.value = String.format(ctx.getString(R.string.account_exists), aor.split(":")[1]) showAlert.value = true return null } val ua = UserAgent.uaAlloc( "<$aor>;stunserver=\"stun:stun.l.google.com:19302\";regq=0.5;pubint=0;regint=0;check_origin=no;mwi=no" ) if (ua == null) { alertTitle.value = ctx.getString(R.string.notice) alertMessage.value = ctx.getString(R.string.account_allocation_failure) showAlert.value = true return null } val acc = ua.account acc.checkOrigin = true Log.d(TAG, "Allocated UA ${ua.uap} with SIP URI ${acc.luri}") Account.saveAccounts() return acc } // createNew var newAor by remember { mutableStateOf("") } val focusManager = LocalFocusManager.current Row( modifier = Modifier .fillMaxWidth() .navigationBarsPadding() .padding(start = 16.dp, end = 8.dp, top = 10.dp, bottom = 16.dp), verticalAlignment = Alignment.CenterVertically ) { val ctx = LocalContext.current val newAccountTitle = stringResource(R.string.new_account) val accountsHelp = stringResource(R.string.accounts_help) OutlinedTextField( value = newAor, placeholder = { Text(text = stringResource(R.string.new_account)) }, onValueChange = { newAor = it }, modifier = Modifier .weight(1f) .padding(end = 8.dp) .verticalScroll(rememberScrollState()) .clickable { alertTitle.value = newAccountTitle alertMessage.value = accountsHelp showAlert.value = true }, singleLine = false, trailingIcon = { if (newAor.isNotEmpty()) { Icon( Icons.Outlined.Clear, contentDescription = "Clear", modifier = Modifier.clickable { newAor = "" }, tint = MaterialTheme.colorScheme.onSurfaceVariant ) } }, label = { Text(stringResource(R.string.new_account)) }, textStyle = TextStyle(fontSize = 18.sp), keyboardOptions = KeyboardOptions( keyboardType = KeyboardType.Text, ) ) SmallFloatingActionButton( modifier = Modifier.offset(y = 2.dp), onClick = { val account = createNew(ctx, newAor.trim()) if (account != null) { navController.navigate("account/${account.aor}/new") newAor = "" focusManager.clearFocus() } }, containerColor = MaterialTheme.colorScheme.secondary, contentColor = MaterialTheme.colorScheme.onSecondary ) { Icon( imageVector = Icons.Filled.Add, modifier = Modifier.size(36.dp), contentDescription = stringResource(R.string.add) ) } } } ================================================ FILE: app/src/main/kotlin/com/tutpro/baresip/AndroidContactScreen.kt ================================================ package com.tutpro.baresip import android.content.Context import android.content.Intent import android.net.Uri import androidx.compose.foundation.Canvas import androidx.compose.foundation.background import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.WindowInsets import androidx.compose.foundation.layout.asPaddingValues import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.imePadding import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.statusBars import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.items import androidx.compose.foundation.lazy.rememberLazyListState import androidx.compose.foundation.shape.CircleShape import androidx.compose.material.icons.Icons import androidx.compose.material.icons.automirrored.filled.ArrowBack import androidx.compose.material.icons.automirrored.filled.Chat import androidx.compose.material.icons.outlined.Call import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.Icon import androidx.compose.material3.IconButton import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Scaffold import androidx.compose.material3.Text import androidx.compose.material3.TopAppBar import androidx.compose.material3.TopAppBarDefaults import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.SolidColor import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import androidx.navigation.NavController import androidx.navigation.NavGraphBuilder import androidx.navigation.NavType import androidx.navigation.compose.composable import androidx.navigation.navArgument import coil.compose.AsyncImage fun NavGraphBuilder.androidContactScreenRoute(navController: NavController, viewModel: ViewModel) { composable( route = "android_contact/{name}", arguments = listOf(navArgument("name") { type = NavType.StringType }) ) { backStackEntry -> val ctx = LocalContext.current val name = backStackEntry.arguments?.getString("name")!! ContactScreen( ctx = ctx, viewModel = viewModel, navController = navController, name = name ) } } @Composable private fun ContactScreen(ctx: Context, viewModel: ViewModel, navController: NavController, name: String) { val contact = Contact.androidContact(name) if (contact == null) { Log.e(TAG, "No Android contact found with name $name") navController.navigateUp() } Scaffold( modifier = Modifier.fillMaxSize().imePadding(), containerColor = MaterialTheme.colorScheme.background, topBar = { Column( modifier = Modifier .fillMaxWidth() .background(MaterialTheme.colorScheme.background) .padding(top = WindowInsets.statusBars.asPaddingValues().calculateTopPadding()) ) { TopAppBar(name, navController) } }, content = { contentPadding -> ContactContent(ctx, viewModel, navController, contentPadding, contact!!) } ) } @OptIn(ExperimentalMaterial3Api::class) @Composable private fun TopAppBar(title: String, navController: NavController) { TopAppBar( title = { Text(text = title, fontWeight = FontWeight.Bold) }, colors = TopAppBarDefaults.topAppBarColors( containerColor = MaterialTheme.colorScheme.primary, navigationIconContentColor = MaterialTheme.colorScheme.onPrimary, titleContentColor = MaterialTheme.colorScheme.onPrimary, ), windowInsets = WindowInsets(0, 0, 0, 0), navigationIcon = { IconButton(onClick = { navController.navigateUp() }) { Icon( imageVector = Icons.AutoMirrored.Filled.ArrowBack, contentDescription = "Back", ) } } ) } @Composable private fun ContactContent( ctx: Context, viewModel: ViewModel, navController: NavController, contentPadding: PaddingValues, contact: Contact.AndroidContact ) { Column( modifier = Modifier .fillMaxWidth() .background(MaterialTheme.colorScheme.background) .padding(contentPadding) .padding(start = 16.dp, end = 16.dp, top = 16.dp, bottom = 52.dp), verticalArrangement = Arrangement.spacedBy(12.dp), ) { Avatar(contact) ContactName(contact.name) Uris(ctx, viewModel, navController, contact) } } @Composable private fun TextAvatar(text: String, color: Int) { Box( modifier = Modifier.size(avatarSize.dp), contentAlignment = Alignment.Center ) { Canvas(modifier = Modifier.fillMaxSize()) { drawCircle(SolidColor(Color(color))) } Text(text, fontSize = 72.sp, color = Color.White) } } @Composable private fun ImageAvatar(uri: Uri) { AsyncImage( model = uri, contentDescription = stringResource(R.string.avatar_image), contentScale = ContentScale.Crop, modifier = Modifier.size(avatarSize.dp).clip(CircleShape) ) } @Composable private fun Avatar(contact: Contact.AndroidContact) { Row( Modifier.fillMaxWidth(), verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.Center ) { val color = contact.color val name = contact.name val thumbnailUri = contact.thumbnailUri if (thumbnailUri != null) ImageAvatar(thumbnailUri) else TextAvatar(if (name == "") "" else name[0].toString(), color) } } @Composable private fun ContactName(name: String) { Row( Modifier.fillMaxWidth().padding(top = 16.dp, bottom = 8.dp), verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.Start ) { Text(name, fontSize = 24.sp, color = MaterialTheme.colorScheme.onBackground) } } @Composable private fun Uris( ctx: Context, viewModel: ViewModel, navController: NavController, contact: Contact.AndroidContact ) { val lazyListState = rememberLazyListState() LazyColumn( modifier = Modifier .fillMaxWidth() .padding(start = 16.dp, end = 4.dp) .background(MaterialTheme.colorScheme.background), state = lazyListState, verticalArrangement = Arrangement.spacedBy(12.dp), ) { items(contact.uris) { uri -> Row( verticalAlignment = Alignment.CenterVertically, ) { Text( text = uri.substringAfter(":"), modifier = Modifier.weight(1f), fontSize = 18.sp, color = MaterialTheme.colorScheme.onBackground, ) // Chat Button IconButton( onClick = { val aor = viewModel.selectedAor.value val ua = UserAgent.ofAor(aor) if (ua == null) Log.w(TAG, "Message clickable did not find AoR $aor") else { val intent = Intent(ctx, MainActivity::class.java) intent.putExtra("uap", ua.uap) intent.putExtra("peer", uri) handleIntent(ctx, viewModel, intent, "message") navController.navigate("main") { popUpTo("main") { inclusive = false } launchSingleTop = true } } } ) { Icon( imageVector = Icons.AutoMirrored.Filled.Chat, contentDescription = "Send Message", tint = MaterialTheme.colorScheme.onBackground ) } // Call Button IconButton( onClick = { val aor = viewModel.selectedAor.value val ua = UserAgent.ofAor(aor) if (ua == null) Log.w(TAG, "Call clickable did not find AoR $aor") else { val intent = Intent(ctx, MainActivity::class.java) intent.putExtra("uap", ua.uap) intent.putExtra("peer", uri) handleIntent(ctx, viewModel, intent, "call") navController.navigate("main") { popUpTo("main") { inclusive = false } launchSingleTop = true } } } ) { Icon( imageVector = Icons.Outlined.Call, contentDescription = "Call", tint = MaterialTheme.colorScheme.onBackground ) } } } } } ================================================ FILE: app/src/main/kotlin/com/tutpro/baresip/Api.kt ================================================ package com.tutpro.baresip object Api { const val VIDMODE_OFF = 0 // const val VIDMODE_ON = 1 const val ANSWERMODE_MANUAL = 0 const val ANSWERMODE_AUTO = 2 const val DTMFMODE_RTP_EVENT = 0 const val DTMFMODE_SIP_INFO = 1 const val DTMFMODE_AUTO = 2 const val REL100_DISABLED = 0 const val REL100_ENABLED = 1 // const cal REL100_REQUIRED = 2 const val SDP_INACTIVE = 0 const val SDP_RECVONLY = 1 // const val SDP_SENDONLY = 2 // const val SDP_SENDRECV = 3 const val CALL_STATE_EARLY = 4 const val REPLACES = 1 external fun account_set_display_name(acc: Long, dn: String): Int external fun account_display_name(acc: Long): String external fun account_aor(acc: Long): String external fun account_luri(acc: Long): String external fun account_auth_user(acc: Long): String external fun account_set_auth_user(acc: Long, user: String): Int external fun account_auth_pass(acc: Long): String external fun account_set_auth_pass(acc: Long, pass: String): Int external fun account_outbound(acc: Long, ix: Int): String external fun account_set_outbound(acc: Long, ob: String, ix: Int): Int external fun account_set_sipnat(acc: Long, sipnat: String): Int external fun account_audio_codec(acc: Long, ix: Int): String external fun account_regint(acc: Long): Int external fun account_set_regint(acc: Long, regint: Int): Int external fun account_check_origin(acc: Long): Boolean external fun account_set_check_origin(acc: Long, check: Boolean) external fun account_stun_uri(acc: Long): String external fun account_set_stun_uri(acc: Long, uri: String): Int external fun account_stun_user(acc: Long): String external fun account_set_stun_user(acc: Long, user: String): Int external fun account_stun_pass(acc: Long): String external fun account_set_stun_pass(acc: Long, pass: String): Int external fun account_mediaenc(acc: Long): String external fun account_set_mediaenc(acc: Long, mediaenc: String): Int external fun account_medianat(acc: Long): String external fun account_set_medianat(acc: Long, medianat: String): Int external fun account_set_audio_codecs(acc: Long, codecs: String): Int external fun account_set_video_codecs(acc: Long, codecs: String): Int external fun account_set_mwi(acc: Long, value: Boolean): Int external fun account_vm_uri(acc: Long): String external fun account_answermode(acc: Long): Int external fun account_set_answermode(acc: Long, mode: Int): Int external fun account_sip_autoredirect(acc: Long): Boolean external fun account_set_sip_autoredirect(acc: Long, allow: Boolean) external fun account_rel100_mode(acc: Long): Int external fun account_set_rel100_mode(acc: Long, mode: Int): Int external fun account_rtcp_mux(acc: Long): Boolean external fun account_set_rtcp_mux(acc: Long, value: Boolean): Int external fun account_dtmfmode(acc: Long): Int external fun account_set_dtmfmode(acc: Long, mode: Int): Int external fun account_extra(acc: Long): String external fun account_debug(acc: Long) external fun uag_reset_transp(register: Boolean, reinvite: Boolean) external fun uag_enable_sip_trace(enable: Boolean) external fun ua_account(uap: Long): Long external fun ua_update_account(uap: Long): Long external fun ua_alloc(uri: String): Long external fun ua_destroy(uap: Long) external fun ua_register(uap: Long): Int @Suppress("unused") external fun ua_isregistered(uap: Long): Boolean external fun ua_unregister(uap: Long) external fun ua_accept(uap: Long, msg: Long) external fun ua_hangup(uap: Long, callp: Long, code: Int, reason: String) external fun ua_call_alloc(uap: Long, xcallp: Long, video: Int): Long external fun ua_answer(uap: Long, callp: Long, video: Int) @Suppress("unused") external fun ua_add_custom_header(uap: Long, name: String, body: String) @Suppress("unused") external fun ua_debug(uap: Long) external fun sip_treply(msg: Long, code: Int, reason: String) external fun bevent_stop(event: Long) external fun call_connect(callp: Long, peer_uri: String): Int external fun call_hold(callp: Long, hold: Boolean): Boolean @Suppress("unused") external fun call_ismuted(callp: Long): Boolean external fun call_transfer(callp: Long, peer_uri: String): Int external fun call_send_digit(callp: Long, digit: Char): Int external fun call_notify_sipfrag(callp: Long, code: Int, reason: String) @Suppress("unused") external fun call_start_audio(callp: Long) external fun call_audio_codecs(callp: Long): String external fun call_duration(callp: Long): Int external fun call_stats(callp: Long, stream: String): String external fun call_state(callp: Long): Int external fun call_replaces(callp: Long): Boolean external fun call_replace_transfer(xfer_callp: Long, callp: Long): Boolean external fun call_peer_uri(callp: Long): String external fun call_diverter_uri(callp: Long): String external fun call_supported(callp: Long, header: Int): Boolean external fun call_destroy(callp: Long) external fun calls_mute(mute: Boolean) external fun message_send(uap: Long, peer_uri: String, message: String, time: String): Int external fun audio_codecs(): String external fun video_codecs(): String external fun log_level_set(level: Int) external fun net_use_nameserver(servers: String): Int external fun net_add_address_ifname(ip_addr: String, if_name: String): Int external fun net_rm_address(ip_addr: String): Int external fun net_debug() @Suppress("unused") external fun net_dns_debug() external fun config_verify_server_set(verify: Boolean) external fun cmd_exec(cmd: String): Int external fun module_load(module: String): Int external fun module_unload(module: String) external fun AAudio_open_stream(): Int external fun AAudio_close_stream() } ================================================ FILE: app/src/main/kotlin/com/tutpro/baresip/AudioScreen.kt ================================================ package com.tutpro.baresip import android.content.Context import androidx.compose.foundation.background import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.WindowInsets import androidx.compose.foundation.layout.asPaddingValues import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.imePadding import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.statusBars import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.text.KeyboardOptions import androidx.compose.foundation.verticalScroll import androidx.compose.material.icons.Icons import androidx.compose.material.icons.automirrored.filled.ArrowBack import androidx.compose.material.icons.filled.ArrowDropDown import androidx.compose.material.icons.filled.Check import androidx.compose.material3.DropdownMenu import androidx.compose.material3.DropdownMenuItem import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.HorizontalDivider import androidx.compose.material3.Icon import androidx.compose.material3.IconButton import androidx.compose.material3.MaterialTheme import androidx.compose.material3.OutlinedTextField import androidx.compose.material3.Scaffold import androidx.compose.material3.Switch import androidx.compose.material3.Text import androidx.compose.material3.TopAppBar import androidx.compose.material3.TopAppBarDefaults import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableIntStateOf import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.input.KeyboardType import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import androidx.navigation.NavController import androidx.navigation.NavGraphBuilder import androidx.navigation.compose.composable import com.tutpro.baresip.CustomElements.AlertDialog import com.tutpro.baresip.CustomElements.verticalScrollbar enum class Result { OK, ERROR, RESTART } fun NavGraphBuilder.audioScreenRoute( navController: NavController, ) { composable("audio") { val ctx = LocalContext.current AudioScreen( onBack = { navController.navigateUp() }, checkOnClick = { when (checkOnClick(ctx)) { Result.OK -> navController.navigateUp() Result.RESTART -> { navController.previousBackStackEntry ?.savedStateHandle ?.set("audio_settings_result", true) navController.navigateUp() } Result.ERROR -> {} } }, ) } } @OptIn(ExperimentalMaterial3Api::class) @Composable private fun AudioScreen( onBack: () -> Unit, checkOnClick: () -> Unit, ) { Scaffold( modifier = Modifier.fillMaxSize().imePadding(), containerColor = MaterialTheme.colorScheme.background, topBar = { Column( modifier = Modifier .fillMaxWidth() .background(MaterialTheme.colorScheme.background) .padding( top = WindowInsets.statusBars.asPaddingValues().calculateTopPadding() ) ) { TopAppBar( title = { Text( text = stringResource(R.string.audio_settings), fontWeight = FontWeight.Bold ) }, colors = TopAppBarDefaults.topAppBarColors( containerColor = MaterialTheme.colorScheme.primary, navigationIconContentColor = MaterialTheme.colorScheme.onPrimary, titleContentColor = MaterialTheme.colorScheme.onPrimary, actionIconContentColor = MaterialTheme.colorScheme.onPrimary ), navigationIcon = { IconButton(onClick = onBack) { Icon( imageVector = Icons.AutoMirrored.Filled.ArrowBack, contentDescription = null, ) } }, windowInsets = WindowInsets(0, 0, 0, 0), actions = { IconButton(onClick = checkOnClick) { Icon( imageVector = Icons.Filled.Check, contentDescription = "Check" ) } }, ) } } ) { contentPadding -> AudioContent(contentPadding) } } private var newCallVolume = BaresipService.callVolume private var oldMicGain = "" private var newMicGain = "" private var oldSpeakerPhone = BaresipService.speakerPhone private var newSpeakerPhone = oldSpeakerPhone private var oldAudioModules = ArrayList() private var newAudioModules = mutableMapOf() private var oldOpusBitrate = "" private var newOpusBitrate = oldOpusBitrate private var oldOpusPacketLoss = "" private var newOpusPacketLoss = oldOpusPacketLoss private var newAudioDelay = BaresipService.audioDelay.toString() private var newToneCountry = BaresipService.toneCountry private var save = false private val alertTitle = mutableStateOf("") private val alertMessage = mutableStateOf("") private val showAlert = mutableStateOf(false) @Composable private fun AudioContent(contentPadding: PaddingValues) { oldAudioModules = Config.variables("module") oldOpusBitrate = Config.variable("opus_bitrate") oldOpusPacketLoss = Config.variable("opus_packet_loss") if (!BaresipService.agcAvailable) oldMicGain = Config.variable("augain") if (showAlert.value) { AlertDialog( showDialog = showAlert, title = alertTitle.value, message = alertMessage.value, lastButtonText = stringResource(R.string.ok), ) } val scrollState = rememberScrollState() Column( modifier = Modifier .fillMaxWidth() .padding(contentPadding) .padding(top = 8.dp, bottom = 8.dp, start = 16.dp, end = 4.dp) .verticalScrollbar(scrollState) .verticalScroll(state = scrollState), verticalArrangement = Arrangement.spacedBy(12.dp), ) { CallVolume() MicGain() SpeakerPhone() AudioModules() OpusBitRate() OpusPacketLoss() AudioDelay() ToneCountry() } } @Composable private fun CallVolume() { Row( Modifier.fillMaxWidth().padding(end=10.dp), verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.Start ) { val defaultCallVolumeTitle = stringResource(R.string.default_call_volume) val defaultCallVolumeHelp = stringResource(R.string.default_call_volume_help) Text(text = defaultCallVolumeTitle, modifier = Modifier.weight(1f) .clickable { alertTitle.value = defaultCallVolumeTitle alertMessage.value = defaultCallVolumeHelp showAlert.value = true }, fontSize = 18.sp) val isDropDownExpanded = remember { mutableStateOf(false) } val volNames = listOf("--", "1", "2", "3", "4", "5", "6", "7", "8", "9", "10") val volValues = listOf(0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10) val itemPosition = remember { mutableIntStateOf(volValues.indexOf(BaresipService.callVolume)) } Box { Row( horizontalArrangement = Arrangement.Center, verticalAlignment = Alignment.CenterVertically, modifier = Modifier.clickable { isDropDownExpanded.value = true } ) { Text(text = volNames[itemPosition.intValue]) Icon( imageVector = Icons.Filled.ArrowDropDown, contentDescription = null, modifier = Modifier.size(36.dp) ) } DropdownMenu( expanded = isDropDownExpanded.value, onDismissRequest = { isDropDownExpanded.value = false }) { volNames.forEachIndexed { index, vol -> DropdownMenuItem(text = { Text(text = vol) }, onClick = { isDropDownExpanded.value = false itemPosition.intValue = index newCallVolume = volValues[index] }) if (index < 10) HorizontalDivider(thickness = 1.dp) } } } } } @Composable private fun MicGain() { if (!BaresipService.agcAvailable) Row( Modifier.fillMaxWidth().padding(end = 10.dp), verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.Start ) { val microphoneGainTitle = stringResource(R.string.microphone_gain) val microphoneGainHelp = stringResource(R.string.microphone_gain_help) var micGain by remember { mutableStateOf(oldMicGain) } newMicGain = micGain OutlinedTextField( value = micGain, placeholder = { Text(microphoneGainTitle) }, onValueChange = { micGain = it newMicGain = micGain }, modifier = Modifier .fillMaxWidth() .clickable { alertTitle.value = microphoneGainTitle alertMessage.value = microphoneGainHelp showAlert.value = true }, textStyle = androidx.compose.ui.text.TextStyle(fontSize = 18.sp), label = { Text(microphoneGainTitle) }, keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Text) ) } } @Composable private fun SpeakerPhone() { Row( Modifier.fillMaxWidth().padding(end=10.dp), verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.Start ) { val speakerPhoneTitle = stringResource(R.string.speaker_phone) val speakerPhoneHelp = stringResource(R.string.speaker_phone_help) Text(text = speakerPhoneTitle, modifier = Modifier.weight(1f) .clickable { alertTitle.value = speakerPhoneTitle alertMessage.value = speakerPhoneHelp showAlert.value = true }, fontSize = 18.sp) var speakerPhone by remember { mutableStateOf(BaresipService.speakerPhone) } Switch( checked = speakerPhone, onCheckedChange = { speakerPhone = it newSpeakerPhone = speakerPhone } ) } } @Composable private fun AudioModules() { Column( modifier = Modifier.fillMaxWidth(), horizontalAlignment = Alignment.Start, ) { val audioModulesTitle = stringResource(R.string.audio_modules_title) val audioModulesHelp = stringResource(R.string.audio_modules_help) Text(text = audioModulesTitle, fontSize = 18.sp, modifier = Modifier.clickable { alertTitle.value = audioModulesTitle alertMessage.value = audioModulesHelp showAlert.value = true }) for (module in Config.audioModules) { Row(horizontalArrangement = Arrangement.Start, verticalAlignment = Alignment.CenterVertically, modifier = Modifier.padding(start = 18.dp, end = 10.dp) ) { Text(text = String.format(stringResource(R.string.bullet_item), module), fontSize = 18.sp) Spacer(modifier = Modifier.weight(1f)) var checked by remember { mutableStateOf(oldAudioModules.contains("${module}.so")) } Switch( checked = checked, onCheckedChange = { checked = it newAudioModules[module] = checked } ) } } } } @Composable private fun OpusBitRate() { Row( Modifier.fillMaxWidth().padding(end = 10.dp), verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.Start ) { val opusBitRateTitle = stringResource(R.string.opus_bit_rate) val opusBitRateHelp = stringResource(R.string.opus_bit_rate_help) var opusBitrate by remember { mutableStateOf(oldOpusBitrate) } newOpusBitrate = opusBitrate OutlinedTextField( value = opusBitrate, placeholder = { Text(opusBitRateTitle) }, onValueChange = { opusBitrate = it newOpusBitrate = opusBitrate }, modifier = Modifier .fillMaxWidth() .clickable { alertTitle.value = opusBitRateTitle alertMessage.value = opusBitRateHelp showAlert.value = true }, textStyle = androidx.compose.ui.text.TextStyle(fontSize = 18.sp), label = { Text(opusBitRateTitle) }, keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Text) ) } } @Composable private fun OpusPacketLoss() { Row( Modifier.fillMaxWidth().padding(end = 10.dp, top = 8.dp), verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.Start ) { val opusPacketLossTitle = stringResource(R.string.opus_packet_loss) val opusPacketLossHelp = stringResource(R.string.opus_packet_loss_help) var opusPacketLoss by remember { mutableStateOf(oldOpusPacketLoss) } newOpusPacketLoss = opusPacketLoss OutlinedTextField( value = opusPacketLoss, placeholder = { Text(opusPacketLossTitle) }, onValueChange = { opusPacketLoss = it newOpusPacketLoss = opusPacketLoss }, modifier = Modifier .fillMaxWidth() .clickable { alertTitle.value = opusPacketLossTitle alertMessage.value = opusPacketLossHelp showAlert.value = true }, textStyle = androidx.compose.ui.text.TextStyle(fontSize = 18.sp), label = { Text(opusPacketLossTitle) }, keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Text) ) } } @Composable private fun AudioDelay() { Row( Modifier.fillMaxWidth().padding(end = 10.dp, top = 8.dp), verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.Start ) { val audioDelayTitle = stringResource(R.string.audio_delay) val audioDelayHelp = stringResource(R.string.audio_delay_help) var audioDelay by remember { mutableStateOf(BaresipService.audioDelay.toString()) } newAudioDelay = audioDelay OutlinedTextField( value = audioDelay, placeholder = { Text(audioDelayTitle) }, onValueChange = { audioDelay = it newAudioDelay = audioDelay }, modifier = Modifier .fillMaxWidth() .clickable { alertTitle.value = audioDelayTitle alertMessage.value = audioDelayHelp showAlert.value = true }, textStyle = androidx.compose.ui.text.TextStyle(fontSize = 18.sp), label = { Text(audioDelayTitle) }, keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Text) ) } } @Composable private fun ToneCountry() { Row( Modifier.fillMaxWidth().padding(end=10.dp), verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.Start ) { val toneCountryTitle = stringResource(R.string.tone_country) val toneCountryHelp = stringResource(R.string.tone_country_help) Text(text = toneCountryTitle, modifier = Modifier.weight(1f) .clickable { alertTitle.value = toneCountryTitle alertMessage.value = toneCountryHelp showAlert.value = true }, fontSize = 18.sp) val isDropDownExpanded = remember { mutableStateOf(false) } val countryNames = arrayListOf("BG", "BR", "DE", "CZ", "ES", "FI", "FR", "GB", "JP", "NO", "NZ", "SE", "RU", "US") val countryValues = arrayListOf("bg", "br", "de", "cz", "es", "fi", "fr", "uk", "jp", "no", "nz", "se", "ru", "us") val itemPosition = remember { mutableIntStateOf(countryValues.indexOf(BaresipService.toneCountry)) } Box { Row( horizontalArrangement = Arrangement.Center, verticalAlignment = Alignment.CenterVertically, modifier = Modifier.clickable { isDropDownExpanded.value = true } ) { Text(text = countryNames[itemPosition.intValue]) Icon( imageVector = Icons.Filled.ArrowDropDown, contentDescription = null, modifier = Modifier.size(36.dp) ) } DropdownMenu( expanded = isDropDownExpanded.value, onDismissRequest = { isDropDownExpanded.value = false }) { countryNames.forEachIndexed { index, name -> DropdownMenuItem(text = { Text(text = name) }, onClick = { isDropDownExpanded.value = false itemPosition.intValue = index newToneCountry = countryValues[index] }) if (index < 10) HorizontalDivider(thickness = 1.dp) } } } } } private fun checkOnClick(ctx: Context): Result { var restart = false if (BaresipService.callVolume != newCallVolume) { BaresipService.callVolume = newCallVolume Config.replaceVariable("call_volume", newCallVolume.toString()) save = true } if (!BaresipService.agcAvailable) { var gain = newMicGain.trim() if (!gain.contains(".")) gain = "$gain.0" if (gain != oldMicGain) { if (!checkMicGain(gain)) { alertTitle.value = ctx.getString(R.string.notice) alertMessage.value = "${ctx.getString(R.string.invalid_microphone_gain)}: $gain." showAlert.value = true return Result.ERROR } if (gain == "1.0") { Api.module_unload("augain") Config.removeVariableValue("module", "augain.so") Config.replaceVariable("augain", "1.0") } else { if (oldMicGain == "1.0") { if (Api.module_load("augain") != 0) { alertTitle.value = ctx.getString(R.string.error) alertMessage.value = ctx.getString(R.string.failed_to_load_module) + ": augain.so" showAlert.value = true return Result.ERROR } Config.addVariable("module", "augain.so") } Config.replaceVariable("augain", gain) Api.cmd_exec("augain $gain") } save = true } } if (newSpeakerPhone != BaresipService.speakerPhone) { BaresipService.speakerPhone = newSpeakerPhone Config.replaceVariable("speaker_phone", if (BaresipService.speakerPhone) "yes" else "no") save = true } for (module in Config.audioModules) { if (newAudioModules[module] != null) { if (newAudioModules[module]!!) { if (!oldAudioModules.contains("${module}.so")) { if (Api.module_load("${module}.so") != 0) { alertTitle.value = ctx.getString(R.string.error) alertMessage.value = "${ctx.getString(R.string.failed_to_load_module)}: ${module}.so" showAlert.value = true return Result.ERROR } Config.addVariable("module", "${module}.so") save = true } } else if (oldAudioModules.contains("${module}.so")) { Api.module_unload("${module}.so") Config.removeVariableValue("module", "${module}.so") for (ua in BaresipService.uas.value) ua.account.removeAudioCodecs(module) Account.saveAccounts() save = true } } } if (newOpusBitrate != oldOpusBitrate) { if (!checkOpusBitRate(newOpusBitrate)) { alertTitle.value = ctx.getString(R.string.notice) alertMessage.value = "${ctx.getString(R.string.invalid_opus_bitrate)}: $newOpusBitrate." showAlert.value = true return Result.ERROR } Config.replaceVariable("opus_bitrate", newOpusBitrate) restart = true save = true } if (newOpusPacketLoss != oldOpusPacketLoss) { if (!checkOpusPacketLoss(newOpusPacketLoss)) { alertTitle.value = ctx.getString(R.string.notice) alertMessage.value = "${ctx.getString(R.string.invalid_opus_packet_loss)}: $newOpusPacketLoss" showAlert.value = true return Result.ERROR } Config.replaceVariable("opus_packet_loss", newOpusPacketLoss) restart = true save = true } val audioDelay = newAudioDelay.trim() if (audioDelay != BaresipService.audioDelay.toString()) { if (!checkAudioDelay(audioDelay)) { alertTitle.value = ctx.getString(R.string.notice) alertMessage.value = String.format(ctx.getString(R.string.invalid_audio_delay), audioDelay) showAlert.value = true return Result.ERROR } Config.replaceVariable("audio_delay", audioDelay) BaresipService.audioDelay = audioDelay.toLong() save = true } if (BaresipService.toneCountry != newToneCountry) { BaresipService.toneCountry = newToneCountry Config.replaceVariable("tone_country", newToneCountry) save = true } if (save) Config.save() return if (restart) Result.RESTART else Result.OK } private fun checkMicGain(micGain: String): Boolean { val number = try { micGain.toDouble() } catch (_: NumberFormatException) { return false } return number >= 1.0 } private fun checkOpusBitRate(opusBitRate: String): Boolean { val number = opusBitRate.toIntOrNull() ?: return false return (number >= 6000) && (number <= 510000) } private fun checkOpusPacketLoss(opusPacketLoss: String): Boolean { val number = opusPacketLoss.toIntOrNull() ?: return false return (number >= 0) && (number <= 100) } private fun checkAudioDelay(audioDelay: String): Boolean { val number = audioDelay.toIntOrNull() ?: return false return (number >= 100) && (number <= 3000) } ================================================ FILE: app/src/main/kotlin/com/tutpro/baresip/BaresipApp.kt ================================================ package com.tutpro.baresip import android.app.Application class BaresipApp : Application() { override fun onCreate() { super.onCreate() if (!BaresipService.libraryLoaded) { Log.i(TAG, "Loading baresip library") System.loadLibrary("baresip") BaresipService.libraryLoaded = true } } } ================================================ FILE: app/src/main/kotlin/com/tutpro/baresip/BaresipContactScreen.kt ================================================ package com.tutpro.baresip import android.content.ContentProviderOperation import android.content.ContentValues import android.content.Context import android.database.Cursor import android.graphics.Bitmap import android.graphics.BitmapFactory import android.graphics.Matrix import android.net.Uri import android.provider.ContactsContract import android.provider.ContactsContract.CommonDataKinds import android.provider.ContactsContract.Contacts.Data import androidx.activity.compose.BackHandler import androidx.activity.compose.rememberLauncherForActivityResult import androidx.activity.result.contract.ActivityResultContracts import androidx.compose.foundation.Canvas import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.foundation.Image import androidx.compose.foundation.background import androidx.compose.foundation.clickable import androidx.compose.foundation.combinedClickable import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.WindowInsets import androidx.compose.foundation.layout.asPaddingValues import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.imePadding import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.statusBars import androidx.compose.foundation.shape.CircleShape import androidx.compose.foundation.text.KeyboardOptions import androidx.compose.material.icons.Icons import androidx.compose.material.icons.automirrored.filled.ArrowBack import androidx.compose.material.icons.filled.Check import androidx.compose.material3.CircularProgressIndicator import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.Icon import androidx.compose.material3.IconButton import androidx.compose.material3.MaterialTheme import androidx.compose.material3.OutlinedTextField import androidx.compose.material3.Scaffold import androidx.compose.material3.Switch import androidx.compose.material3.Text import androidx.compose.material3.TopAppBar import androidx.compose.material3.TopAppBarDefaults import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip import androidx.compose.ui.focus.FocusRequester import androidx.compose.ui.focus.focusRequester import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.SolidColor import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.input.KeyboardCapitalization import androidx.compose.ui.text.input.KeyboardType import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import androidx.core.graphics.scale import androidx.exifinterface.media.ExifInterface import androidx.navigation.NavController import androidx.navigation.NavGraphBuilder import androidx.navigation.NavType import androidx.navigation.compose.composable import androidx.navigation.navArgument import coil.compose.rememberAsyncImagePainter import com.tutpro.baresip.CustomElements.AlertDialog import java.io.ByteArrayOutputStream import java.io.File import java.io.FileOutputStream fun NavGraphBuilder.baresipContactScreenRoute(navController: NavController) { composable( route = "baresip_contact/{uri_or_name}/{kind}", arguments = listOf( navArgument("uri_or_name") { type = NavType.StringType }, navArgument("kind") { type = NavType.StringType } ) ) { backStackEntry -> val uriOrNameArg = backStackEntry.arguments?.getString("uri_or_name")!! val kindArg = backStackEntry.arguments?.getString("kind")!! ContactScreen( navController = navController, uriOrNameArg = uriOrNameArg, kindArg = kindArg ) } } private data class ScreenState( val new: Boolean = false, val favorite: Boolean = false, val android: Boolean = false, val id: Long = 0, val newId: Long = 0, val name: String = "", val uri: String = "", val color: Int = 0, val avatarImageUri: String? = null, val tmpAvatarFile: File? = null, val isLoading: Boolean = true ) @OptIn(ExperimentalMaterial3Api::class) @Composable private fun ContactScreen( navController: NavController, uriOrNameArg: String, kindArg: String ) { val ctx = LocalContext.current var screenState by remember { mutableStateOf(ScreenState()) } val title = if (screenState.new) stringResource(R.string.new_contact) else uriOrNameArg LaunchedEffect(key1 = uriOrNameArg, key2 = kindArg) { val isNew = kindArg == "new" if (isNew) { val time = System.currentTimeMillis() screenState = ScreenState( new = true, name = "", uri = uriOrNameArg, favorite = false, android = BaresipService.contactsMode == "android", color = Utils.randomColor(), id = time, newId = time, isLoading = false ) } else { val contact = Contact.baresipContact(uriOrNameArg)!! val avatarFile = File(BaresipService.filesPath, "${contact.id}.png") screenState = ScreenState( new = false, name = uriOrNameArg, uri = contact.uri, favorite = contact.favorite, android = false, color = contact.color, id = contact.id, newId = contact.id, avatarImageUri = if (contact.avatarImage != null && avatarFile.exists()) Uri.fromFile(avatarFile).toString() else null, isLoading = false ) } } val onBack: () -> Unit = { screenState.tmpAvatarFile?.let { tempFile -> if (tempFile.exists()) { Log.d(TAG, "Back pressed, deleting temp avatar: ${tempFile.name}") Utils.deleteFile(tempFile) } } navController.navigateUp() } val onCheck: () -> Unit = { val result = checkOnClick( ctx = ctx, currentState = screenState, uriOrNameArg = uriOrNameArg, ) if (result) navController.navigateUp() } BackHandler(enabled = true) { onBack() } Scaffold( modifier = Modifier .fillMaxSize() .imePadding(), containerColor = MaterialTheme.colorScheme.background, topBar = { Column( modifier = Modifier .fillMaxWidth() .background(MaterialTheme.colorScheme.background) .padding(top = WindowInsets.statusBars.asPaddingValues().calculateTopPadding()) ) { TopAppBar(title, onBack = onBack, onCheck = onCheck) } }, content = { contentPadding -> if (!screenState.isLoading) { ContactContent( contentPadding = contentPadding, screenState = screenState, onStateChange = { newState -> screenState = newState } ) } else { // Optional: Show a loading indicator Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) { CircularProgressIndicator() } } } ) } @OptIn(ExperimentalMaterial3Api::class) @Composable private fun TopAppBar(title: String, onBack: () -> Unit, onCheck: () -> Unit) { TopAppBar( title = { Text(text = title, fontWeight = FontWeight.Bold) }, colors = TopAppBarDefaults.topAppBarColors( containerColor = MaterialTheme.colorScheme.primary, navigationIconContentColor = MaterialTheme.colorScheme.onPrimary, titleContentColor = MaterialTheme.colorScheme.onPrimary, actionIconContentColor = MaterialTheme.colorScheme.onPrimary ), windowInsets = WindowInsets(0, 0, 0, 0), navigationIcon = { IconButton(onClick = onBack) { Icon( imageVector = Icons.AutoMirrored.Filled.ArrowBack, contentDescription = "Back", ) } }, actions = { IconButton(onClick = onCheck) { Icon( imageVector = Icons.Filled.Check, contentDescription = "Check" ) } } ) } private val alertTitle = mutableStateOf("") private val alertMessage = mutableStateOf("") private val showAlert = mutableStateOf(false) @OptIn(ExperimentalFoundationApi::class) @Composable private fun ContactContent( contentPadding: PaddingValues, screenState: ScreenState, onStateChange: (ScreenState) -> Unit ) { val ctx = LocalContext.current if (showAlert.value) { AlertDialog( showDialog = showAlert, title = alertTitle.value, message = alertMessage.value, lastButtonText = stringResource(R.string.ok), ) } Column( modifier = Modifier .fillMaxWidth() .background(MaterialTheme.colorScheme.background) .padding(contentPadding) .padding(start = 16.dp, end = 16.dp, top = 16.dp, bottom = 52.dp), verticalArrangement = Arrangement.spacedBy(12.dp), ) { Avatar( ctx = ctx, name = screenState.name, color = screenState.color, currentAvatarUri = screenState.avatarImageUri, onNewAvatarChosen = { processedTmpFile, generatedNewId -> onStateChange( screenState.copy( avatarImageUri = Uri.fromFile(processedTmpFile).toString(), tmpAvatarFile = processedTmpFile, newId = generatedNewId ) ) }, onAvatarColorChange = { newRandomColor -> // User long-clicked to change color, this means discarding any image. // The old tempAvatarFile (if any) should be deleted. screenState.tmpAvatarFile?.let { if (it.exists()) Utils.deleteFile(it) } onStateChange( screenState.copy( color = newRandomColor, avatarImageUri = null, tmpAvatarFile = null ) ) } ) ContactName( name = screenState.name, new = screenState.new, onNameChange = { newName -> onStateChange(screenState.copy(name = newName)) } ) ContactUri( uri = screenState.uri, onUriChange = { newUri -> onStateChange(screenState.copy(uri = newUri)) } ) Favorite( ctx = ctx, favorite = screenState.favorite, onFavoriteChange = { newFavorite -> onStateChange(screenState.copy(favorite = newFavorite)) } ) if (screenState.new && BaresipService.contactsMode == "both") Android( ctx = ctx, android = screenState.android, onAndroidChange = { newAndroid -> onStateChange(screenState.copy(android = newAndroid)) } ) } } @OptIn(ExperimentalFoundationApi::class) @Composable private fun Avatar( ctx: Context, name: String, color: Int, currentAvatarUri: String?, onNewAvatarChosen: (newImageFile: File, newImageId: Long) -> Unit, onAvatarColorChange: (newColor: Int) -> Unit ) { val avatarImagePicker = rememberLauncherForActivityResult(ActivityResultContracts.GetContent()) { uri: Uri? -> if (uri != null) { try { val inputStream = ctx.contentResolver.openInputStream(uri) val avatarBitmap = BitmapFactory.decodeStream(inputStream) inputStream?.close() if (avatarBitmap == null) { Log.e(TAG, "Failed to decode bitmap from URI: $uri") return@rememberLauncherForActivityResult } val scaledBitmap = avatarBitmap.scale(192, 192) // Define desired scale val orientationInputStream = ctx.contentResolver.openInputStream(uri) val exif = if (orientationInputStream != null) ExifInterface(orientationInputStream) else null orientationInputStream?.close() val orientation = exif?.getAttributeInt( ExifInterface.TAG_ORIENTATION, ExifInterface.ORIENTATION_NORMAL ) ?: ExifInterface.ORIENTATION_NORMAL val rotatedBitmap = rotateBitmap(scaledBitmap, orientation) val newImageId = System.currentTimeMillis() val tempNewImageFile = File(BaresipService.filesPath, "${newImageId}.png") if (saveBitmap(rotatedBitmap, tempNewImageFile)) { onNewAvatarChosen(tempNewImageFile, newImageId) } else { Log.e(TAG, "Failed to save processed avatar image to ${tempNewImageFile.absolutePath}") if (tempNewImageFile.exists()) Utils.deleteFile(tempNewImageFile) } } catch (e: Exception) { Log.e(TAG, "Could not process avatar image: ${e.message}") } } } Row( Modifier.fillMaxWidth(), verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.Center ) { Box( contentAlignment = Alignment.Center, modifier = Modifier .size(avatarSize.dp) .clip(CircleShape) .background(if (currentAvatarUri == null) Color(color) else Color.Transparent) .combinedClickable( onClick = { avatarImagePicker.launch("image/*") }, onLongClick = { onAvatarColorChange(Utils.randomColor()) } ) ) { if (currentAvatarUri == null) { Box( modifier = Modifier.size(avatarSize.dp), contentAlignment = Alignment.Center ) { Canvas(modifier = Modifier.fillMaxSize()) { drawCircle(SolidColor(Color(color))) } val text = if (name.isNotBlank()) name.take(1).uppercase() else "?" Text(text, fontSize = 72.sp, color = Color.White) } } else { Image( painter = rememberAsyncImagePainter(model = currentAvatarUri), contentDescription = stringResource(R.string.avatar_image), contentScale = ContentScale.Crop, modifier = Modifier.size(avatarSize.dp).clip(CircleShape) ) } } } } @Composable private fun ContactName(name: String, new: Boolean, onNameChange: (String) -> Unit) { val focusRequester = remember { FocusRequester() } OutlinedTextField( value = name, placeholder = { Text(stringResource(R.string.contact_name)) }, onValueChange = onNameChange, modifier = Modifier .fillMaxWidth() .focusRequester(focusRequester), textStyle = androidx.compose.ui.text.TextStyle(fontSize = 18.sp), label = { Text(stringResource(R.string.contact_name)) }, keyboardOptions = KeyboardOptions( keyboardType = KeyboardType.Text, capitalization = KeyboardCapitalization.Words ) ) LaunchedEffect(new) { if (new) focusRequester.requestFocus() } } @Composable private fun ContactUri(uri: String, onUriChange: (String) -> Unit) { OutlinedTextField( value = uri, placeholder = { Text(stringResource(R.string.user_domain_or_number)) }, onValueChange = onUriChange, modifier = Modifier.fillMaxWidth(), textStyle = androidx.compose.ui.text.TextStyle(fontSize = 18.sp), label = { Text(stringResource(R.string.sip_or_tel_uri)) }, keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Text) ) } @Composable private fun Favorite(ctx: Context, favorite: Boolean, onFavoriteChange: (Boolean) -> Unit) { Row( Modifier.fillMaxWidth(), verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.Start ) { Text( text = stringResource(R.string.favorite), modifier = Modifier.weight(1f) .clickable { alertTitle.value = ctx.getString(R.string.favorite) alertMessage.value = ctx.getString(R.string.favorite_help) showAlert.value = true }, ) Switch( checked = favorite, onCheckedChange = onFavoriteChange ) } } @Composable private fun Android(ctx: Context, android: Boolean, onAndroidChange: (Boolean) -> Unit) { Row( Modifier.fillMaxWidth(), verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.Start ) { Text( text = stringResource(R.string.android), modifier = Modifier.weight(1f) .clickable { alertTitle.value = ctx.getString(R.string.android) alertMessage.value = ctx.getString(R.string.android_contact_help) showAlert.value = true }, ) Switch( checked = android, onCheckedChange = onAndroidChange ) } } private fun checkOnClick( ctx: Context, currentState: ScreenState, uriOrNameArg: String ): Boolean { var newUri = currentState.uri.filterNot{setOf('-', ' ', '(', ')').contains(it)} if (!newUri.startsWith("sip:") && !newUri.startsWith("tel:")) newUri = if (Utils.isTelNumber(newUri)) "tel:$newUri" else "sip:$newUri" if (!Utils.checkUri(newUri)) { alertTitle.value = ctx.getString(R.string.notice) alertMessage.value = String.format(ctx.getString(R.string.invalid_sip_or_tel_uri), newUri) showAlert.value = true return false } var newName = currentState.name.trim() if (newName == "") newName = newUri.substringAfter(":") if (!Utils.checkName(newName)) { alertTitle.value = ctx.getString(R.string.notice) alertMessage.value = String.format(ctx.getString(R.string.invalid_contact), newName) showAlert.value = true return false } val alert: Boolean = if (currentState.new) Contact.nameExists(newName, BaresipService.contacts, true) else { (uriOrNameArg != newName) && Contact.nameExists(newName, BaresipService.contacts, false) } if (alert) { alertTitle.value = ctx.getString(R.string.notice) alertMessage.value = String.format(ctx.getString(R.string.contact_already_exists), newName) showAlert.value = true return false } var idToUse = currentState.id if (currentState.tmpAvatarFile != null && currentState.tmpAvatarFile.exists()) { if (currentState.id != currentState.newId) { // Avatar changed, implying new ID val oldAvatar = File(BaresipService.filesPath, "${currentState.id}.png") if (oldAvatar.exists()) Utils.deleteFile(oldAvatar) idToUse = currentState.newId } } else if (currentState.avatarImageUri == null) { // Avatar was explicitly cleared val avatarFile = File(BaresipService.filesPath, "$idToUse.png") if (avatarFile.exists()) Utils.deleteFile(avatarFile) } val contact: Contact.BaresipContact = Contact.BaresipContact( newName, newUri, currentState.color, idToUse, currentState.favorite ) if (currentState.avatarImageUri == null) contact.avatarImage = null else { val imageFilePath = BaresipService.filesPath + "/${idToUse}.png" contact.avatarImage = BitmapFactory.decodeFile(imageFilePath) } if (currentState.android) { addOrUpdateAndroidContact(ctx, contact) if (contact.favorite) { val contentValues = ContentValues() contentValues.put(ContactsContract.Contacts.STARRED, 1) try { ctx.contentResolver.update( ContactsContract.RawContacts.CONTENT_URI, contentValues, ContactsContract.Contacts.DISPLAY_NAME + "='" + newName + "'", null ) } catch (e: Exception) { Log.e(TAG, "Update of Android favorite failed: ${e.message}") } } } else { if (currentState.new) Contact.addBaresipContact(contact) else Contact.updateBaresipContact(currentState.id, contact) } return true } private fun rotateBitmap(bitmap: Bitmap, orientation: Int): Bitmap { val matrix = Matrix() when (orientation) { ExifInterface.ORIENTATION_NORMAL -> return bitmap ExifInterface.ORIENTATION_FLIP_HORIZONTAL -> matrix.setScale(-1f, 1f) ExifInterface.ORIENTATION_ROTATE_180 -> matrix.setRotate(180f) ExifInterface.ORIENTATION_FLIP_VERTICAL -> { matrix.setRotate(180f) matrix.postScale(-1f, 1f) } ExifInterface.ORIENTATION_TRANSPOSE -> { matrix.setRotate(90f) matrix.postScale(-1f, 1f) } ExifInterface.ORIENTATION_ROTATE_90 -> matrix.setRotate(90f) ExifInterface.ORIENTATION_TRANSVERSE -> { matrix.setRotate(-90f) matrix.postScale(-1f, 1f) } ExifInterface.ORIENTATION_ROTATE_270 -> matrix.setRotate(-90f) else -> return bitmap } val rotatedBitmap = Bitmap.createBitmap(bitmap, 0, 0, bitmap.width, bitmap.height, matrix, true) bitmap.recycle() return rotatedBitmap } private fun saveBitmap(bitmap: Bitmap, file: File): Boolean { if (file.exists()) file.delete() try { val out = FileOutputStream(file) val scaledBitmap = bitmap.scale(avatarSize, avatarSize) scaledBitmap.compress(Bitmap.CompressFormat.PNG, 100, out) out.flush() out.close() Log.d(TAG, "Saved bitmap to ${file.absolutePath} of length ${file.length()}") } catch (e: Exception) { Log.e(TAG, "Failed to save bitmap to ${file.absolutePath}: $e") return false } return true } private fun addOrUpdateAndroidContact(ctx: Context, contact: Contact.BaresipContact) { val projection = arrayOf(ContactsContract.Data.RAW_CONTACT_ID) val selection = ContactsContract.Data.MIMETYPE + "='" + CommonDataKinds.StructuredName.CONTENT_ITEM_TYPE + "' AND " + CommonDataKinds.StructuredName.DISPLAY_NAME + "='" + contact.name + "'" val c: Cursor? = ctx.contentResolver.query( ContactsContract.Data.CONTENT_URI, projection, selection, null, null) if (c != null && c.moveToFirst()) { updateAndroidContact(ctx, c.getLong(0), contact) } else { addAndroidContact(ctx, contact) } c?.close() } private fun addAndroidContact(ctx: Context, contact: Contact.BaresipContact): Boolean { val ops = ArrayList() ops.add( ContentProviderOperation.newInsert(ContactsContract.RawContacts.CONTENT_URI) .withValue(ContactsContract.RawContacts.ACCOUNT_TYPE, null) .withValue(ContactsContract.RawContacts.ACCOUNT_NAME, null).build()) ops.add( ContentProviderOperation .newInsert(ContactsContract.Data.CONTENT_URI) .withValueBackReference(Data.RAW_CONTACT_ID, 0) .withValue(Data.MIMETYPE, CommonDataKinds.StructuredName.CONTENT_ITEM_TYPE) .withValue(CommonDataKinds.StructuredName.DISPLAY_NAME, contact.name) .build()) val mimeType = if (contact.uri.startsWith("sip:")) CommonDataKinds.SipAddress.CONTENT_ITEM_TYPE else CommonDataKinds.Phone.CONTENT_ITEM_TYPE ops.add( ContentProviderOperation .newInsert(ContactsContract.Data.CONTENT_URI) .withValueBackReference(Data.RAW_CONTACT_ID, 0) .withValue(Data.MIMETYPE, mimeType) .withValue(Data.DATA1, contact.uri.substringAfter(":")) .build()) if (contact.avatarImage != null) { val photoData: ByteArray? = bitmapToPNGByteArray(contact.avatarImage!!) if (photoData != null) { ops.add( ContentProviderOperation .newInsert(ContactsContract.Data.CONTENT_URI) .withValueBackReference(Data.RAW_CONTACT_ID, 0) .withValue(Data.MIMETYPE, CommonDataKinds.Photo.CONTENT_ITEM_TYPE) .withValue(CommonDataKinds.Photo.PHOTO, photoData) .build()) } } try { ctx.contentResolver.applyBatch(ContactsContract.AUTHORITY, ops) } catch (e: Exception) { Log.e(TAG, "Adding of contact ${contact.name} failed: ${e.message}") return false } return true } private fun updateAndroidContact(ctx: Context, rawContactId: Long, contact: Contact.BaresipContact) { if (updateAndroidUri(ctx, rawContactId, contact.uri) == 0) addAndroidUri(ctx, rawContactId, contact.uri) if (updateAndroidPhoto(ctx, rawContactId, contact.avatarImage) == 0) if (contact.avatarImage != null) addAndroidPhoto(ctx, rawContactId, contact.avatarImage!!) } private fun addAndroidUri(ctx: Context, rawContactId: Long, uri: String) { val mimeType = if (uri.startsWith("sip:")) CommonDataKinds.SipAddress.CONTENT_ITEM_TYPE else CommonDataKinds.Phone.CONTENT_ITEM_TYPE val ops = ArrayList() ops.add( ContentProviderOperation .newInsert(ContactsContract.Data.CONTENT_URI) .withValue(Data.RAW_CONTACT_ID, rawContactId) .withValue(Data.MIMETYPE, mimeType) .withValue(Data.DATA1, uri.substringAfter(":")) .build()) try { ctx.contentResolver.applyBatch(ContactsContract.AUTHORITY, ops) } catch (e: Exception) { Log.e(TAG, "Adding of SIP URI $uri failed: ${e.message}") } } private fun updateAndroidUri(ctx: Context, rawContactId: Long, uri: String): Int { val mimeType = if (uri.startsWith("sip:")) CommonDataKinds.SipAddress.CONTENT_ITEM_TYPE else CommonDataKinds.Phone.CONTENT_ITEM_TYPE val contentValues = ContentValues() contentValues.put(ContactsContract.Data.DATA1, uri) val where = "${ContactsContract.Data.RAW_CONTACT_ID}=$rawContactId and " + "${ContactsContract.Data.MIMETYPE}='$mimeType'" return try { ctx.contentResolver.update(ContactsContract.Data.CONTENT_URI, contentValues, where, null) } catch (e: Exception) { Log.e(TAG, "Update of Android URI $uri failed: ${e.message}") 0 } } private fun addAndroidPhoto(ctx: Context, rawContactId: Long, photoBits: Bitmap) { val photoBytes = bitmapToPNGByteArray(photoBits) if (photoBytes != null) { val ops = ArrayList() ops.add( ContentProviderOperation .newInsert(ContactsContract.Data.CONTENT_URI) .withValue(Data.RAW_CONTACT_ID, rawContactId) .withValue(Data.MIMETYPE, CommonDataKinds.Photo.CONTENT_ITEM_TYPE) .withValue(CommonDataKinds.Photo.PHOTO, photoBytes) .build()) try { ctx.contentResolver.applyBatch(ContactsContract.AUTHORITY, ops) } catch (e: Exception) { Log.e(TAG, "Adding of Android photo failed: ${e.message}") } } } private fun updateAndroidPhoto(ctx: Context, rawContactId: Long, photoBits: Bitmap?): Int { val photoBytes = if (photoBits == null) null else bitmapToPNGByteArray(photoBits) val contentValues = ContentValues() contentValues.put(CommonDataKinds.Photo.PHOTO, photoBytes) val where = "${ContactsContract.Data.RAW_CONTACT_ID}=$rawContactId and " + "${ContactsContract.Data.MIMETYPE}='${CommonDataKinds.Photo.CONTENT_ITEM_TYPE}'" return try { ctx.contentResolver.update(ContactsContract.Data.CONTENT_URI, contentValues, where, null) } catch (e: Exception) { Log.e(TAG, "updateAndroidPhoto failed: ${e.message}") 0 } } private fun bitmapToPNGByteArray(bitmap: Bitmap): ByteArray? { val size = bitmap.width * bitmap.height * 4 val out = ByteArrayOutputStream(size) return try { bitmap.compress(Bitmap.CompressFormat.PNG, 100, out) out.flush() out.close() out.toByteArray() } catch (e: Exception) { Log.w(TAG, "Unable to serialize photo: ${e.message}") null } } ================================================ FILE: app/src/main/kotlin/com/tutpro/baresip/BaresipService.kt ================================================ package com.tutpro.baresip import android.Manifest.permission.RECORD_AUDIO import android.annotation.SuppressLint import android.app.Notification import android.app.NotificationChannel import android.app.NotificationManager import android.app.PendingIntent import android.app.Service import android.content.BroadcastReceiver import android.content.Context import android.content.Intent import android.content.IntentFilter import android.content.pm.PackageManager import android.content.pm.ServiceInfo import android.content.pm.ServiceInfo.FOREGROUND_SERVICE_TYPE_MICROPHONE import android.content.pm.ServiceInfo.FOREGROUND_SERVICE_TYPE_PHONE_CALL import android.content.res.Configuration import android.database.ContentObserver import android.graphics.ImageDecoder import android.media.AudioAttributes import android.media.AudioManager import android.media.AudioManager.MODE_IN_COMMUNICATION import android.media.AudioManager.MODE_NORMAL import android.media.MediaPlayer import android.media.Ringtone import android.media.RingtoneManager import android.media.audiofx.AcousticEchoCanceler import android.media.audiofx.AutomaticGainControl import android.media.audiofx.NoiseSuppressor import android.net.ConnectivityManager import android.net.LinkProperties import android.net.Network import android.net.NetworkCapabilities import android.net.NetworkRequest import android.net.Uri import android.net.wifi.WifiManager import android.os.Build.VERSION import android.os.CountDownTimer import android.os.Handler import android.os.IBinder import android.os.Looper import android.os.PowerManager import android.os.VibrationEffect import android.os.Vibrator import android.os.VibratorManager import android.provider.ContactsContract import android.provider.Settings import android.system.OsConstants import android.telecom.DisconnectCause import android.telecom.PhoneAccountHandle import android.telecom.TelecomManager import android.view.View import android.widget.RemoteViews import android.widget.Toast import androidx.annotation.Keep import androidx.appcompat.app.AppCompatDelegate import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.setValue import androidx.core.app.NotificationCompat import androidx.core.app.NotificationCompat.MessagingStyle import androidx.core.app.Person import androidx.core.app.RemoteInput import androidx.core.content.ContextCompat import androidx.core.graphics.drawable.IconCompat import androidx.core.net.toUri import androidx.lifecycle.MutableLiveData import com.tutpro.baresip.Utils.toCircle import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.delay import kotlinx.coroutines.launch import java.io.File import java.net.InetAddress import java.nio.charset.Charset import java.nio.charset.StandardCharsets import java.util.GregorianCalendar import java.util.Timer import java.util.TimerTask import kotlin.concurrent.schedule import kotlin.math.roundToInt class BaresipService: Service() { internal lateinit var intent: Intent private lateinit var am: AudioManager private lateinit var nt: Ringtone private lateinit var nm: NotificationManager private lateinit var snb: NotificationCompat.Builder private lateinit var cm: ConnectivityManager private lateinit var pm: PowerManager private lateinit var wm: WifiManager private lateinit var tm: TelecomManager private lateinit var vibrator: Vibrator private lateinit var partialWakeLock: PowerManager.WakeLock private lateinit var proximityWakeLock: PowerManager.WakeLock private lateinit var wifiLock: WifiManager.WifiLock private lateinit var hotSpotReceiver: BroadcastReceiver private lateinit var androidContactsObserver: ContentObserver private lateinit var networkCallback: ConnectivityManager.NetworkCallback private lateinit var stopState: String private lateinit var quitTimer: CountDownTimer private var vbTimer: Timer? = null private var origVolume = mutableMapOf() private var linkAddresses = mutableMapOf() private var activeNetwork: Network? = null private var allNetworks = mutableSetOf() private var hotSpotIsEnabled = false private var hotSpotAddresses = mapOf() private var mediaPlayer: MediaPlayer? = null private var androidContactsObserverRegistered = false private var hotSpotReceiverRegistered = false private var isNotificationInCall = false private var isServiceClean = false @SuppressLint("WakelockTimeout") override fun onCreate() { super.onCreate() Log.i(TAG, "BaresipService onCreate") instance = this intent = Intent("com.tutpro.baresip.EVENT") intent.setPackage("com.tutpro.baresip") filesPath = filesDir.absolutePath pName = packageName am = applicationContext.getSystemService(AUDIO_SERVICE) as AudioManager val ntUri = RingtoneManager.getActualDefaultRingtoneUri(applicationContext, RingtoneManager.TYPE_NOTIFICATION) nt = RingtoneManager.getRingtone(applicationContext, ntUri) val rtUri = if (Preferences(applicationContext).ringtoneUri == "") Settings.System.DEFAULT_RINGTONE_URI else Preferences(applicationContext).ringtoneUri!!.toUri() rt = RingtoneManager.getRingtone(applicationContext, rtUri) nm = getSystemService(NOTIFICATION_SERVICE) as NotificationManager createNotificationChannels() snb = NotificationCompat.Builder(this, LOW_CHANNEL_ID) pm = getSystemService(POWER_SERVICE) as PowerManager vibrator = if (VERSION.SDK_INT >= 31) { val vibratorManager = applicationContext.getSystemService(VIBRATOR_MANAGER_SERVICE) as VibratorManager vibratorManager.defaultVibrator } else { @Suppress("DEPRECATION") applicationContext.getSystemService(VIBRATOR_SERVICE) as Vibrator } // This is needed to keep service running also in Doze Mode partialWakeLock = pm.newWakeLock( PowerManager.PARTIAL_WAKE_LOCK, "com.tutpro.baresip:wakelock" ).apply { setReferenceCounted(false) } networkCallback = object : ConnectivityManager.NetworkCallback() { override fun onAvailable(network: Network) { super.onAvailable(network) Log.d(TAG, "Network $network is available") synchronized(allNetworks) { if (network !in allNetworks) allNetworks.add(network) } } override fun onLosing(network: Network, maxMsToLive: Int) { super.onLosing(network, maxMsToLive) Log.d(TAG, "Network $network is losing after $maxMsToLive ms") } override fun onLost(network: Network) { super.onLost(network) Log.d(TAG, "Network $network is lost") synchronized(allNetworks) { if (network in allNetworks) allNetworks.remove(network) } if (isServiceRunning) updateNetwork() } override fun onCapabilitiesChanged(network: Network, caps: NetworkCapabilities) { super.onCapabilitiesChanged(network, caps) synchronized(allNetworks) { if (network !in allNetworks) allNetworks.add(network) } if (isServiceRunning) updateNetwork() } override fun onLinkPropertiesChanged(network: Network, props: LinkProperties) { super.onLinkPropertiesChanged(network, props) Log.d(TAG, "Network $network link properties changed: $props") synchronized(allNetworks) { if (network !in allNetworks) allNetworks.add(network) } if (isServiceRunning) updateNetwork() } } cm = getSystemService(CONNECTIVITY_SERVICE) as ConnectivityManager cm.registerNetworkCallback( NetworkRequest.Builder() .removeCapability(NetworkCapabilities.NET_CAPABILITY_NOT_VPN) .build(), networkCallback ) wm = applicationContext.getSystemService(WIFI_SERVICE) as WifiManager hotSpotIsEnabled = Utils.isHotSpotOn(wm) hotSpotReceiver = object : BroadcastReceiver() { override fun onReceive(contxt: Context, intent: Intent) { val action = intent.action if ("android.net.wifi.WIFI_AP_STATE_CHANGED" == action) { val state = intent.getIntExtra(WifiManager.EXTRA_WIFI_STATE, 0) if (WifiManager.WIFI_STATE_ENABLED == state % 10) { if (hotSpotIsEnabled) { Log.d(TAG, "HotSpot is still enabled") } else { Log.d(TAG, "HotSpot is enabled") hotSpotIsEnabled = true Timer().schedule(1000) { hotSpotAddresses = Utils.hotSpotAddresses() Log.d(TAG, "HotSpot addresses $hotSpotAddresses") if (hotSpotAddresses.isNotEmpty()) { var reset = false for ((k, v) in hotSpotAddresses) if (afMatch(k)) if (Api.net_add_address_ifname(k, v) != 0) Log.e(TAG, "Failed to add $v address $k") else reset = true if (reset) Timer().schedule(2000) { updateNetwork() } } else { Log.w(TAG, "Could not get hotspot addresses") } } } } else { if (!hotSpotIsEnabled) { Log.d(TAG, "HotSpot is still disabled") } else { Log.d(TAG, "HotSpot is disabled") hotSpotIsEnabled = false if (hotSpotAddresses.isNotEmpty()) { for ((k, _) in hotSpotAddresses) if (Api.net_rm_address(k) != 0) Log.e(TAG, "Failed to remove address $k") hotSpotAddresses = mapOf() updateNetwork() } } } } } } ContextCompat.registerReceiver( applicationContext, hotSpotReceiver, IntentFilter("android.net.wifi.WIFI_AP_STATE_CHANGED"), ContextCompat.RECEIVER_NOT_EXPORTED ) hotSpotReceiverRegistered = true tm = getSystemService(TELECOM_SERVICE) as TelecomManager registerPhoneAccount() proximityWakeLock = pm.newWakeLock(PowerManager.PROXIMITY_SCREEN_OFF_WAKE_LOCK, "com.tutpro.baresip:proximity_wakelock") wifiLock = if (VERSION.SDK_INT < 29) @Suppress("DEPRECATION") wm.createWifiLock(WifiManager.WIFI_MODE_FULL_HIGH_PERF, "Baresip") else wm.createWifiLock(WifiManager.WIFI_MODE_FULL_LOW_LATENCY, "Baresip") wifiLock.setReferenceCounted(false) androidContactsObserver = object : ContentObserver(Handler(Looper.getMainLooper())) { override fun onChange(self: Boolean) { Log.d(TAG, "Android contacts change") if (contactsMode != "baresip") { Contact.loadAndroidContacts(this@BaresipService.applicationContext) Contact.contactsUpdate() } } } stopState = "initial" quitTimer = object : CountDownTimer(5000, 1000) { override fun onTick(millisUntilFinished: Long) { Log.d(TAG, "Seconds remaining: ${millisUntilFinished / 1000}") } override fun onFinish() { when (stopState) { "initial" -> { if (isServiceRunning) baresipStop(true) stopState = "force" quitTimer.start() } "force" -> { cleanService() isServiceRunning = false postServiceEvent(ServiceEvent("stopped", arrayListOf(""), System.nanoTime())) stopSelf() // exitProcess(0) } } } } } override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int { val action: String if (intent == null) { action = "Start" Log.d(TAG, "Received onStartCommand with null intent") } else { // Utils.dumpIntent(intent) action = intent.action!! Log.d(TAG, "Received onStartCommand action $action") } when (action) { "Start" -> { isStartReceived = true if (VERSION.SDK_INT < 31) { @Suppress("DEPRECATION") allNetworks = cm.allNetworks.toMutableSet() } updateDnsServers() updatePartialWakeLock() val assets = arrayOf("accounts", "config", "contacts") var file = File(filesPath) if (!file.exists()) { Log.i(TAG, "Creating baresip directory") try { File(filesPath).mkdirs() } catch (e: Error) { Log.e(TAG, "Failed to create directory: ${e.message}") } } for (a in assets) { file = File("${filesPath}/$a") if (!file.exists() && a != "config") { Log.i(TAG, "Copying asset '$a'") Utils.copyAssetToFile(applicationContext, a, "$filesPath/$a") } else { Log.i(TAG, "Asset '$a' already copied") } if (a == "config") Config.initialize(applicationContext) } if (contactsMode != "android") Contact.restoreBaresipContacts() if (contactsMode != "baresip") { Contact.loadAndroidContacts(applicationContext) registerAndroidContactsObserver() } Contact.contactsUpdate() val history = CallHistory.get() if (history.isEmpty()) { CallHistoryNew.restore() } else { for (old in history) { val new = CallHistoryNew(old.aor, old.peerUri, old.direction) new.stopTime = old.stopTime new.startTime = old.startTime new.recording = old.recording new.add() } } Blocked.restore() val recordings = File(filesDir, "recordings") val restored = File(filesPath, "restored") if (restored.exists()) { Log.d(TAG, "Clearing recordings") CallHistoryNew.clearRecordings() CallHistoryNew.save() if (recordings.exists()) recordings.deleteRecursively() restored.delete() } File(filesDir, "recordings").mkdir() File(filesDir, "tmp").mkdir() Message.restore() hotSpotAddresses = Utils.hotSpotAddresses() linkAddresses = linkAddresses() var addresses = "" for (la in linkAddresses) addresses = "$addresses;${la.key};${la.value}" Log.i(TAG, "Link addresses: $addresses") activeNetwork = cm.activeNetwork Log.i(TAG, "Active network: $activeNetwork") Log.i(TAG, "AEC/AGC/NS available = $aecAvailable/$agcAvailable/$nsAvailable") val userAgent = Config.variable("user_agent") Thread { baresipStart( filesPath, addresses.removePrefix(";"), logLevel, if (userAgent != "") userAgent else "baresip v${BuildConfig.VERSION_NAME} " + "(Android ${VERSION.RELEASE}/${System.getProperty("os.arch") ?: "?"})" ) }.start() isServiceRunning = true showStatusNotification() if (linkAddresses.isEmpty()) toast(getString(R.string.no_network), Toast.LENGTH_LONG) if (!aecAvailable) toast(getString(R.string.no_aec), Toast.LENGTH_LONG) } "Notification Dismissed" -> { updateStatusNotification() } "Start Content Observer" -> { registerAndroidContactsObserver() } "Stop Content Observer" -> { unRegisterAndroidContactsObserver() } "Call Answer" -> { val uap = intent!!.getLongExtra("uap", 0L) val callp = intent.getLongExtra("callp", 0L) stopRinging() stopMediaPlayer() am.mode = MODE_IN_COMMUNICATION setCallVolume() proximitySensing(proximitySensing) Api.ua_answer(uap, callp, Api.VIDMODE_OFF) } "Call Reject" -> { val callp = intent!!.getLongExtra("callp", 0L) val call = Call.ofCallp(callp) if (call == null) { Log.w(TAG, "onStartCommand did not find call $callp") } else { val peerUri = call.peerUri val aor = call.ua.account.aor Log.d(TAG, "Aor $aor rejected incoming call $callp from $peerUri") call.rejected = true Api.ua_hangup(call.ua.uap, callp, 486, "Rejected") } } "Call Hangup" -> { val callp = intent!!.getLongExtra("callp", 0L) Log.d(TAG, "onStartCommand Hangup action for $callp") val connection = ConnectionService.connections[callp] if (connection != null) { connection.onDisconnect() // This calls Api.ua_hangup(..., 0, "") } else { val call = Call.ofCallp(callp) if (call != null) Api.ua_hangup(call.ua.uap, callp, 0, "") } } "Transfer Deny" -> { val callp = intent!!.getLongExtra("callp", 0L) val call = Call.ofCallp(callp) if (call == null) Log.w(TAG, "onStartCommand did not find call $callp") else call.notifySipfrag(603, "Decline") nm.cancel(TRANSFER_NOTIFICATION_ID) } "Message Save" -> { val uap = intent!!.getLongExtra("uap", 0L) val ua = UserAgent.ofUap(uap) if (ua == null) Log.w(TAG, "onStartCommand did not find UA $uap") else { Message.updateAorMessage( ua.account.aor, intent.getStringExtra("time")!!.toLong() ) ua.account.unreadMessages = Message.unreadMessages(ua.account.aor) } nm.cancel(MESSAGE_NOTIFICATION_ID) } "Message Delete" -> { val uap = intent!!.getLongExtra("uap", 0L) val ua = UserAgent.ofUap(uap) if (ua == null) Log.w(TAG, "onStartCommand did not find UA $uap") else { Message.deleteAorMessage( ua.account.aor, intent.getStringExtra("time")!!.toLong() ) ua.account.unreadMessages = Message.unreadMessages(ua.account.aor) } nm.cancel(MESSAGE_NOTIFICATION_ID) } "Message Inline Reply" -> { val remoteInputResults = RemoteInput.getResultsFromIntent(intent!!) if (remoteInputResults != null) { val replyText = remoteInputResults.getCharSequence(KEY_TEXT_REPLY)?.toString() if (!replyText.isNullOrEmpty()) { val uap = intent.getLongExtra("uap", -1L) val ua = UserAgent.ofUap(uap)!! val aor = ua.account.aor var peerUri = intent.getStringExtra("peer")!! val timeStamp = intent.getLongExtra("time", 0L) if (Utils.isTelUri(peerUri)) { if (ua.account.telProvider == "") { Log.w(TAG, "No telephony provider for $aor") peerUri = "" } else peerUri = Utils.telToSip(peerUri, ua.account) } if (peerUri != "") { Log.d(TAG, "Direct Reply from $aor to $peerUri: $replyText") Message.updateAorMessage(aor, timeStamp) val time = System.currentTimeMillis() val msg = Message(aor, peerUri, replyText, time, MESSAGE_UP_WAIT, 0, "", false) msg.add() if (Api.message_send(uap, peerUri, replyText, time.toString()) != 0) { Log.w(TAG, "message_send failed") msg.direction = MESSAGE_UP_FAIL msg.responseReason = getString(R.string.message_failed) } else { ua.account.unreadMessages = Message.unreadMessages(aor) } } } } nm.cancel(MESSAGE_NOTIFICATION_ID) } "Update Notification" -> { updateStatusNotification() } "Stop" -> { cleanService() if (isServiceRunning) { baresipStop(false) quitTimer.start() } } else -> { Log.e(TAG, "Unknown start action $action") } } return START_STICKY } override fun onConfigurationChanged(newConfig: Configuration) { super.onConfigurationChanged(newConfig) val currentNightMode = newConfig.uiMode and Configuration.UI_MODE_NIGHT_MASK when (currentNightMode) { Configuration.UI_MODE_NIGHT_NO -> { darkTheme.value = Preferences(applicationContext).displayTheme == AppCompatDelegate.MODE_NIGHT_YES } Configuration.UI_MODE_NIGHT_YES -> { darkTheme.value = true } } } override fun onBind(intent: Intent): IBinder? { return null } override fun onDestroy() { super.onDestroy() Log.d(TAG, "onDestroy at Baresip Service") cleanService() instance = null if (isServiceRunning) sendBroadcast(Intent("com.tutpro.baresip.Restart")) } @Suppress("unused") @SuppressLint("UnspecifiedImmutableFlag", "DiscouragedApi", "FullScreenIntentPolicy") @Keep fun uaEvent(event: String, uap: Long, callp: Long) { if (!isServiceRunning) return val ev = event.split(",") if (ev[0] == "create") { val ua = UserAgent(uap) ua.status = if (ua.account.regint == 0) R.drawable.circle_white else circleYellow.getValue(colorblind) ua.add() val acc = ua.account if (acc.authUser != "" && acc.authPass == NO_AUTH_PASS) { val password = aorPasswords[acc.aor] if (password != null) { Api.account_set_auth_pass(acc.accp, password) acc.authPass = password } else { Api.account_set_auth_pass(acc.accp, NO_AUTH_PASS) } } Log.d(TAG, "got uaEvent $event/${acc.aor}") return } if (ev[0] == "sndfile dump") { Log.d(TAG, "Got sndfile dump ${ev[1]}") if (Call.inCall()) { if (ev[1].endsWith("enc.wav")) Call.calls()[0].dumpfiles[0] = ev[1] else Call.calls()[0].dumpfiles[1] = ev[1] } return } if (ev[0] == "recorder sessionid") { recorderSessionId = ev[1].toInt() Log.d(TAG, "got recorder sessionid $recorderSessionId") if (recorderSessionId != 0) { if (aecAvailable) { aec = AcousticEchoCanceler.create(recorderSessionId) if (aec != null) { if (!aec!!.enabled) { aec!!.enabled = true if (aec!!.enabled) Log.d(TAG, "AEC is enabled") else Log.w(TAG, "Failed to enable AEC") } else Log.d(TAG, "AEC is already enabled") } else Log.w(TAG, "Failed to create AEC for session $recorderSessionId") } if (agcAvailable) { agc = AutomaticGainControl.create(recorderSessionId) if (agc != null) { if (!agc!!.enabled) { agc!!.enabled = true if (agc!!.enabled) Log.d(TAG, "AGC is enabled") } } else Log.w(TAG, "Failed to create AGC") } if (nsAvailable) { ns = NoiseSuppressor.create(recorderSessionId) if (ns != null) { if (!ns!!.enabled) { ns!!.enabled = true if (ns!!.enabled) Log.d(TAG, "NS is enabled") } } else Log.w(TAG, "Failed to create NS") } recorderSessionId = 0 } return } val ua = UserAgent.ofUap(uap) if (ua == null) { Log.w(TAG, "uaEvent $event did not find ua $uap") return } val aor = ua.account.aor Log.d(TAG, "got uaEvent $event/$aor/$callp") val call = Call.ofCallp(callp) if (call == null && callp != 0L && !setOf("incoming call", "call incoming", "call closed").contains(ev[0])) { Log.w(TAG, "uaEvent $event did not find call $callp") return } for (accountIndex in uas.value.indices) { if (uas.value[accountIndex].account.aor == aor) { when (ev[0]) { "registering", "unregistering" -> { ua.updateStatus(circleYellow.getValue(colorblind)) updateStatusNotification() if (isMainVisible) registrationUpdate.postValue(System.currentTimeMillis()) return } "registered" -> { ua.updateStatus( if (Api.account_regint(ua.account.accp) == 0) R.drawable.circle_white else circleGreen.getValue(colorblind) ) updateStatusNotification() if (isMainVisible) registrationUpdate.postValue(System.currentTimeMillis()) return } "registering failed" -> { ua.updateStatus(if (Api.account_regint(ua.account.accp) == 0) R.drawable.circle_white else circleRed.getValue(colorblind) ) updateStatusNotification() if (isMainVisible) registrationUpdate.postValue(System.currentTimeMillis()) if (Utils.isVisible()) { val reason = if (ev.size > 1) { if (ev[1] == "Invalid argument") // Likely due to DNS lookup failure ": DNS lookup failed" else ": ${ev[1]}" } else "" toast(String.format(getString(R.string.registering_failed), aor) + reason) } return } "call outgoing" -> { if (call!!.status.value == "transferring") break stopMediaPlayer() setCallVolume() proximitySensing(proximitySensing) } "call ringing" -> { ConnectionService.connections[callp]?.setRinging() ensureCommunicationMode() playRingBack() return } "call progress" -> { ensureCommunicationMode() if ((ev[1].toInt() and Api.SDP_RECVONLY) != 0) stopMediaPlayer() else { ConnectionService.connections[callp]?.setRinging() playRingBack() } return } "incoming call" -> { val peerUri = ev[1] val toastMsg = if (Call.isAnyCallActive(applicationContext)) String.format(getString(R.string.call_auto_rejected), Utils.friendlyUri(this, peerUri, ua.account)) else if (ua.account.blockUnknown && Contact.contactName(peerUri) == peerUri) String.format(getString(R.string.call_blocked), Utils.friendlyUri(this, peerUri, ua.account)) else if (!Utils.checkPermissions(this, arrayOf(RECORD_AUDIO))) getString(R.string.no_calls) else "" if (toastMsg != "") { Log.d(TAG, "Auto-rejecting incoming call to $uap from $peerUri") Api.sip_treply(callp, 486, "Busy Here") Api.bevent_stop(ev[2].toLong()) toast(toastMsg) if (toastMsg.contains(getString(R.string.call_blocked))) { if (ua.account.callHistory) Blocked( ua.account.aor, peerUri, "invite", GregorianCalendar().timeInMillis ).add() } else { val name = "callwaiting_$toneCountry" val resourceId = resources.getIdentifier( name, "raw", packageName ) if (resourceId != 0) { playUnInterrupted(resourceId, 1) } else { Log.e(TAG, "Callwaiting tone $name.wav not found") } if (ua.account.callHistory) { CallHistoryNew(aor, peerUri, "in").add() ua.account.missedCalls = true } } return } // callp holds SIP message pointer Api.ua_accept(uap, callp) return } "call incoming" -> { val peerUri = ev[1] Log.d(TAG, "Incoming call $uap/$callp/$peerUri") if (Call.ofCallp(callp) == null) Call(callp, ua, peerUri, "in", "incoming").add() val extras = android.os.Bundle() extras.putLong("uap", uap) extras.putLong("callp", callp) extras.putString("peerUri", peerUri) try { tm.addNewIncomingCall(getPhoneAccountHandle(this), extras) } catch (e: Exception) { Log.e(TAG, "Telecom addNewIncomingCall failed: ${e.message}") } return } "call answered" -> { stopMediaPlayer() ensureCommunicationMode() if (call!!.status.value == "incoming") call.status.value = "answered" else return } "call redirect" -> { stopMediaPlayer() } "call established" -> { ConnectionService.connections[callp]?.setActive() ensureCommunicationMode() nm.cancel(CALL_NOTIFICATION_ID) Log.d(TAG, "AoR $aor call $callp established") call!!.status.value = "connected" call.onhold = false call.startTime = GregorianCalendar() updateStatusNotification() if (!isMainVisible) return } "call update" -> { if (call!!.conferenceCall) { Log.d(TAG, "Refusing to update conference call ${call.callp}") Api.bevent_stop(ev[2].toLong()) return } val newHeldState = when (ev[1].toInt()) {Api.SDP_INACTIVE, Api.SDP_RECVONLY -> true else -> false } val connection = ConnectionService.connections[callp] if (call.held && !newHeldState) { Log.d(TAG, "Call ${call.callp} un-held by peer.") call.onhold = false // Use a Coroutine with a small delay to let the SIP // transaction (the re-INVITE from the peer) finish // before trying to hold the other call and resume this one. CoroutineScope(Dispatchers.Main).launch { delay(100) call.resume() } } call.held = newHeldState if (newHeldState) { // Peer put us on hold call.showOnHoldNotice.value = true call.callOnHold.value = true connection?.setOnHold() } else { // Peer un-held us call.showOnHoldNotice.value = false // Only clear the UI if we aren't also manually holding it if (!call.onhold) { call.callOnHold.value = false connection?.setActive() } } if (call.state() == Api.CALL_STATE_EARLY) { if ((ev[1].toInt() and Api.SDP_RECVONLY) != 0) stopMediaPlayer() else { ConnectionService.connections[callp]?.setRinging() playRingBack() } } if (call.status.value == "connected" && !call.held && !call.onhold) { if (call.callOnHold.value || call.showOnHoldNotice.value) { Log.d(TAG, "Safety guard: Clearing stuck hold flags for ${call.callp}") call.callOnHold.value = false call.showOnHoldNotice.value = false connection?.setActive() } } if (!isMainVisible || call.status.value != "connected") return } "call verified", "call secure" -> { if (ev[0] == "call secure") { call!!.security = R.color.colorTrafficYellow } else { call!!.security = R.color.colorTrafficGreen call.zid = ev[1] } if (!isMainVisible) return } "call transfer" -> { if (!Utils.isVisible()) { val piFlags = PendingIntent.FLAG_IMMUTABLE or PendingIntent.FLAG_UPDATE_CURRENT val intent = Intent(applicationContext, MainActivity::class.java) intent.flags = Intent.FLAG_ACTIVITY_SINGLE_TOP or Intent.FLAG_ACTIVITY_CLEAR_TOP or Intent.FLAG_ACTIVITY_NEW_TASK intent.putExtra("action", "transfer show") .putExtra("callp", callp).putExtra("uri", ev[1]) val pi = PendingIntent.getActivity(applicationContext, TRANSFER_REQ_CODE, intent, piFlags) val nb = NotificationCompat.Builder(this, HIGH_CHANNEL_ID) val target = Utils.friendlyUri(this, ev[1], ua.account) nb.setSmallIcon(R.drawable.ic_notification_call) .setColor(ContextCompat.getColor(this, R.color.colorPrimary)) .setContentIntent(pi) .setDefaults(Notification.DEFAULT_SOUND) .setAutoCancel(true) .setContentTitle(getString(R.string.transfer_request_to)) .setContentText(target) val acceptIntent = Intent(applicationContext, MainActivity::class.java) acceptIntent.flags = Intent.FLAG_ACTIVITY_SINGLE_TOP or Intent.FLAG_ACTIVITY_CLEAR_TOP or Intent.FLAG_ACTIVITY_NEW_TASK acceptIntent.putExtra("action", "transfer accept") .putExtra("callp", callp).putExtra("uri", ev[1]) val acceptPendingIntent = PendingIntent.getActivity(applicationContext, ACCEPT_REQ_CODE, acceptIntent, piFlags) val denyIntent = Intent(this, BaresipService::class.java) denyIntent.action = "Transfer Deny" denyIntent.putExtra("callp", callp) val denyPendingIntent = PendingIntent.getService(this, DENY_REQ_CODE, denyIntent, piFlags) nb.addAction(R.drawable.ic_notification_call, getString(R.string.accept), acceptPendingIntent) nb.addAction(R.drawable.ic_notification_call_end, getString(R.string.deny), denyPendingIntent) nm.notify(TRANSFER_NOTIFICATION_ID, nb.build()) return } } "transfer failed" -> { Log.d(TAG, "AoR $aor call $callp transfer failed: ${ev[1]}") stopMediaPlayer() call!!.referTo = "" if (Utils.isVisible()) toast("${getString(R.string.transfer_failed)}: ${ev[1].trim()}") if (!isMainVisible) return } "call closed" -> { Log.d(TAG, "AoR $aor call $callp is closed prm: ${ev[1]}") ConnectionService.lastDisconnectTime = System.currentTimeMillis() val connection = ConnectionService.connections[callp] if (connection != null) { val cause = when { ev[1].contains("200") -> DisconnectCause.LOCAL ev[1].contains("486") -> DisconnectCause.BUSY ev[1].contains("404") -> DisconnectCause.ERROR ev[1].contains("403") || ev[1].contains("401") -> DisconnectCause.RESTRICTED else -> DisconnectCause.REMOTE } connection.setDisconnected(DisconnectCause(cause)) Handler(Looper.getMainLooper()).postDelayed({ connection.destroy() ConnectionService.connections.remove(callp) }, 500) } nm.cancel(CALL_NOTIFICATION_ID) if (call != null) { stopRinging() stopMediaPlayer() aec?.release() aec = null agc?.release() agc = null ns?.release() ns = null val newCall = call.newCall if (newCall != null) { newCall.onHoldCall = null call.newCall = null } val onHoldCall = call.onHoldCall if (onHoldCall != null) { onHoldCall.newCall = null onHoldCall.referTo = "" call.onHoldCall = null } val isConference = call.conferenceCall call.remove() updateStatusNotification() if (isConference && ua.calls().isEmpty()) Api.module_unload("mixminus") val reason = ev[1] val tone = ev[2] if (tone == "busy") playBusy() else ensureCommunicationMode() if (call.dir == "out") call.rejected = call.startTime == null && !reason.startsWith("408") && !reason.startsWith("480") && !reason.startsWith("Connection reset by") val missed = call.dir == "in" && call.startTime == null && !call.rejected val completedElsewhere = missed && ev[2].startsWith("SIP") && ev[2].contains(";cause=200") if (ua.account.callHistory) { CoroutineScope(Dispatchers.IO).launch { val history = CallHistoryNew(aor, call.peerUri, call.dir) history.stopTime = GregorianCalendar() history.startTime = if (completedElsewhere) history.stopTime else call.startTime history.rejected = call.rejected if (call.dumpfiles[0] != "") { history.recording = call.dumpfiles } history.add() if (call.startTime != null && call.dumpfiles[0] != "") { delay(500) val rxFile = File(call.dumpfiles[0]) val txFile = File(call.dumpfiles[1]) val mergedFileName = rxFile.name .replace("dump", "rec") .replace("=>", "-") .replace("sip:", "") .replace("-enc", "") .replace("*", "#") .replace(";user=phone", "") val mergedFile = File(filesPath, mergedFileName) if (Utils.mergeWavFiles(rxFile, txFile, mergedFile)) { Log.d(TAG, "Automatic merge succeeded.") history.recording = arrayOf(mergedFile.absolutePath, "") CallHistoryNew.save() try { rxFile.delete() txFile.delete() } catch (e: Exception) { Log.w(TAG, "Could not delete temporary raw files after merge: ${e.message}") } } else { Log.e(TAG, "Automatic merge failed. Storing raw file paths as fallback.") history.recording = call.dumpfiles } } } ua.account.missedCalls = ua.account.missedCalls || missed } if (missed) { val piFlags = PendingIntent.FLAG_IMMUTABLE or PendingIntent.FLAG_UPDATE_CURRENT val caller = Utils.friendlyUri(this, call.peerUri, ua.account) val intent = Intent(applicationContext, MainActivity::class.java) intent.flags = Intent.FLAG_ACTIVITY_SINGLE_TOP or Intent.FLAG_ACTIVITY_CLEAR_TOP or Intent.FLAG_ACTIVITY_NEW_TASK intent.putExtra("action", "call missed") .putExtra("uap", uap) val pi = PendingIntent.getActivity(applicationContext, CALL_REQ_CODE, intent, piFlags) val nb = NotificationCompat.Builder(this, HIGH_CHANNEL_ID) nb.setSmallIcon(R.drawable.ic_notification_call_missed) .setColor(ContextCompat.getColor(this, R.color.colorPrimary)) .setContentIntent(pi) .setCategory(Notification.CATEGORY_CALL) .setAutoCancel(true) var missedCalls = 0 for (notification in nm.activeNotifications) if (notification.id == CALL_MISSED_NOTIFICATION_ID) missedCalls++ if (missedCalls == 0) { nb.setContentTitle(getString(R.string.missed_call_from)) nb.setContentText(caller) } else { nb.setContentTitle(getString(R.string.missed_calls)) nb.setContentText( String.format(getString(R.string.missed_calls_count), missedCalls + 1)) } nm.notify(CALL_MISSED_NOTIFICATION_ID, nb.build()) } if (!Utils.isVisible()) return } val reason = ev[1].trim() if ((reason != "") && (ua.calls().isEmpty())) { if (reason[0].isDigit()) { if (reason[0] != '3') toast("${getString(R.string.call_failed)}: $reason") } else toast("${getString(R.string.call_closed)}: ${Api.call_peer_uri(callp)}: $reason") } } } } } postServiceEvent(ServiceEvent(event, arrayListOf(uap, callp), System.nanoTime())) } @Suppress("unused") @SuppressLint("UnspecifiedImmutableFlag") @Keep fun messageEvent(uap: Long, peerUri: String, cType: String, msg: ByteArray) { val ua = UserAgent.ofUap(uap) if (ua == null) { Log.w(TAG, "messageEvent did not find ua $uap") return } if (ua.account.blockUnknown && Contact.contactName(peerUri) == peerUri) { Log.d(TAG, "Auto-rejecting incoming message by $uap from $peerUri") Blocked( ua.account.aor, peerUri, "message", GregorianCalendar().timeInMillis ).add() toast(String.format( getString(R.string.message_blocked), Utils.friendlyUri(this, peerUri, ua.account) ) ) // Api.sip_treply(callp, 486, "Busy Here") // Api.bevent_stop(bevent) return } val charsetString = Utils.paramValue(cType.replace(" ", ""), "charset") val charset = try { Charset.forName(charsetString) } catch (_: Exception) { StandardCharsets.UTF_8 } val text = try { String(msg, charset) } catch (e: Exception) { val error = "Decoding of message failed using charset $charset from $cType: ${e.message}!" Log.w(TAG, error) error } val timeStamp = System.currentTimeMillis() val timeStampString = timeStamp.toString() Log.d(TAG, "Message event for $uap from $peerUri at $timeStampString") Message(ua.account.aor, peerUri, text, timeStamp, MESSAGE_DOWN, 0, "", true).add() ua.account.unreadMessages = true if (!Utils.isVisible()) { // common flags val piFlags = PendingIntent.FLAG_IMMUTABLE or PendingIntent.FLAG_UPDATE_CURRENT // message show val intent = Intent(applicationContext, MainActivity::class.java) intent.flags = Intent.FLAG_ACTIVITY_SINGLE_TOP or Intent.FLAG_ACTIVITY_CLEAR_TOP or Intent.FLAG_ACTIVITY_NEW_TASK intent.putExtra("action", "message show").putExtra("uap", uap).putExtra("peer", peerUri) val pi = PendingIntent.getActivity(applicationContext, MESSAGE_REQ_CODE, intent, piFlags) // message notification builder val sender = Utils.friendlyUri(this, peerUri, ua.account) val senderContact = Contact.findContact(peerUri) val personBuilder = Person.Builder().setName(sender) val contactColor = senderContact?.color() ?: "#B0B0B0" val initial = if (sender.isNotEmpty()) sender.take(1) else "?" val textAvatarBitmap = Utils.createTextAvatar(initial,contactColor) var icon = IconCompat.createWithBitmap(textAvatarBitmap) if (senderContact is Contact.BaresipContact) { if (senderContact.avatarImage != null) icon = IconCompat.createWithBitmap(senderContact.avatarImage!!.toCircle()) } else if (senderContact is Contact.AndroidContact) { if (senderContact.thumbnailUri != null) { try { val source = ImageDecoder.createSource(contentResolver, senderContact.thumbnailUri!!) val bitmap = ImageDecoder.decodeBitmap(source) { decoder, _, _ -> decoder.allocator = ImageDecoder.ALLOCATOR_SOFTWARE // decoder.setTargetSize(256, 256) } icon = IconCompat.createWithBitmap(bitmap.toCircle()) } catch (e: Exception) { Log.e(TAG, "Failed to load Android contact avatar: $e") } } } val senderPerson = personBuilder.setIcon(icon).build() val localUserPerson = Person.Builder() .setName(getString(R.string.you)) .setKey(ua.account.aor) .build() val messagingStyle = MessagingStyle(localUserPerson) .setConversationTitle(null) .setGroupConversation(false) .addMessage(text, timeStamp, senderPerson) val nb = NotificationCompat.Builder(this, HIGH_CHANNEL_ID) .setSmallIcon(R.drawable.ic_notification_message) .setColor(ContextCompat.getColor(this, R.color.colorPrimary)) .setContentIntent(pi) .setSound(Settings.System.DEFAULT_NOTIFICATION_URI) .setAutoCancel(true) .setStyle(messagingStyle) .setCategory(NotificationCompat.CATEGORY_MESSAGE) .setPriority(NotificationCompat.PRIORITY_HIGH) // message inline reply val remoteInput = RemoteInput.Builder(KEY_TEXT_REPLY) .setLabel(getString(R.string.reply)) .build() val directReplyIntent = Intent(this, BaresipService::class.java) directReplyIntent.action = "Message Inline Reply" directReplyIntent.putExtra("uap", uap).putExtra("peer", peerUri).putExtra("time", timeStamp) val directReplyPendingIntent = PendingIntent.getService( this, DIRECT_REPLY_REQ_CODE, directReplyIntent, PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_MUTABLE ) val inlineReplyAction = NotificationCompat.Action.Builder( R.drawable.ic_notification_reply, getString(R.string.reply), directReplyPendingIntent ).addRemoteInput(remoteInput) .setAllowGeneratedReplies(true) .setSemanticAction(NotificationCompat.Action.SEMANTIC_ACTION_REPLY). build() // message save val saveIntent = Intent(this, BaresipService::class.java) saveIntent.action = "Message Save" saveIntent.putExtra("uap", uap).putExtra("time", timeStampString) val savePendingIntent = PendingIntent.getService(this, SAVE_REQ_CODE, saveIntent, piFlags) val saveAction = NotificationCompat.Action.Builder( R.drawable.ic_notification_save, getString(R.string.save), savePendingIntent ).build() // message delete val deleteIntent = Intent(this, BaresipService::class.java) deleteIntent.action = "Message Delete" deleteIntent.putExtra("uap", uap).putExtra("time", timeStampString) val deletePendingIntent = PendingIntent.getService(this, DELETE_REQ_CODE, deleteIntent, piFlags) val deleteAction = NotificationCompat.Action.Builder( R.drawable.ic_notification_delete, getString(R.string.delete), deletePendingIntent ).build() nb.addAction(inlineReplyAction).addAction(saveAction).addAction(deleteAction) nm.notify(MESSAGE_NOTIFICATION_ID, nb.build()) return } if (nm.currentInterruptionFilter <= NotificationManager.INTERRUPTION_FILTER_ALL) nt.play() postServiceEvent(ServiceEvent("message show", arrayListOf(uap, peerUri), System.nanoTime())) } @Keep @Suppress("UNUSED") fun messageResponse(responseCode: Int, responseReason: String, time: String) { Log.d(TAG, "Message response '$responseCode $responseReason' at $time") val timeStamp = time.toLong() for (m in messages.reversed()) if (m.timeStamp == timeStamp) { if (responseCode < 300) { m.direction = MESSAGE_UP } else { m.direction = MESSAGE_UP_FAIL m.responseCode = responseCode m.responseReason = responseReason } messageUpdate.postValue(System.currentTimeMillis()) break } else { if (m.timeStamp < timeStamp - 60000) break } } private var audioModeChangedListener: AudioManager.OnModeChangedListener? = null fun runCall(uap: Long, uri: String, conferenceCall: Boolean, onHoldCallp: Long) { val executeCall = { val handler = Handler(Looper.getMainLooper()) handler.postDelayed({ val ua = UserAgent.ofUap(uap) if (ua != null) { if (conferenceCall && ua.calls().isEmpty()) Api.module_load("mixminus") val callp = ua.callAlloc(0L, Api.VIDMODE_OFF) if (callp != 0L) { ConnectionService.promoteOutgoingConnection(callp) val onHoldCall = Call.ofCallp(onHoldCallp) val call = Call(callp, ua, uri, "out", "outgoing") call.onHoldCall = onHoldCall call.conferenceCall = conferenceCall call.add() updateStatusNotification() if (onHoldCall != null) onHoldCall.newCall = call if (!call.connect(uri)) { Log.w(TAG, "call_connect $callp failed") ConnectionService.onCallClosed(callp) call.remove() call.destroy() updateStatusNotification() } } else { ConnectionService.pendingOutgoingConnection?.let { it.setDisconnected(DisconnectCause(DisconnectCause.ERROR)) it.destroy() ConnectionService.pendingOutgoingConnection = null } } } }, audioDelay) } if (VERSION.SDK_INT < 31) { Log.d(TAG, "Setting audio mode to MODE_IN_COMMUNICATION") am.mode = MODE_IN_COMMUNICATION executeCall() } else { if (am.mode == MODE_IN_COMMUNICATION) { Log.d(TAG, "Audio mode already in MODE_IN_COMMUNICATION") executeCall() } else { audioModeChangedListener = AudioManager.OnModeChangedListener { mode -> if (mode == MODE_IN_COMMUNICATION) { Log.d(TAG, "Audio mode changed to MODE_IN_COMMUNICATION") audioModeChangedListener?.let { am.removeOnModeChangedListener(it) audioModeChangedListener = null } executeCall() } } am.addOnModeChangedListener(mainExecutor, audioModeChangedListener!!) Log.d(TAG, "Setting audio mode to MODE_IN_COMMUNICATION (waiting for callback)") am.mode = MODE_IN_COMMUNICATION } } } @Suppress("unused") @Keep fun started() { Log.d(TAG, "Received 'started' from baresip") isNativeReady = true Api.net_debug() postServiceEvent(ServiceEvent("started", arrayListOf(callActionUri), System.nanoTime())) callActionUri = "" Log.d(TAG, "Battery optimizations are ignored: " + "${pm.isIgnoringBatteryOptimizations(packageName)}") Log.d(TAG, "Partial wake lock/wifi lock is held: " + "${partialWakeLock.isHeld}/${wifiLock.isHeld}") updateStatusNotification() } @Suppress("unused") @Keep fun stopped(error: String) { Log.d(TAG, "Received 'stopped' from baresip with start error '$error'") isNativeReady = false quitTimer.cancel() cleanService() isServiceRunning = false postServiceEvent(ServiceEvent("stopped", arrayListOf(error), System.nanoTime())) stopSelf() } private fun createNotificationChannels() { val lowChannel = NotificationChannel(LOW_CHANNEL_ID, "No sound, no vibrate", NotificationManager.IMPORTANCE_LOW) lowChannel.description = "Background status notifications" lowChannel.enableVibration(false) lowChannel.lockscreenVisibility = Notification.VISIBILITY_PUBLIC nm.createNotificationChannel(lowChannel) val ringAttributes = AudioAttributes.Builder() .setContentType(AudioAttributes.CONTENT_TYPE_SONIFICATION) .setUsage(AudioAttributes.USAGE_NOTIFICATION_RINGTONE) .build() val highChannel = NotificationChannel(HIGH_CHANNEL_ID, "Sound, vibrate, and peek", NotificationManager.IMPORTANCE_HIGH) highChannel.description = "Incoming calls and important alerts" highChannel.lockscreenVisibility = Notification.VISIBILITY_PUBLIC highChannel.enableVibration(true) highChannel.setSound(Settings.System.DEFAULT_NOTIFICATION_URI, ringAttributes) nm.createNotificationChannel(highChannel) val mediumChannel = NotificationChannel(MEDIUM_CHANNEL_ID, "Sound only", NotificationManager.IMPORTANCE_DEFAULT) mediumChannel.description = "Incoming messages" mediumChannel.lockscreenVisibility = Notification.VISIBILITY_PUBLIC mediumChannel.enableVibration(false) mediumChannel.setSound(Settings.System.DEFAULT_NOTIFICATION_URI, ringAttributes) nm.createNotificationChannel(mediumChannel) } private fun buildStatusNotification(): Notification { if (VERSION.SDK_INT >= 31) snb.foregroundServiceBehavior = Notification.FOREGROUND_SERVICE_IMMEDIATE snb.setOngoing(true) val notification = snb.build() notification.flags = notification.flags or Notification.FLAG_NO_CLEAR or Notification.FLAG_ONGOING_EVENT return notification } @SuppressLint("UnspecifiedImmutableFlag") private fun showStatusNotification() { val intent = Intent(applicationContext, MainActivity::class.java) .setAction(Intent.ACTION_MAIN) .addCategory(Intent.CATEGORY_LAUNCHER) val pi = PendingIntent.getActivity(applicationContext, STATUS_REQ_CODE, intent, PendingIntent.FLAG_IMMUTABLE) val deleteIntent = Intent(this, BaresipService::class.java) .setAction("Notification Dismissed") val dpi = PendingIntent.getService( this, STATUS_REQ_CODE, deleteIntent, PendingIntent.FLAG_IMMUTABLE ) val notificationLayout = RemoteViews(packageName, R.layout.status_notification) snb.setVisibility(NotificationCompat.VISIBILITY_PUBLIC) .setSmallIcon(R.drawable.ic_notification_b) .setContentIntent(pi) .setDeleteIntent(dpi) .setOngoing(true) .setCategory(Notification.CATEGORY_SERVICE) .setStyle(NotificationCompat.DecoratedCustomViewStyle()) .setCustomContentView(notificationLayout) val notification = buildStatusNotification() try { if (VERSION.SDK_INT >= 34) startForeground(STATUS_NOTIFICATION_ID, notification, ServiceInfo.FOREGROUND_SERVICE_TYPE_SPECIAL_USE) else startForeground(STATUS_NOTIFICATION_ID, notification) } catch (e: Exception) { Log.e(TAG, "Failed to start foreground service: ${e.message}") } } fun updateStatusNotification() { val activeCall = Call.calls().find { it.status.value == "connected" || it.status.value == "outgoing" || it.status.value == "answered" } val builder = NotificationCompat.Builder(this, LOW_CHANNEL_ID) val intent = Intent(applicationContext, MainActivity::class.java) .setAction(Intent.ACTION_MAIN) .addCategory(Intent.CATEGORY_LAUNCHER) val pi = PendingIntent.getActivity(applicationContext, STATUS_REQ_CODE, intent, PendingIntent.FLAG_IMMUTABLE) val deleteIntent = Intent(this, BaresipService::class.java).setAction("Notification Dismissed") val dpi = PendingIntent.getService(this, STATUS_REQ_CODE, deleteIntent, PendingIntent.FLAG_IMMUTABLE) builder.setVisibility(NotificationCompat.VISIBILITY_PUBLIC) .setSmallIcon(R.drawable.ic_notification_b) .setContentIntent(pi) .setDeleteIntent(dpi) .setOngoing(true) if (activeCall != null) { val peerUri = activeCall.peerUri val caller = Utils.friendlyUri(this, peerUri, activeCall.ua.account) val person = Person.Builder().setName(caller).build() val hangupIntent = Intent(this, BaresipService::class.java) hangupIntent.action = "Call Hangup" hangupIntent.putExtra("callp", activeCall.callp) val hpi = PendingIntent.getService(this, REJECT_REQ_CODE, hangupIntent, PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE) builder.setStyle(NotificationCompat.CallStyle.forOngoingCall(person, hpi)) .setCategory(Notification.CATEGORY_CALL) .setPriority(NotificationCompat.PRIORITY_LOW) .setWhen(activeCall.startTime?.timeInMillis ?: System.currentTimeMillis()) .setUsesChronometer(true) .setContentText( if (activeCall.onhold) getString(R.string.call_is_on_hold) else when (activeCall.status.value) { "outgoing", "incoming" -> getString(R.string.call_is_ringing) "connected", "answered" -> getString(R.string.call_is_connected) else -> getString(R.string.call) } ) } else { builder.setStyle(null) builder.setStyle(NotificationCompat.DecoratedCustomViewStyle()) .setCategory(Notification.CATEGORY_SERVICE) .setPriority(NotificationCompat.PRIORITY_MIN) .setWhen(0) .setShowWhen(false) .setUsesChronometer(false) .setContentTitle("") .setContentText("") val notificationLayout = RemoteViews(packageName, R.layout.status_notification) for (i in 0..3) { val resId = when (i) { 0 -> R.id.status0 1 -> R.id.status1 2 -> R.id.status2 else -> R.id.status3 } if (i < uas.value.size) { notificationLayout.setImageViewResource(resId, uas.value[i].status) notificationLayout.setViewVisibility(resId, View.VISIBLE) } else { notificationLayout.setViewVisibility(resId, View.INVISIBLE) } } if (uas.value.size > 4) notificationLayout.setViewVisibility(R.id.etc, View.VISIBLE) else notificationLayout.setViewVisibility(R.id.etc, View.INVISIBLE) builder.setCustomContentView(notificationLayout) } if (VERSION.SDK_INT >= 31) builder.foregroundServiceBehavior = Notification.FOREGROUND_SERVICE_IMMEDIATE builder.setOngoing(true) val notification = builder.build() notification.flags = notification.flags or Notification.FLAG_NO_CLEAR or Notification.FLAG_ONGOING_EVENT try { if (activeCall != null) { val type = if (VERSION.SDK_INT >= 30) { if (ContextCompat.checkSelfPermission(this, RECORD_AUDIO) == PackageManager.PERMISSION_GRANTED) FOREGROUND_SERVICE_TYPE_PHONE_CALL or FOREGROUND_SERVICE_TYPE_MICROPHONE else FOREGROUND_SERVICE_TYPE_PHONE_CALL } else 0 if (VERSION.SDK_INT >= 29) startForeground(STATUS_NOTIFICATION_ID, notification, type) else startForeground(STATUS_NOTIFICATION_ID, notification) isNotificationInCall = true } else { if (isNotificationInCall) { stopForeground(STOP_FOREGROUND_REMOVE) isNotificationInCall = false if (VERSION.SDK_INT >= 34) startForeground(STATUS_NOTIFICATION_ID, notification, ServiceInfo.FOREGROUND_SERVICE_TYPE_SPECIAL_USE) else if (VERSION.SDK_INT >= 30) startForeground(STATUS_NOTIFICATION_ID, notification, 0) else startForeground(STATUS_NOTIFICATION_ID, notification) } else { // Use standard notify for standby updates. // This is much more stable for background registration. nm.notify(STATUS_NOTIFICATION_ID, notification) } } } catch (e: Exception) { Log.e(TAG, "Failed to update foreground notification: ${e.message}") nm.notify(STATUS_NOTIFICATION_ID, notification) } } @SuppressLint("WakelockTimeout") private fun updatePartialWakeLock() { // Hold the wake lock as long as the service is active to ensure // native SIP timers (registration, keep-alives) continue to run. try { if (!partialWakeLock.isHeld) { Log.i(TAG, "Acquiring Partial Wake Lock") partialWakeLock.acquire() } } catch (e: Exception) { Log.e(TAG, "Error managing partialWakeLock: ${e.message}") } } private fun toast(message: String, length: Int = Toast.LENGTH_SHORT) { Handler(Looper.getMainLooper()).post { Toast.makeText(this@BaresipService.applicationContext, message, length).show() } } @SuppressLint("FullScreenIntentPolicy") fun handleIncomingCall(call: Call) { val ua = call.ua val peerUri = call.peerUri val callp = call.callp val callerNumber = peerUri.split(":")[1].split("@")[0] if (shouldStartRinging(callerNumber)) startRinging() if (!Utils.isVisible()) { val piFlags = PendingIntent.FLAG_IMMUTABLE or PendingIntent.FLAG_UPDATE_CURRENT val intent = Intent(applicationContext, MainActivity::class.java) .putExtra("action", "call show") .putExtra("callp", callp) intent.flags = Intent.FLAG_ACTIVITY_SINGLE_TOP or Intent.FLAG_ACTIVITY_CLEAR_TOP or Intent.FLAG_ACTIVITY_NEW_TASK val pi = PendingIntent.getActivity(applicationContext, CALL_REQ_CODE, intent, piFlags) val channelId = HIGH_CHANNEL_ID val nb = NotificationCompat.Builder(this, channelId) val caller = Utils.friendlyUri(this, peerUri, ua.account) val callerContact = Contact.findContact(peerUri) val personBuilder = Person.Builder().setName(caller) val contactColor = callerContact?.color() ?: "#B0B0B0" val initial = if (caller.isNotEmpty()) caller.take(1) else "?" val textAvatarBitmap = Utils.createTextAvatar(initial, contactColor) var icon = IconCompat.createWithBitmap(textAvatarBitmap) if (callerContact is Contact.BaresipContact) { if (callerContact.avatarImage != null) icon = IconCompat.createWithBitmap(callerContact.avatarImage!!.toCircle()) } else if (callerContact is Contact.AndroidContact) { if (callerContact.thumbnailUri != null) { try { val source = ImageDecoder.createSource(contentResolver, callerContact.thumbnailUri!!) val bitmap = ImageDecoder.decodeBitmap(source) { decoder, _, _ -> decoder.allocator = ImageDecoder.ALLOCATOR_SOFTWARE } icon = IconCompat.createWithBitmap(bitmap.toCircle()) } catch (e: Exception) { Log.e(TAG, "Failed to load Android contact avatar: $e") } } } val person = personBuilder.setIcon(icon).build() nb.setSmallIcon(R.drawable.ic_notification_call) .setColor(ContextCompat.getColor(this, R.color.colorPrimary)) .setContentIntent(pi) .setCategory(Notification.CATEGORY_CALL) .setAutoCancel(false) .setOngoing(true) .setContentText(getString(R.string.is_calling)) .setWhen(System.currentTimeMillis()) .setShowWhen(true) .setVisibility(NotificationCompat.VISIBILITY_PUBLIC) .setPriority(NotificationCompat.PRIORITY_MAX) if (VERSION.SDK_INT < 34 || nm.canUseFullScreenIntent()) nb.setFullScreenIntent(pi, true) val answerIntent = Intent(applicationContext, MainActivity::class.java) .putExtra("action", "call answer") .putExtra("callp", callp) val api = PendingIntent.getActivity(applicationContext, ANSWER_REQ_CODE, answerIntent, piFlags) val rejectIntent = Intent(this, BaresipService::class.java) rejectIntent.action = "Call Reject" rejectIntent.putExtra("callp", callp) val rpi = PendingIntent.getService(this, REJECT_REQ_CODE, rejectIntent, piFlags) nb.setStyle(NotificationCompat.CallStyle.forIncomingCall(person, rpi, api)) nm.notify(CALL_NOTIFICATION_ID, nb.build()) } postServiceEvent(ServiceEvent("call incoming", arrayListOf(ua.uap, callp), System.nanoTime())) } private fun startRinging() { am.mode = AudioManager.MODE_RINGTONE rt!!.isLooping = true rt!!.play() if (shouldVibrate()) { val effect = VibrationEffect.createOneShot(500, VibrationEffect.DEFAULT_AMPLITUDE) vbTimer = Timer() vbTimer!!.schedule(object : TimerTask() { override fun run() { if (VERSION.SDK_INT >= 33) { vibrator.vibrate( effect, android.os.VibrationAttributes.Builder() .setUsage(android.os.VibrationAttributes.USAGE_RINGTONE) .build() ) } else { val attributes = AudioAttributes.Builder() .setUsage(AudioAttributes.USAGE_NOTIFICATION_RINGTONE) .setContentType(AudioAttributes.CONTENT_TYPE_SONIFICATION) .build() @Suppress("DEPRECATION") vibrator.vibrate(effect, attributes) } } }, 500L, 2000L) } } private fun shouldStartRinging(callerNumber: String): Boolean { val currentFilter = nm.currentInterruptionFilter if (currentFilter <= NotificationManager.INTERRUPTION_FILTER_ALL) return true val channel = nm.getNotificationChannel(HIGH_CHANNEL_ID) if (channel != null && channel.canBypassDnd()) return true return isStarredContact(callerNumber) } private fun shouldVibrate(): Boolean { // 1. If the phone is in Silent mode, never vibrate if (am.ringerMode == AudioManager.RINGER_MODE_SILENT) return false // 2. If the phone is in Vibrate mode, always vibrate if (am.ringerMode == AudioManager.RINGER_MODE_VIBRATE) return true // 3. If the phone is in Normal (Ringing) mode: // First, check if the ringer volume is actually non-zero if (am.getStreamVolume(AudioManager.STREAM_RING) == 0) return false // Finally, check the system "Vibrate for calls" setting. // Although deprecated, it is the standard way to check the "Also vibrate for calls" toggle. return try { @Suppress("DEPRECATION") Settings.System.getInt(contentResolver, Settings.System.VIBRATE_WHEN_RINGING, 0) != 0 } catch (_: Exception) { // If the setting can't be read, default to FALSE. false } } private fun isStarredContact(callerNumber: String): Boolean { if (contactsMode == "baresip" || callerNumber.isBlank()) return false val phoneUri = Uri.withAppendedPath( ContactsContract.PhoneLookup.CONTENT_FILTER_URI, Uri.encode(callerNumber) ) val projection = arrayOf(ContactsContract.PhoneLookup.STARRED) try { val cursor = contentResolver.query(phoneUri, projection, null, null, null) cursor?.use { if (it.moveToFirst()) { val isStarred = it.getInt(0) == 1 if (isStarred) { Log.d(TAG, "Caller '$callerNumber' is a starred contact.") } return isStarred } } } catch (e: Exception) { Log.e(TAG, "Could not query contacts for starred status: ${e.message}") } return false } private fun stopRinging() { rt!!.stop() if (vbTimer != null) { vbTimer!!.cancel() vbTimer = null } } @SuppressLint("DiscouragedApi") private fun playRingBack() { if (mediaPlayer == null) { val name = "ringback_$toneCountry" val resourceId = resources.getIdentifier(name, "raw", packageName) if (resourceId != 0) { val audioAttributes = AudioAttributes.Builder() .setUsage(AudioAttributes.USAGE_VOICE_COMMUNICATION) .setContentType(AudioAttributes.CONTENT_TYPE_SPEECH) .build() mediaPlayer = MediaPlayer().apply { setAudioAttributes(audioAttributes) val afd = resources.openRawResourceFd(resourceId) setDataSource(afd.fileDescriptor, afd.startOffset, afd.length) afd.close() isLooping = true prepare() start() } } else { Log.e(TAG, "Ringback tone $name.wav not found") } } } @SuppressLint("DiscouragedApi") private fun playBusy() { if (mediaPlayer == null) { val name = "busy_$toneCountry" val resourceId = resources.getIdentifier(name, "raw", packageName) if (resourceId != 0) { val audioAttributes = AudioAttributes.Builder() .setUsage(AudioAttributes.USAGE_VOICE_COMMUNICATION) .setContentType(AudioAttributes.CONTENT_TYPE_SPEECH) .build() mediaPlayer = MediaPlayer().apply { setAudioAttributes(audioAttributes) val afd = resources.openRawResourceFd(resourceId) setDataSource(afd.fileDescriptor, afd.startOffset, afd.length) afd.close() setOnCompletionListener { stopMediaPlayer() ensureCommunicationMode() } prepare() start() } } else { Log.e(TAG, "Busy tone $name.wav not found") } } } private fun playUnInterrupted(raw: Int, count: Int) { val player = MediaPlayer.create(this, raw) player.setOnCompletionListener { it.stop() it.release() if (count > 1) playUnInterrupted(raw, count - 1) } player.start() } private fun stopMediaPlayer() { mediaPlayer?.stop() mediaPlayer?.release() mediaPlayer = null } private fun setCallVolume() { 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) Log.d(TAG, "Orig/new/max $streamType volume is " + "${origVolume[streamType]}/${am.getStreamVolume(streamType)}/$maxVolume") } } private fun resetCallVolume() { if (callVolume != 0) for ((streamType, streamVolume) in origVolume) { am.setStreamVolume(streamType, streamVolume, 0) Log.d(TAG, "Reset $streamType volume to ${am.getStreamVolume(streamType)}") } } private fun ensureCommunicationMode() { if (Call.inCall()) { if (am.mode != MODE_IN_COMMUNICATION) { am.mode = MODE_IN_COMMUNICATION Log.d(TAG, "Manual Mode Guard (SDK ${VERSION.SDK_INT}): Setting MODE_IN_COMMUNICATION") } } else { if (am.mode != MODE_NORMAL) { am.mode = MODE_NORMAL Log.d(TAG, "Manual Mode Guard (SDK ${VERSION.SDK_INT}): Resetting to MODE_NORMAL") } Utils.clearCommunicationDevice(am) if (speakerPhone) { speakerPhone = false postServiceEvent(ServiceEvent("speaker update,false", arrayListOf(0L, 0L), System.nanoTime())) } if (isMicMuted) { isMicMuted = false postServiceEvent(ServiceEvent("mic muted,false", arrayListOf(0L, 0L), System.nanoTime())) } resetCallVolume() proximitySensing(false) } } @SuppressLint("WakelockTimeout", "Wakelock") private fun proximitySensing(enable: Boolean) { if (enable) { if (!proximityWakeLock.isHeld) { Log.d(TAG, "Acquiring proximity wake lock") proximityWakeLock.acquire() } else { Log.d(TAG, "Proximity wake lock already acquired") } } else { if (proximityWakeLock.isHeld) { proximityWakeLock.release() Log.d(TAG, "Released proximity wake lock") } else { Log.d(TAG, "Proximity wake lock is not held") } } } private fun updateNetwork() { if (!isNativeReady) return val dnsChanged = updateDnsServers() val addresses = linkAddresses() if (linkAddresses != addresses) Log.d(TAG, "Old/new link addresses $linkAddresses/$addresses") var added = 0 for (a in addresses) if (!linkAddresses.containsKey(a.key)) { if (Api.net_add_address_ifname(a.key, a.value) != 0) Log.e(TAG, "Failed to add address: $a") else added++ } var removed = 0 for (a in linkAddresses) if (!addresses.containsKey(a.key)) { if (Api.net_rm_address(a.key) != 0) Log.e(TAG, "Failed to remove address: $a") else removed++ } val active = cm.activeNetwork if (added != removed || activeNetwork != active || dnsChanged) Log.d(TAG, "Added/Removed = $added/$removed Old/New Active = $activeNetwork/$active DNS Changed = $dnsChanged") if (added > 0 || removed > 0 || active != activeNetwork || dnsChanged) { Api.net_debug() linkAddresses = addresses activeNetwork = active Api.uag_reset_transp(register = true, reinvite = true) } val hasWifi = allNetworks.any { network -> val caps = cm.getNetworkCapabilities(network) caps != null && caps.hasTransport(NetworkCapabilities.TRANSPORT_WIFI) } if (hasWifi) { if (!wifiLock.isHeld) { Log.d(TAG, "Acquiring WiFi Lock") wifiLock.acquire() } } else { if (wifiLock.isHeld) { Log.d(TAG, "Releasing WiFi Lock") wifiLock.release() } } } private fun linkAddresses(): MutableMap { val addresses = mutableMapOf() synchronized(allNetworks) { for (n in allNetworks) { val caps = cm.getNetworkCapabilities(n) ?: continue if (caps.hasCapability(NetworkCapabilities.NET_CAPABILITY_FOREGROUND) || caps.hasTransport(NetworkCapabilities.TRANSPORT_VPN) ) { val props = cm.getLinkProperties(n) ?: continue for (la in props.linkAddresses) if (la.scope == OsConstants.RT_SCOPE_UNIVERSE && props.interfaceName != null && la.address.hostAddress != null && afMatch(la.address.hostAddress!!)) addresses[la.address.hostAddress!!] = props.interfaceName!! } } } if (hotSpotIsEnabled) { for ((k, v) in hotSpotAddresses) if (afMatch(k)) addresses[k] = v } return addresses } private fun afMatch(address: String): Boolean { return when (addressFamily) { "" -> true "ipv4" -> address.contains(".") else -> address.contains(":") } } private fun updateDnsServers(): Boolean { if (isServiceRunning && !dynDns) return false val servers = mutableListOf() // Use DNS servers first from active network (if available) val activeNetwork = cm.activeNetwork if (activeNetwork != null) { val linkProps = cm.getLinkProperties(activeNetwork) if (linkProps != null) servers.addAll(linkProps.dnsServers) } // Then add DNS servers from the other networks for (n in allNetworks) { if (n == cm.activeNetwork) continue val linkProps = cm.getLinkProperties(n) if (linkProps != null) for (server in linkProps.dnsServers) if (!servers.contains(server)) servers.add(server) } // Update if change if (servers != dnsServers) { if (isServiceRunning && Config.updateDnsServers(servers) != 0) { Log.w(TAG, "Failed to update DNS servers '${servers}'") } else { // Log.d(TAG, "Updated DNS servers: '${servers}'") dnsServers = servers return true } } return false } private fun registerAndroidContactsObserver() { if (!androidContactsObserverRegistered) try { contentResolver.registerContentObserver(ContactsContract.Contacts.CONTENT_URI, true, androidContactsObserver) androidContactsObserverRegistered = true } catch (e: SecurityException) { Log.i(TAG, "No Contacts permission: ${e.message}") } } private fun unRegisterAndroidContactsObserver() { if (androidContactsObserverRegistered) { contentResolver.unregisterContentObserver(androidContactsObserver) androidContactsObserverRegistered = false } } private fun registerPhoneAccount() { val phoneAccountHandle = getPhoneAccountHandle(this) val phoneAccount = android.telecom.PhoneAccount.builder(phoneAccountHandle, getString(R.string.app_name)) .setCapabilities(android.telecom.PhoneAccount.CAPABILITY_SELF_MANAGED) .addSupportedUriScheme(android.telecom.PhoneAccount.SCHEME_SIP) .addSupportedUriScheme(android.telecom.PhoneAccount.SCHEME_TEL) .build() tm.registerPhoneAccount(phoneAccount) } private fun cleanService() { if (!isServiceClean) { try { if (hotSpotReceiverRegistered) { applicationContext.unregisterReceiver(hotSpotReceiver) hotSpotReceiverRegistered = false } } catch (_: IllegalArgumentException) { Log.e(TAG, "hotSpotReceiver was not registered with applicationContext") } val callps = ConnectionService.connections.keys.toList() for (callp in callps) ConnectionService.onCallClosed(callp) ConnectionService.pendingOutgoingConnection?.let { it.setDisconnected(DisconnectCause(DisconnectCause.CANCELED)) it.destroy() ConnectionService.pendingOutgoingConnection = null } stopRinging() stopMediaPlayer() uas.value = emptyList() uasStatus.value = emptyMap() callHistory.clear() messages = emptyList() if (this::nm.isInitialized) nm.cancelAll() if (this::partialWakeLock.isInitialized && partialWakeLock.isHeld) partialWakeLock.release() if (this::proximityWakeLock.isInitialized && proximityWakeLock.isHeld) proximityWakeLock.release() if (this::wifiLock.isInitialized) wifiLock.release() if (this::networkCallback.isInitialized) cm.unregisterNetworkCallback(networkCallback) if (this::androidContactsObserver.isInitialized) contentResolver.unregisterContentObserver(androidContactsObserver) isServiceClean = true } } private external fun baresipStart( path: String, addresses: String, logLevel: Int, software: String ) external fun baresipStop(force: Boolean) @SuppressLint("MutableCollectionMutableState") companion object { private const val TAG = "BaresipService" var instance: BaresipService? = null var isServiceRunning = false var isNativeReady = false var isStartReceived = false var isConfigInitialized = false var libraryLoaded = false var callVolume = 0 var speakerPhone = false var audioDelay = if (VERSION.SDK_INT < 31) 1500L else 500L var dynDns = false var filesPath = "" var pName = "" var logLevel = 2 var sipTrace = false var callActionUri = "" var isMainVisible = false var isMicMuted = false var isRecOn = false var toneCountry = "us" var proximitySensing = true val uas = mutableStateOf(emptyList()) val uasStatus = mutableStateOf(emptyMap()) var contacts by mutableStateOf(mutableListOf()) val baresipContacts = mutableStateOf(emptyList()) val androidContacts = mutableStateOf(emptyList()) val contactNames = mutableStateOf(emptyList()) val darkTheme = mutableStateOf(false) val dynamicColors = mutableStateOf(false) var messages by mutableStateOf(emptyList()) val messageUpdate = MutableLiveData() val registrationUpdate = MutableLiveData() val serviceEvent = MutableLiveData>() val serviceEvents = mutableListOf() val calls = ArrayList() var callHistory = ArrayList() var blocked = ArrayList() var contactsMode = "baresip" var addressFamily = "" var dnsServers = listOf() // of those accounts that have auth username without auth password val aorPasswords = mutableMapOf() var aecAvailable = false private var aec: AcousticEchoCanceler? = null var agcAvailable = false var rt: Ringtone? = null var colorblind = false val circleGreen = mapOf(true to R.drawable.circle_green_blind, false to R.drawable.circle_green) val circleYellow = mapOf(true to R.drawable.circle_yellow_blind, false to R.drawable.circle_yellow) val circleRed = mapOf(true to R.drawable.circle_red_blind, false to R.drawable.circle_red) private var agc: AutomaticGainControl? = null private val nsAvailable = NoiseSuppressor.isAvailable() private var ns: NoiseSuppressor? = null private var recorderSessionId = 0 internal const val KEY_TEXT_REPLY = "key_text_reply_baresip" private const val PHONE_ACCOUNT_ID = "baresip_phone_account" fun getPhoneAccountHandle(ctx: Context): PhoneAccountHandle { val componentName = android.content.ComponentName(ctx, ConnectionService::class.java) return PhoneAccountHandle(componentName, PHONE_ACCOUNT_ID) } fun postServiceEvent(event: ServiceEvent) { serviceEvents.add(event) if (serviceEvents.size == 1) { Log.d(TAG, "Posted service event ${event.event} at ${event.timeStamp}") serviceEvent.postValue(Event(event.timeStamp)) } else { Log.d(TAG, "Added service event ${event.event}") } } } } ================================================ FILE: app/src/main/kotlin/com/tutpro/baresip/Blocked.kt ================================================ package com.tutpro.baresip import kotlinx.serialization.Serializable import kotlinx.serialization.json.Json import java.io.File @Serializable class Blocked ( val aor: String, val peerUri: String, val request: String, val timeStamp: Long ) { private val blockedSize = 128 fun add() { BaresipService.blocked.add(this) val aorBlocked = BaresipService.blocked.filter { it.aor == this.aor && it.request == this.request } if (aorBlocked.size > blockedSize) { val oldestToRemove = aorBlocked.first() BaresipService.blocked.remove(oldestToRemove) } save() } companion object { fun clear(aor: String) { val updatedBlockedList = BaresipService.blocked.filter { it.aor != aor } BaresipService.blocked = ArrayList(updatedBlockedList) save() } fun save() { Log.d(TAG, "Saving ${BaresipService.blocked.size} blocked calls and messages") val file = File(BaresipService.filesPath + "/blocked.json") try { val jsonString = Json.encodeToString(BaresipService.blocked) file.writeText(jsonString) } catch (e: Exception) { Log.e(TAG, "Serialization exception: $e") e.printStackTrace() } } fun restore() { val file = File(BaresipService.filesPath + "/blocked.json") if (file.exists()) { try { val jsonString = file.readText() val blockedList = Json.decodeFromString>(jsonString) BaresipService.blocked = ArrayList(blockedList) Log.d(TAG, "Restored ${BaresipService.blocked.size} blocked calls and messages") } catch (e: Exception) { Log.e(TAG, "Deserialization exception: $e") } } } } } ================================================ FILE: app/src/main/kotlin/com/tutpro/baresip/BlockedScreen.kt ================================================ package com.tutpro.baresip import android.content.Context import androidx.activity.compose.BackHandler import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.foundation.background import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.WindowInsets import androidx.compose.foundation.layout.asPaddingValues import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.imePadding import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.statusBars import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.items import androidx.compose.foundation.lazy.rememberLazyListState import androidx.compose.material.icons.Icons import androidx.compose.material.icons.automirrored.filled.ArrowBack import androidx.compose.material.icons.filled.Menu import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.Icon import androidx.compose.material3.IconButton import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Scaffold import androidx.compose.material3.Text import androidx.compose.material3.TopAppBar import androidx.compose.material3.TopAppBarDefaults import androidx.compose.runtime.Composable import androidx.compose.runtime.DisposableEffect import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.MutableState import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableIntStateOf import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import androidx.lifecycle.Lifecycle import androidx.lifecycle.LifecycleEventObserver import androidx.lifecycle.compose.LocalLifecycleOwner import androidx.navigation.NavController import androidx.navigation.NavGraphBuilder import androidx.navigation.NavType import androidx.navigation.compose.composable import androidx.navigation.navArgument import com.tutpro.baresip.CustomElements.AlertDialog import com.tutpro.baresip.CustomElements.verticalScrollbar import java.util.GregorianCalendar fun NavGraphBuilder.blockedScreenRoute(navController: NavController) { composable( route = "blocked/{request}/{aor}", arguments = listOf( navArgument("aor") { type = NavType.StringType }, navArgument("request") { type = NavType.StringType } ) ) { backStackEntry -> val aor = backStackEntry.arguments?.getString("aor")!! val request = backStackEntry.arguments?.getString("request")!! BlockedScreen(navController, request, aor) } } @OptIn(ExperimentalMaterial3Api::class) @Composable private fun BlockedScreen(navController: NavController, request: String, aor: String) { val account = Account.ofAor(aor)!! val blocked: MutableState> = remember { mutableStateOf(emptyList()) } var isBlockedLoaded by remember { mutableStateOf(false) } var refreshTrigger by remember { mutableIntStateOf(0) } val lifecycleOwner = LocalLifecycleOwner.current LaunchedEffect(aor, refreshTrigger) { blocked.value = loadBlocked(request, aor) isBlockedLoaded = true } DisposableEffect(lifecycleOwner) { val observer = LifecycleEventObserver { _, event -> if (event == Lifecycle.Event.ON_RESUME) refreshTrigger++ } lifecycleOwner.lifecycle.addObserver(observer) onDispose { lifecycleOwner.lifecycle.removeObserver(observer) } } BackHandler(enabled = true) { navController.navigateUp() } Scaffold( modifier = Modifier .fillMaxSize() .imePadding(), containerColor = MaterialTheme.colorScheme.background, topBar = { Column( modifier = Modifier .fillMaxWidth() .background(MaterialTheme.colorScheme.background) .padding(top = WindowInsets.statusBars.asPaddingValues().calculateTopPadding()) ) { TopAppBar(navController, account, request, blocked) } }, content = { contentPadding -> if (isBlockedLoaded) BlockedContent( LocalContext.current, navController, contentPadding, account, blocked ) }, ) } @OptIn(ExperimentalMaterial3Api::class) @Composable private fun TopAppBar( navController: NavController, account: Account, request: String, blocked: MutableState> ) { var expanded by remember { mutableStateOf(false) } val delete = stringResource(R.string.delete) val showDialog = remember { mutableStateOf(false) } val lastAction = remember { mutableStateOf({}) } AlertDialog( showDialog = showDialog, title = stringResource(R.string.confirmation), message = String.format(stringResource(R.string.blocked_delete_alert), account.text()), firstButtonText = stringResource(R.string.cancel), lastButtonText = stringResource(R.string.delete), onLastClicked = lastAction.value, ) TopAppBar( title = { Text( text = if (request == "invite") stringResource(R.string.blocked_calls) else stringResource(R.string.blocked_messages), fontWeight = FontWeight.Bold ) }, colors = TopAppBarDefaults.topAppBarColors( containerColor = MaterialTheme.colorScheme.primary, navigationIconContentColor = MaterialTheme.colorScheme.onPrimary, titleContentColor = MaterialTheme.colorScheme.onPrimary, actionIconContentColor = MaterialTheme.colorScheme.onPrimary ), windowInsets = WindowInsets(0, 0, 0, 0), navigationIcon = { IconButton( onClick = { navController.navigateUp() } ) { Icon( imageVector = Icons.AutoMirrored.Filled.ArrowBack, contentDescription = "Back", ) } }, actions = { IconButton( onClick = { expanded = !expanded } ) { Icon( imageVector = Icons.Filled.Menu, contentDescription = "Menu", ) } CustomElements.DropdownMenu( expanded, { expanded = false }, listOf(delete), onItemClick = { selectedItem -> expanded = false when (selectedItem) { delete -> { lastAction.value = { Blocked.clear(account.aor) blocked.value = emptyList() } showDialog.value = true } } } ) } ) } @Composable private fun BlockedContent( ctx: Context, navController: NavController, contentPadding: PaddingValues, account: Account, blocked: MutableState> ) { Column( modifier = Modifier .fillMaxWidth() .padding(contentPadding) .padding(bottom = 16.dp), verticalArrangement = Arrangement.spacedBy(12.dp) ) { Account(account) Blocked(ctx, navController, blocked) } } @Composable private fun Account(account: Account) { Text( text = stringResource(R.string.account) + " " + account.text(), modifier = Modifier .fillMaxWidth() .padding(top = 8.dp), fontSize = 18.sp, fontWeight = FontWeight.SemiBold, textAlign = TextAlign.Center ) } @OptIn(ExperimentalFoundationApi::class) @Composable private fun Blocked(ctx: Context, navController: NavController, blocked: MutableState>) { val showDialog = remember { mutableStateOf(false) } val message = remember { mutableStateOf("") } val lastButtonText = remember { mutableStateOf("") } val lastAction = remember { mutableStateOf({}) } AlertDialog( showDialog = showDialog, title = stringResource(R.string.confirmation), message = message.value, firstButtonText = stringResource(R.string.cancel), lastButtonText = lastButtonText.value, onLastClicked = lastAction.value, ) val lazyListState = rememberLazyListState() LazyColumn( modifier = Modifier .fillMaxWidth() .padding(start = 16.dp, end = 4.dp) .verticalScrollbar(state = lazyListState) .background(MaterialTheme.colorScheme.background), state = lazyListState, verticalArrangement = Arrangement.spacedBy(12.dp), ) { items(items = blocked.value, key = { blocked -> blocked.timeStamp }) { blocked -> val peerUri = blocked.peerUri Row( verticalAlignment = Alignment.CenterVertically, modifier = Modifier.fillMaxWidth() .clickable(onClick = { message.value = String.format(ctx.getString(R.string.blocked_contact_question), peerUri) lastButtonText.value = ctx.getString(R.string.add_contact) lastAction.value = { navController.navigate("baresip_contact/$peerUri/new") } showDialog.value = true }) ) { Text(text = "\u2022", modifier = Modifier.padding(start = 8.dp, end = 4.dp), fontSize = 18.sp) Text(text = peerUri.replace("sip:", ""), fontSize = 18.sp, maxLines = 1, overflow = TextOverflow.Ellipsis ) Spacer(modifier = Modifier.weight(1f)) val calendar = GregorianCalendar() calendar.timeInMillis = blocked.timeStamp Text( text = Utils.relativeTime(ctx, calendar), fontSize = 12.sp, minLines = 2, maxLines = 2, lineHeight = 16.sp, textAlign = TextAlign.End, color = MaterialTheme.colorScheme.onBackground, modifier = Modifier.padding(end = 16.dp) ) } } } } private fun loadBlocked(request: String, aor: String): MutableList { val res = mutableListOf() for (i in BaresipService.blocked.indices.reversed()) { val b = BaresipService.blocked[i] if (b.aor == aor && b.request == request) { res.add(Blocked("", b.peerUri, "", b.timeStamp)) } } Log.d(TAG, "Loaded ${res.size} blocked $request requests") return res } ================================================ FILE: app/src/main/kotlin/com/tutpro/baresip/BootCompletedReceiver.kt ================================================ package com.tutpro.baresip import android.content.BroadcastReceiver import android.content.Context import android.content.Intent import androidx.core.content.ContextCompat import java.nio.charset.StandardCharsets class BootCompletedReceiver : BroadcastReceiver() { override fun onReceive(context: Context, intent: Intent) { Log.i(TAG, "BootCompletedReceiver received intent ${intent.action}") val configPath = context.filesDir.absolutePath + "/config" val config = Utils.getFileContents(configPath) ?: return val asCv = Utils.getNameValue(String(config, StandardCharsets.ISO_8859_1), "auto_start") if ((asCv.isNotEmpty()) && (asCv[0] == "yes")) { Log.i(TAG, "Start baresip service upon boot completed") val baresipService = Intent(context, BaresipService::class.java).apply { action = "Start" putExtra("onStartup", true) } ContextCompat.startForegroundService(context, baresipService) } } } ================================================ FILE: app/src/main/kotlin/com/tutpro/baresip/Call.kt ================================================ package com.tutpro.baresip import android.content.Context import android.media.AudioManager import androidx.compose.runtime.MutableState import androidx.compose.runtime.mutableIntStateOf import androidx.compose.runtime.mutableStateOf import androidx.core.net.toUri import java.util.* class Call(val callp: Long, val ua: UserAgent, val peerUri: String, val dir: String, initialStatus: String) { var status: MutableState = mutableStateOf(initialStatus) var onhold = false var held = false val terminated = mutableStateOf(false) var conferenceCall = false var onHoldCall: Call? = null var newCall: Call? = null var rejected = false // Incoming rejected by user or outgoing fails but not due to 408 or 480 var security = R.color.colorTrafficRed var zid = "" var startTime: GregorianCalendar? = null // Set when call is established var referTo = "" var dumpfiles = arrayOf("", "") // UI state properties val callUri: MutableState = mutableStateOf("") val callUriEnabled: MutableState = mutableStateOf(true) val callUriLabel: MutableState = mutableStateOf("") val securityIconTint: MutableState = mutableIntStateOf(-1) val showCallTimer: MutableState = mutableStateOf(false) var callDuration: Int = 0 val showSuggestions: MutableState = mutableStateOf(false) val showCallButton: MutableState = mutableStateOf(true) val showCancelButton: MutableState = mutableStateOf(false) val showAnswerRejectButtons: MutableState = mutableStateOf(false) val showHangupButton: MutableState = mutableStateOf(false) val showOnHoldNotice: MutableState = mutableStateOf(false) val callOnHold: MutableState = mutableStateOf(false) val transferButtonEnabled: MutableState = mutableStateOf(false) val callTransfer: MutableState = mutableStateOf(false) val dtmfText: MutableState = mutableStateOf("") val dtmfEnabled: MutableState = mutableStateOf(false) val focusDtmf: MutableState = mutableStateOf(false) fun add() { BaresipService.calls.add(this) } fun remove() { BaresipService.calls.remove(this) } fun connect(uri: String): Boolean { return Api.call_connect(callp, uri) == 0 } fun hold(): Boolean { if (onhold) return true if (Api.call_hold(callp, true)) { onhold = true callOnHold.value = true showOnHoldNotice.value = false ConnectionService.connections[callp]?.setOnHold() return true } return false } fun resume(): Boolean { if (!onhold && !held) return true // 1. Hold other calls first for (c in BaresipService.calls) { if (c.callp != this.callp && !c.onhold && !c.held) { Log.d("Baresip", "Auto-holding active call ${c.callp}") c.hold() } } val connection = ConnectionService.connections[callp] // 2. SIP Signaling if (Api.call_hold(callp, false)) { onhold = false callOnHold.value = false showOnHoldNotice.value = false // 3. Telecom Sync connection?.setAddress("sip:$peerUri".toUri(), android.telecom.TelecomManager.PRESENTATION_ALLOWED) connection?.setActive() return true } return false } fun transfer(uri: String): Boolean { if (!onhold) hold() Log.d(TAG, "Transferring call $callp to $uri") return Api.call_transfer(callp, uri) == 0 } fun executeTransfer(): Boolean { return if (onHoldCall != null) { if (Api.call_hold(callp, true)) Api.call_replace_transfer(onHoldCall!!.callp, callp) else false } else false } fun sendDigit(digit: Char): Int { return Api.call_send_digit(callp, digit) } fun notifySipfrag(code: Int, reason: String) { Api.call_notify_sipfrag(callp, code, reason) } fun duration(): Int { return Api.call_duration(callp) } fun stats(stream: String): String { return Api.call_stats(callp, stream) } fun state(): Int { return Api.call_state(callp) } fun audioCodecs(): String { return Api.call_audio_codecs(callp) } fun replaces(): Boolean { return Api.call_replaces(callp) } fun diverterUri(): String { return Api.call_diverter_uri(callp) } init { if (ua.account.mediaEnc != "") security = R.color.colorTrafficRed } fun destroy() { Api.call_destroy(callp) } companion object { fun calls(): ArrayList { return BaresipService.calls } fun ofCallp(callp: Long): Call? { for (c in BaresipService.calls) if (c.callp == callp) return c return null } fun call(status: String): Call? { for (c in BaresipService.calls) if (c.status.value == status) return c return null } fun inCall(): Boolean { return BaresipService.calls.isNotEmpty() } fun isAnyCallActive(ctx: Context): Boolean { // Check if there exist SIP calls that are not onhold or held if (BaresipService.calls.any { !it.onhold && !it.held }) return true // MODE_IN_CALL indicates a PSTN call is active val am = ctx.getSystemService(Context.AUDIO_SERVICE) as AudioManager return am.mode == AudioManager.MODE_IN_CALL } } } ================================================ FILE: app/src/main/kotlin/com/tutpro/baresip/CallDetailsScreen.kt ================================================ package com.tutpro.baresip import android.content.Context import android.media.AudioAttributes import android.media.MediaPlayer import android.text.format.DateUtils import android.widget.Toast import androidx.activity.compose.rememberLauncherForActivityResult import androidx.activity.result.contract.ActivityResultContracts import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.foundation.background import androidx.compose.foundation.clickable import androidx.compose.foundation.combinedClickable import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.WindowInsets import androidx.compose.foundation.layout.asPaddingValues import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.imePadding import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.statusBars import androidx.compose.foundation.layout.width import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.items import androidx.compose.foundation.lazy.rememberLazyListState import androidx.compose.foundation.shape.CircleShape import androidx.compose.material.icons.Icons import androidx.compose.material.icons.automirrored.filled.ArrowBack import androidx.compose.material.icons.automirrored.filled.CallMade import androidx.compose.material.icons.automirrored.filled.CallReceived import androidx.compose.material3.AlertDialog import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.Icon import androidx.compose.material3.IconButton import androidx.compose.material3.LinearProgressIndicator import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Scaffold import androidx.compose.material3.Text import androidx.compose.material3.TextButton import androidx.compose.material3.TopAppBar import androidx.compose.material3.TopAppBarDefaults import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.MutableState import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableFloatStateOf import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.setValue import androidx.compose.runtime.snapshots.SnapshotStateList import androidx.compose.runtime.toMutableStateList import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.colorResource import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import androidx.navigation.NavController import androidx.navigation.NavGraphBuilder import androidx.navigation.compose.composable import com.tutpro.baresip.CallRow.Details import com.tutpro.baresip.CustomElements.AlertDialog import com.tutpro.baresip.CustomElements.verticalScrollbar import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.delay import kotlinx.coroutines.launch import kotlinx.coroutines.withContext import java.io.File import java.io.FileInputStream import java.text.DateFormat import java.util.GregorianCalendar fun NavGraphBuilder.callDetailsScreenRoute(navController: NavController, viewModel: ViewModel) { composable("call_details") { _ -> val callRow = remember { viewModel.consumeSelectedCallRow() } CallDetailsScreen(navController, callRow!!) } } @OptIn(ExperimentalMaterial3Api::class) @Composable private fun CallDetailsScreen(navController: NavController, callRow: CallRow) { val detailsState = remember { callRow.details.toMutableStateList() } Scaffold( modifier = Modifier.fillMaxSize().imePadding(), containerColor = MaterialTheme.colorScheme.background, topBar = { Column( modifier = Modifier .fillMaxWidth() .background(MaterialTheme.colorScheme.background) .padding(top = WindowInsets.statusBars.asPaddingValues().calculateTopPadding()) ) { TopAppBar( title = { Text( text = stringResource(R.string.call_details), fontWeight = FontWeight.Bold ) }, colors = TopAppBarDefaults.topAppBarColors( containerColor = MaterialTheme.colorScheme.primary, navigationIconContentColor = MaterialTheme.colorScheme.onPrimary, titleContentColor = MaterialTheme.colorScheme.onPrimary, ), windowInsets = WindowInsets(0, 0, 0, 0), navigationIcon = { IconButton(onClick = navController::navigateUp) { Icon( imageVector = Icons.AutoMirrored.Filled.ArrowBack, contentDescription = "Back", ) } }, ) } }, content = { contentPadding -> CallDetailsContent( LocalContext.current, contentPadding, callRow, detailsState, onDelete = { detail -> detailsState.remove(detail) if (detailsState.isEmpty()) navController.navigateUp() } ) }, ) } @Composable private fun CallDetailsContent( ctx: Context, contentPadding: PaddingValues, callRow: CallRow, details: SnapshotStateList
, onDelete: (Details) -> Unit ) { Column( modifier = Modifier .fillMaxWidth() .padding(contentPadding) .padding(top = 16.dp, start = 16.dp, end = 4.dp, bottom = 16.dp), verticalArrangement = Arrangement.spacedBy(12.dp) ) { Peer(ctx, callRow) Details(ctx, details, onDelete) } } @Composable private fun Peer(ctx: Context, callRow: CallRow) { val account = Account.ofAor(callRow.aor)!! val headerText = stringResource(R.string.peer) + " " + Utils.friendlyUri(ctx, callRow.peerUri, account) Text( text = headerText, modifier = Modifier.fillMaxWidth(), fontSize = 18.sp, fontWeight = FontWeight.SemiBold, textAlign = TextAlign.Center ) } @Composable private fun Details(ctx: Context, details: SnapshotStateList
, onDelete: (Details) -> Unit) { Row(verticalAlignment = Alignment.CenterVertically) { Text(text = stringResource(R.string.direction), fontSize = 16.sp, fontWeight = FontWeight.SemiBold, modifier = Modifier.width(96.dp) ) Spacer(modifier = Modifier.width(6.dp)) Text(text = stringResource(R.string.time), fontSize = 16.sp, fontWeight = FontWeight.SemiBold ) Spacer(modifier = Modifier.weight(1f)) Text(text = stringResource(R.string.calls_duration), modifier = Modifier.padding(end = 12.dp), fontSize = 16.sp, fontWeight = FontWeight.SemiBold ) } val lazyListState = rememberLazyListState() LazyColumn( modifier = Modifier .fillMaxWidth() .verticalScrollbar(state = lazyListState) .background(MaterialTheme.colorScheme.background), state = lazyListState, verticalArrangement = Arrangement.spacedBy(12.dp), ) { items(details) { detail -> Row(verticalAlignment = Alignment.CenterVertically) { Box( modifier = Modifier .clip(CircleShape) .clickable { Toast.makeText( ctx, when (detail.direction) { CALL_DOWN_GREEN, CALL_UP_GREEN -> ctx.getString(R.string.call_answered) CALL_DOWN_BLUE -> ctx.getString(R.string.call_answered_elsewhere) CALL_MISSED_IN, CALL_MISSED_OUT -> ctx.getString(R.string.call_missed) CALL_DOWN_RED, CALL_UP_RED -> ctx.getString(R.string.call_rejected) else -> "" }, Toast.LENGTH_SHORT ).show() }, contentAlignment = Alignment.Center ) { Icon( imageVector = if (callUp(detail.direction)) Icons.AutoMirrored.Filled.CallMade else Icons.AutoMirrored.Filled.CallReceived, tint = colorResource(id = callTint(detail.direction)), contentDescription = "Direction" ) } Spacer(modifier = Modifier.width(78.dp)) val durationText = startTime(detail, onDelete) Spacer(modifier = Modifier.weight(1f)) Duration(ctx, detail, durationText) } } } } @Composable private fun startTime(detail: Details, onDelete: (Details) -> Unit): String { val startTime = detail.startTime val stopTime = detail.stopTime val startTimeText: String val durationText: String val stopText = if (DateUtils.isToday(stopTime.timeInMillis)) { val fmt = DateFormat.getTimeInstance(DateFormat.MEDIUM) stringResource(R.string.today) + " " + fmt.format(stopTime.time) } else { val fmt = DateFormat.getDateTimeInstance() fmt.format(stopTime.time) } if (startTime == GregorianCalendar(0, 0, 0)) { startTimeText = stopText durationText = "?" } else { if (startTime == null || detail.direction == CALL_DOWN_BLUE) { startTimeText = stopText durationText = "" } else { val startText = if (DateUtils.isToday(startTime.timeInMillis)) { val fmt = DateFormat.getTimeInstance(DateFormat.MEDIUM) stringResource(R.string.today) + " " + fmt.format(startTime.time) } else { val fmt = DateFormat.getDateTimeInstance() fmt.format(startTime.time) } startTimeText = startText val duration = (stopTime.time.time - startTime.time.time) / 1000 durationText = DateUtils.formatElapsedTime(duration) } } val showDialog = remember { mutableStateOf(false) } AlertDialog( showDialog = showDialog, title = stringResource(R.string.confirmation), message = stringResource(R.string.delete_call_alert), firstButtonText = stringResource(R.string.cancel), lastButtonText = stringResource(R.string.delete), onLastClicked = { CallHistoryNew.remove(detail.startTime, detail.stopTime) onDelete(detail) }, ) Text( text = startTimeText, modifier = Modifier.combinedClickable( onClick = {}, onLongClick = { showDialog.value = true } ) ) return durationText } @OptIn(ExperimentalFoundationApi::class) @Composable private fun Duration(ctx: Context, detail: Details, durationText: String) { val showPlaybackDialog = remember { mutableStateOf(false) } val showDownloadDialog = remember { mutableStateOf(false) } val mediaPlayer = remember { MediaPlayer() } val scope = rememberCoroutineScope() // 1. Setup the File Saver Launcher val saveLauncher = rememberLauncherForActivityResult( contract = ActivityResultContracts.CreateDocument("audio/x-wav") ) { uri -> uri?.let { destinationUri -> scope.launch(Dispatchers.IO) { try { val sourcePath = detail.recording.firstOrNull { it.isNotEmpty() } if (sourcePath != null) { val sourceFile = File(sourcePath) if (sourceFile.exists()) { ctx.contentResolver.openOutputStream(destinationUri)?.use { output -> FileInputStream(sourceFile).use { input -> input.copyTo(output) } } withContext(Dispatchers.Main) { Toast.makeText( ctx, ctx.getString(R.string.recording_saved), Toast.LENGTH_SHORT ).show() } } else { withContext(Dispatchers.Main) { Toast.makeText(ctx, "Source file not found", Toast.LENGTH_SHORT) .show() } } } } catch (e: Exception) { Log.e(TAG, "Failed to save file: $e") withContext(Dispatchers.Main) { Toast.makeText(ctx, "Save failed", Toast.LENGTH_SHORT).show() } } } } } PlaybackDialog( showDialog = showPlaybackDialog, mediaPlayer = mediaPlayer, onStop = { if (mediaPlayer.isPlaying) mediaPlayer.stop() mediaPlayer.reset() showPlaybackDialog.value = false } ) // 2. Download Confirmation Dialog if (showDownloadDialog.value) AlertDialog( showDialog = showDownloadDialog, title = stringResource(R.string.save_recording), message = stringResource(R.string.save_recording_question), firstButtonText = stringResource(R.string.cancel), lastButtonText = stringResource(R.string.save), onLastClicked = { showDownloadDialog.value = false detail.recording.firstOrNull { it.isNotEmpty() }?.let { val suggestedName = File(it).name saveLauncher.launch(suggestedName) } }, ) val hasRecording = detail.recording.isNotEmpty() && detail.recording[0].isNotEmpty() if (hasRecording) { Text( text = durationText, color = MaterialTheme.colorScheme.error, modifier = Modifier .padding(end = 12.dp) .combinedClickable( onLongClick = { showDownloadDialog.value = true }, onClick = { if (!mediaPlayer.isPlaying) { mediaPlayer.reset() scope.launch(Dispatchers.IO) { var finalFile: File? = null // Use a local copy of the recording state for this operation val currentRecording = detail.recording val currentIsRaw = currentRecording.size > 1 && currentRecording[0].isNotEmpty() && currentRecording[1].isNotEmpty() val currentIsMerged = currentRecording.isNotEmpty() && currentRecording[0].isNotEmpty() && (currentRecording.size == 1 || currentRecording[1].isEmpty()) if (currentIsRaw) { val fileIn = File(currentRecording[0]) val fileOut = File(currentRecording[1]) // SAFETY CHECK: If the raw file is gone, the background service // likely finished merging just now. if (!fileIn.exists()) { val expectedMergedName = "merged_${fileIn.nameWithoutExtension}_${fileOut.nameWithoutExtension}.wav" val fallbackMerged = File( BaresipService.filesPath + "/recordings", expectedMergedName ) if (fallbackMerged.exists()) { Log.d( TAG, "Raw file missing, found merged fallback: ${fallbackMerged.name}" ) finalFile = fallbackMerged // Update state to match reality by creating a new List detail.recording = listOf(fallbackMerged.absolutePath, "") } else { Log.e( TAG, "Raw file missing and fallback not found: ${currentRecording[0]}" ) } } else { // Normal Raw processing val mergedFileName = "merged_${fileIn.nameWithoutExtension}_${fileOut.nameWithoutExtension}.wav" val mergedFile = File( BaresipService.filesPath + "/recordings", mergedFileName ) if (mergedFile.exists()) { Log.d( TAG, "Using already merged file: ${mergedFile.name}" ) finalFile = mergedFile } else { if (Utils.mergeWavFiles(fileIn, fileOut, mergedFile)) finalFile = mergedFile } // If merge successful, update state and delete originals if (finalFile != null && finalFile.exists()) { detail.recording = listOf(finalFile.absolutePath, "") try { if (fileIn.exists()) fileIn.delete() if (fileOut.exists()) fileOut.delete() CallHistoryNew.save() } catch (e: Exception) { Log.w( TAG, "MergeWav: Failed to delete original files: ${e.message}" ) } } } } else if (currentIsMerged) { val f = File(currentRecording[0]) if (f.exists()) { Log.d(TAG, "Using already merged file: ${currentRecording[0]}") finalFile = f } else { Log.e( TAG, "Merged file record exists but file is missing: ${currentRecording[0]}" ) } } withContext(Dispatchers.Main) { if (finalFile != null && finalFile.exists()) { try { mediaPlayer.apply { setAudioAttributes( AudioAttributes.Builder() .setContentType(AudioAttributes.CONTENT_TYPE_SPEECH) .setUsage(AudioAttributes.USAGE_MEDIA) .build() ) val fis = FileInputStream(finalFile) setDataSource(fis.fd) fis.close() setOnPreparedListener { it.start() showPlaybackDialog.value = true } setOnCompletionListener { showPlaybackDialog.value = false it.reset() } prepareAsync() } } catch (e: Exception) { Log.e(TAG, "Playback failed: $e") Toast.makeText( ctx, "Playback error", Toast.LENGTH_SHORT ).show() } } else { Toast.makeText( ctx, "Failed to process audio file", Toast.LENGTH_SHORT ).show() } } } } else { mediaPlayer.stop() mediaPlayer.reset() showPlaybackDialog.value = false } } ) ) } else { Text(text = durationText, modifier = Modifier.padding(end = 12.dp)) } } @Composable private fun PlaybackDialog( showDialog: MutableState, mediaPlayer: MediaPlayer, onStop: () -> Unit ) { if (showDialog.value) { // State to hold progress (0.0f to 1.0f) var currentProgress by remember { mutableFloatStateOf(0f) } var currentPositionText by remember { mutableStateOf("00:00") } var totalDurationText by remember { mutableStateOf("00:00") } // Update progress every 100ms LaunchedEffect(showDialog.value) { if (mediaPlayer.isPlaying) { val duration = mediaPlayer.duration totalDurationText = DateUtils.formatElapsedTime(duration / 1000L) while (mediaPlayer.isPlaying) { val current = mediaPlayer.currentPosition currentProgress = if (duration > 0) current.toFloat() / duration.toFloat() else 0f currentPositionText = DateUtils.formatElapsedTime(current / 1000L) delay(100) // Poll every 100ms } } } AlertDialog( onDismissRequest = { // If user clicks outside, stop playback onStop() }, title = { Text(text = stringResource(R.string.playing_recording)) }, text = { Column( modifier = Modifier.fillMaxWidth(), horizontalAlignment = Alignment.CenterHorizontally ) { LinearProgressIndicator( progress = { currentProgress }, modifier = Modifier.fillMaxWidth().height(8.dp), ) Spacer(modifier = Modifier.height(8.dp)) Row( modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.SpaceBetween ) { Text(text = currentPositionText, style = MaterialTheme.typography.bodySmall) Text(text = totalDurationText, style = MaterialTheme.typography.bodySmall) } } }, confirmButton = { TextButton( onClick = { onStop() } ) { Text(stringResource(R.string.stop)) } } ) } } ================================================ FILE: app/src/main/kotlin/com/tutpro/baresip/CallHistory.kt ================================================ package com.tutpro.baresip import java.io.File import java.io.FileInputStream import java.io.FileOutputStream import java.io.IOException import java.io.ObjectInputStream import java.io.ObjectOutputStream import java.io.Serializable import java.util.GregorianCalendar class CallHistoryNew(val aor: String, val peerUri: String, val direction: String) : Serializable { // Set to time when call is established (if ever) or stopTime if call was completed elsewhere var startTime: GregorianCalendar? = null var stopTime = GregorianCalendar() // Set to time when call is closed var rejected = false var recording = arrayOf("", "") // Encoder and decoder recording files, merged file is in [0] fun add() { BaresipService.callHistory.add(this) val aorSpecificHistory = BaresipService.callHistory.filter { it.aor == this.aor } if (aorSpecificHistory.size > CALL_HISTORY_SIZE) { val oldestToRemove = aorSpecificHistory.first() deleteRecordingFiles(oldestToRemove.recording) BaresipService.callHistory.remove(oldestToRemove) } save() } companion object { private const val serialVersionUID: Long = 3 private const val CALL_HISTORY_SIZE = 256 fun aorLatestPeerUri(aor: String): String? { for (h in BaresipService.callHistory.reversed()) if (h.aor == aor) return h.peerUri return null } fun clear(aor: String) { for (i in BaresipService.callHistory.indices.reversed()) { val h = BaresipService.callHistory[i] if (h.aor == aor) { deleteRecordingFiles(h.recording) BaresipService.callHistory.removeAt(i) } } save() } fun remove(startTime: GregorianCalendar?, stopTime: GregorianCalendar) { val iterator = BaresipService.callHistory.iterator() while (iterator.hasNext()) { val h = iterator.next() if (h.startTime == startTime && h.stopTime == stopTime) { deleteRecordingFiles(h.recording) iterator.remove() } } save() } fun save() { Log.d(TAG, "Saving history of ${BaresipService.callHistory.size} calls") val file = File(BaresipService.filesPath + "/call_history") try { val fos = FileOutputStream(file) val oos = ObjectOutputStream(fos) oos.writeObject(BaresipService.callHistory) oos.close() fos.close() } catch (e: IOException) { Log.e(TAG, "OutputStream exception: $e") e.printStackTrace() } } fun restore() { val file = File(BaresipService.filesPath + "/call_history") if (file.exists()) { try { val fis = FileInputStream(file) val ois = ObjectInputStream(fis) @Suppress("UNCHECKED_CAST") val restoredHistory = ois.readObject() as? List if (restoredHistory != null) BaresipService.callHistory = ArrayList(restoredHistory) ois.close() fis.close() Log.d(TAG, "Restored history of ${BaresipService.callHistory.size} calls") } catch (e: Exception) { Log.e(TAG, "InputStream exception: - $e") } } } fun deleteRecordingFiles(recording: Array) { Utils.deleteFile(File(recording[0])) Utils.deleteFile(File(recording[1])) } fun clearRecordings() { for (h in BaresipService.callHistory) h.recording = arrayOf("", "") } @Suppress("UNUSED") fun print() { for (h in BaresipService.callHistory) Log.d(TAG, "[${h.aor}, ${h.peerUri}, ${h.direction}, ${h.startTime}," + "${h.stopTime}, ${h.rejected}, ${h.recording}") } } } class CallHistory(val aor: String, val peerUri: String, val direction: String) : Serializable { var startTime: GregorianCalendar? = null var stopTime = GregorianCalendar() var recording = arrayOf("", "") companion object { private const val serialVersionUID: Long = 2 fun get(): ArrayList { val file = File(BaresipService.filesPath, "history") var result = ArrayList() if (file.exists()) { try { val fis = FileInputStream(file) val ois = ObjectInputStream(fis) @Suppress("UNCHECKED_CAST") val restoredHistory = ois.readObject() as? List if (restoredHistory != null) result = ArrayList(restoredHistory) ois.close() fis.close() Log.d(TAG, "Got history of ${result.size} calls") file.delete() } catch (e: Exception) { Log.e(TAG, "InputStream exception: - $e") } } return result } } } ================================================ FILE: app/src/main/kotlin/com/tutpro/baresip/CallRow.kt ================================================ package com.tutpro.baresip import java.util.GregorianCalendar data class CallRow( val aor: String, val peerUri: String, var direction: Int, var startTime: GregorianCalendar?, var stopTime: GregorianCalendar, var recording: List ) { data class Details( var direction: Int, var startTime: GregorianCalendar?, var stopTime: GregorianCalendar, var recording: List ) val details = mutableListOf(Details(direction, startTime, stopTime, recording)) } ================================================ FILE: app/src/main/kotlin/com/tutpro/baresip/CallsScreen.kt ================================================ package com.tutpro.baresip import android.content.Context import android.content.Intent import androidx.activity.compose.BackHandler import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.foundation.background import androidx.compose.foundation.clickable import androidx.compose.foundation.combinedClickable import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.WindowInsets import androidx.compose.foundation.layout.asPaddingValues import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.imePadding import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.statusBars import androidx.compose.foundation.layout.width import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.items import androidx.compose.foundation.lazy.rememberLazyListState import androidx.compose.foundation.shape.CircleShape import androidx.compose.material.icons.Icons import androidx.compose.material.icons.automirrored.filled.ArrowBack import androidx.compose.material.icons.automirrored.filled.CallMade import androidx.compose.material.icons.automirrored.filled.CallReceived import androidx.compose.material.icons.filled.AccountCircle import androidx.compose.material.icons.filled.Menu import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.Icon import androidx.compose.material3.IconButton import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Scaffold import androidx.compose.material3.Text import androidx.compose.material3.TopAppBar import androidx.compose.material3.TopAppBarDefaults import androidx.compose.runtime.Composable import androidx.compose.runtime.DisposableEffect import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.MutableState import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableIntStateOf import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip import androidx.compose.ui.draw.scale import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.colorResource import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import androidx.lifecycle.Lifecycle import androidx.lifecycle.LifecycleEventObserver import androidx.lifecycle.compose.LocalLifecycleOwner import androidx.navigation.NavController import androidx.navigation.NavGraphBuilder import androidx.navigation.NavType import androidx.navigation.compose.composable import androidx.navigation.navArgument import coil.compose.AsyncImage import com.tutpro.baresip.CustomElements.AlertDialog import com.tutpro.baresip.CustomElements.verticalScrollbar fun NavGraphBuilder.callsScreenRoute(navController: NavController, viewModel: ViewModel) { composable( route = "calls/{aor}", arguments = listOf(navArgument("aor") { type = NavType.StringType }) ) { backStackEntry -> val aor = backStackEntry.arguments?.getString("aor")!! CallsScreen(navController, viewModel, aor) } } @OptIn(ExperimentalMaterial3Api::class) @Composable private fun CallsScreen(navController: NavController, viewModel: ViewModel, aor: String) { val account = Account.ofAor(aor)!! val callHistory: MutableState> = remember { mutableStateOf(emptyList()) } var isHistoryLoaded by remember { mutableStateOf(false) } var refreshTrigger by remember { mutableIntStateOf(0) } val lifecycleOwner = LocalLifecycleOwner.current LaunchedEffect(aor, refreshTrigger) { callHistory.value = loadCallHistory(aor) isHistoryLoaded = true } DisposableEffect(lifecycleOwner) { val observer = LifecycleEventObserver { _, event -> if (event == Lifecycle.Event.ON_RESUME) refreshTrigger++ } lifecycleOwner.lifecycle.addObserver(observer) onDispose { lifecycleOwner.lifecycle.removeObserver(observer) } } BackHandler(enabled = true) { account.missedCalls = false navController.navigateUp() } Scaffold( modifier = Modifier.fillMaxSize().imePadding(), containerColor = MaterialTheme.colorScheme.background, topBar = { Column( modifier = Modifier .fillMaxWidth() .background(MaterialTheme.colorScheme.background) .padding(top = WindowInsets.statusBars.asPaddingValues().calculateTopPadding()) ) { TopAppBar(navController, account, callHistory) } }, content = { contentPadding -> if (isHistoryLoaded) CallsContent( LocalContext.current, navController, viewModel, contentPadding, account, callHistory ) }, ) } @OptIn(ExperimentalMaterial3Api::class) @Composable private fun TopAppBar(navController: NavController, account: Account, callHistory: MutableState>) { var expanded by remember { mutableStateOf(false) } val delete = stringResource(R.string.delete) val disable = stringResource(R.string.disable_history) val enable = stringResource(R.string.enable_history) val blocked = stringResource(R.string.blocked) val showDialog = remember { mutableStateOf(false) } val lastAction = remember { mutableStateOf({}) } AlertDialog( showDialog = showDialog, title = stringResource(R.string.confirmation), message = String.format(stringResource(R.string.delete_history_alert), account.text()), firstButtonText = stringResource(R.string.cancel), lastButtonText = stringResource(R.string.delete), onLastClicked = lastAction.value, ) TopAppBar( title = { Text( text = stringResource(R.string.call_history), fontWeight = FontWeight.Bold ) }, colors = TopAppBarDefaults.topAppBarColors( containerColor = MaterialTheme.colorScheme.primary, navigationIconContentColor = MaterialTheme.colorScheme.onPrimary, titleContentColor = MaterialTheme.colorScheme.onPrimary, actionIconContentColor = MaterialTheme.colorScheme.onPrimary ), windowInsets = WindowInsets(0, 0, 0, 0), navigationIcon = { IconButton( onClick = { account.missedCalls = false navController.navigateUp() } ) { Icon( imageVector = Icons.AutoMirrored.Filled.ArrowBack, contentDescription = "Back", ) } }, actions = { IconButton( onClick = { expanded = !expanded } ) { Icon( imageVector = Icons.Filled.Menu, contentDescription = "Menu", ) } CustomElements.DropdownMenu( expanded, { expanded = false }, if (account.callHistory) listOf(disable, delete, blocked) else listOf(enable), onItemClick = { selectedItem -> expanded = false when (selectedItem) { delete -> { lastAction.value = { CallHistoryNew.clear(account.aor) callHistory.value = emptyList() Blocked.clear(account.aor) } showDialog.value = true } disable, enable -> { account.callHistory = !account.callHistory Account.saveAccounts() } blocked -> { navController.navigate("blocked/invite/${account.aor}") } } } ) } ) } @Composable private fun CallsContent( ctx: Context, navController: NavController, viewModel: ViewModel, contentPadding: PaddingValues, account: Account, callHistory: MutableState> ) { Column( modifier = Modifier .fillMaxWidth() .padding(contentPadding) .padding(bottom = 16.dp), verticalArrangement = Arrangement.spacedBy(12.dp) ) { Account(account) Calls(ctx, navController, viewModel, account, callHistory) } } @Composable private fun Account(account: Account) { Text( text = stringResource(R.string.account) + " " + account.text(), modifier = Modifier.fillMaxWidth().padding(top = 8.dp), fontSize = 18.sp, fontWeight = FontWeight.SemiBold, textAlign = TextAlign.Center ) } @OptIn(ExperimentalFoundationApi::class) @Composable private fun Calls( ctx: Context, navController: NavController, viewModel: ViewModel, account: Account, callHistory: MutableState> ) { val showDialog = remember { mutableStateOf(false) } val message = remember { mutableStateOf("") } val secondButtonText = remember { mutableStateOf("") } val secondAction = remember { mutableStateOf({}) } val lastButtonText = remember { mutableStateOf("") } val lastAction = remember { mutableStateOf({}) } AlertDialog( showDialog = showDialog, title = stringResource(R.string.confirmation), message = message.value, firstButtonText = stringResource(R.string.cancel), secondButtonText = secondButtonText.value, onSecondClicked = secondAction.value, lastButtonText = lastButtonText.value, onLastClicked = lastAction.value, ) val lazyListState = rememberLazyListState() LazyColumn( modifier = Modifier .fillMaxWidth() .padding(start = 16.dp, end = 4.dp) .verticalScrollbar(state = lazyListState) .background(MaterialTheme.colorScheme.background), state = lazyListState, verticalArrangement = Arrangement.spacedBy(12.dp), ) { items(items = callHistory.value, key = { callRow -> callRow.stopTime }) { callRow -> val peerUri = callRow.peerUri var recordings = false Row( verticalAlignment = Alignment.CenterVertically, modifier = Modifier.fillMaxWidth() ) { Box(modifier = Modifier.weight(1f)) { Row( verticalAlignment = Alignment.CenterVertically, modifier = Modifier.combinedClickable( onClick = { val aor = account.aor val ua = UserAgent.ofAor(aor) val intent = Intent(ctx, MainActivity::class.java) if (ua != null) { intent.putExtra("uap", ua.uap) intent.putExtra("peer", peerUri) } else Log.w(TAG, "onClickListener did not find UA for $aor") val peerName = Utils.friendlyUri(ctx, peerUri, account) message.value = String.format(ctx.getString(R.string.contact_action_question), peerName) secondButtonText.value = ctx.getString(R.string.call) secondAction.value = { if (ua != null) { handleIntent(ctx, viewModel, intent, "call") navController.navigate("main") { popUpTo("main") launchSingleTop = true } } } lastButtonText.value = ctx.getString(R.string.send_message) lastAction.value = { if (ua != null) { handleIntent(ctx, viewModel, intent, "message") navController.navigateUp() } } showDialog.value = true }, onLongClick = { val peerName = Utils.friendlyUri(ctx, peerUri, account) val callText: String = if (callRow.details.size > 1) ctx.getString(R.string.calls_calls) else ctx.getString(R.string.calls_call) val contactExists = Contact.nameExists(peerName, BaresipService.contacts, false) if (contactExists) { message.value = String.format( ctx.getString(R.string.calls_delete_question), peerName, callText ) secondButtonText.value = "" lastButtonText.value = ctx.getString(R.string.delete) lastAction.value = { removeFromHistory(callHistory, callRow) } } else { message.value = String.format( ctx.getString(R.string.calls_add_delete_question), peerName, callText ) secondButtonText.value = ctx.getString(R.string.add_contact) secondAction.value = { navController.navigate("baresip_contact/$peerUri/new") } lastButtonText.value = ctx.getString(R.string.delete) lastAction.value = { removeFromHistory(callHistory, callRow) } } showDialog.value = true } ) ) { when (val contact = Contact.findContact(peerUri)) { is Contact.BaresipContact -> { val avatarImage = contact.avatarImage if (avatarImage != null) CustomElements.ImageAvatar(avatarImage) else CustomElements.TextAvatar(contact.name, contact.color) } is Contact.AndroidContact -> { val thumbNailUri = contact.thumbnailUri if (thumbNailUri != null) AsyncImage( model = thumbNailUri, contentDescription = "Avatar", contentScale = ContentScale.Crop, modifier = Modifier .size(36.dp) .clip(CircleShape), ) else CustomElements.TextAvatar(contact.name, contact.color) } null -> { Icon( imageVector = Icons.Filled.AccountCircle, contentDescription = "Avatar", modifier = Modifier.size(36.dp).scale(1.2f), tint = MaterialTheme.colorScheme.secondary ) } } Spacer(modifier = Modifier.width(4.dp)) var count = 1 for (d in callRow.details) { if (d.recording.isNotEmpty() && d.recording[0] != "") recordings = true if (count > 3) continue Icon( imageVector = if (callUp(d.direction)) Icons.AutoMirrored.Filled.CallMade else Icons.AutoMirrored.Filled.CallReceived, modifier = Modifier.size(20.dp), tint = colorResource(id = callTint(d.direction)), contentDescription = "Direction" ) count++ } if (count > 3) Text("...", color = MaterialTheme.colorScheme.onBackground) Text(text = Utils.friendlyUri(ctx, peerUri, account), modifier = Modifier.padding(start = 8.dp), fontSize = 18.sp, maxLines = 1, overflow = TextOverflow.Ellipsis ) } } Text( text = Utils.relativeTime(ctx, callRow.stopTime), fontSize = 12.sp, minLines = 2, maxLines = 2, lineHeight = 16.sp, textAlign = TextAlign.End, color = if (recordings) MaterialTheme.colorScheme.error else MaterialTheme.colorScheme.onBackground, modifier = Modifier .padding(end = 16.dp) .clickable(onClick = { viewModel.selectCallRow(callRow) navController.navigate("call_details") }) ) } } } } private fun loadCallHistory(aor: String): MutableList { val res = mutableListOf() for (i in BaresipService.callHistory.indices.reversed()) { val h = BaresipService.callHistory[i] if (h.aor == aor) { val direction: Int = if (h.direction == "in") { if (h.startTime != null) { if (h.startTime != h.stopTime) CALL_DOWN_GREEN else CALL_DOWN_BLUE } else if (h.rejected) CALL_DOWN_RED else CALL_MISSED_IN } else { if (h.startTime != null) CALL_UP_GREEN else if (h.rejected) CALL_UP_RED else CALL_MISSED_OUT } if (res.isNotEmpty() && res.last().peerUri == h.peerUri) res.last().details.add(CallRow.Details( direction, h.startTime, h.stopTime, h.recording.toList() )) else res.add(CallRow(h.aor, h.peerUri, direction, h.startTime, h.stopTime, h.recording.toList())) } } return res } private fun removeFromHistory(callHistory: MutableState>, callRow: CallRow) { for (details in callRow.details) { CallHistoryNew.deleteRecordingFiles(details.recording.toTypedArray()) BaresipService.callHistory.removeAll { it.startTime == details.startTime && it.stopTime == details.stopTime } } CallHistoryNew.deleteRecordingFiles(callRow.recording.toTypedArray()) val updatedList = callHistory.value.filterNot { it == callRow } callHistory.value = updatedList CallHistoryNew.save() } fun callUp(direction: Int): Boolean { return when (direction) { CALL_UP_GREEN, CALL_UP_RED, CALL_MISSED_OUT -> true else -> false } } fun callTint(direction: Int): Int { return when (direction) { CALL_UP_GREEN, CALL_DOWN_GREEN -> R.color.colorTrafficGreen CALL_UP_RED, CALL_DOWN_RED -> R.color.colorTrafficRed CALL_DOWN_BLUE -> R.color.colorPrimary else -> R.color.colorTrafficYellow } } ================================================ FILE: app/src/main/kotlin/com/tutpro/baresip/ChatScreen.kt ================================================ package com.tutpro.baresip import android.content.Context import android.content.Intent import android.text.format.DateUtils.isToday import android.widget.Toast import androidx.activity.compose.BackHandler import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.foundation.background import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.WindowInsets import androidx.compose.foundation.layout.asPaddingValues import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.imePadding import androidx.compose.foundation.layout.navigationBarsPadding import androidx.compose.foundation.layout.offset import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.statusBars import androidx.compose.foundation.layout.wrapContentHeight import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.items import androidx.compose.foundation.lazy.rememberLazyListState import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.text.KeyboardOptions import androidx.compose.foundation.text.selection.SelectionContainer import androidx.compose.foundation.verticalScroll import androidx.compose.material.icons.Icons import androidx.compose.material.icons.automirrored.filled.ArrowBack import androidx.compose.material.icons.automirrored.filled.Send import androidx.compose.material.icons.outlined.Call import androidx.compose.material.icons.outlined.Clear import androidx.compose.material3.Button import androidx.compose.material3.ButtonDefaults import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.Icon import androidx.compose.material3.IconButton import androidx.compose.material3.MaterialTheme import androidx.compose.material3.OutlinedTextField import androidx.compose.material3.Scaffold import androidx.compose.material3.SmallFloatingActionButton import androidx.compose.material3.Text import androidx.compose.material3.TopAppBar import androidx.compose.material3.TopAppBarDefaults import androidx.compose.runtime.Composable import androidx.compose.runtime.DisposableEffect import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.focus.FocusRequester import androidx.compose.ui.focus.focusRequester import androidx.compose.ui.layout.onGloballyPositioned import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalSoftwareKeyboardController import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.TextStyle import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.input.KeyboardCapitalization import androidx.compose.ui.text.input.KeyboardType import androidx.compose.ui.text.input.TextFieldValue import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import androidx.lifecycle.Lifecycle import androidx.lifecycle.LifecycleEventObserver import androidx.lifecycle.Observer import androidx.lifecycle.compose.LocalLifecycleOwner import androidx.navigation.NavController import androidx.navigation.NavGraphBuilder import androidx.navigation.NavType import androidx.navigation.compose.composable import androidx.navigation.navArgument import com.tutpro.baresip.CustomElements.AlertDialog import com.tutpro.baresip.CustomElements.verticalScrollbar import kotlinx.coroutines.launch import java.lang.String.format import java.text.DateFormat import java.util.GregorianCalendar fun NavGraphBuilder.chatScreenRoute(navController: NavController, viewModel: ViewModel) { composable( route = "chat/{aor}/{peer}", arguments = listOf( navArgument("aor") { type = NavType.StringType }, navArgument("peer") { type = NavType.StringType } ) ) { backStackEntry -> val aor = backStackEntry.arguments?.getString("aor")!! val peerUri = backStackEntry.arguments?.getString("peer")!! ChatScreen( ctx = LocalContext.current, navController = navController, viewModel = viewModel, account = Account.ofAor(aor)!!, peerUri = peerUri ) } } @Composable private fun ChatScreen( ctx: Context, navController: NavController, viewModel: ViewModel, account: Account, peerUri: String ) { val lifecycleOwner = LocalLifecycleOwner.current val aor = account.aor var chatMessages by remember(aor, peerUri) { mutableStateOf>(emptyList()) } DisposableEffect(lifecycleOwner) { val observer = LifecycleEventObserver { _, event -> if (event == Lifecycle.Event.ON_RESUME) { Log.d(TAG, "Resumed to ChatScreen for AOR: $aor peer $peerUri") chatMessages = loadPeerMessages(aor, peerUri) } } lifecycleOwner.lifecycle.addObserver(observer) onDispose { lifecycleOwner.lifecycle.removeObserver(observer) } } var areMessagesLoaded by remember(aor, peerUri) { mutableStateOf(false) } val reloadMessages = { Log.d(TAG, "Reloading messages for $aor peer $peerUri") chatMessages = loadPeerMessages(aor, peerUri) if (!areMessagesLoaded) areMessagesLoaded = true } val addMessage = { newMessage: Message -> chatMessages = chatMessages + newMessage } DisposableEffect(key1 = lifecycleOwner, key2 = account.aor, key3 = peerUri) { val messagesObserver = Observer { timestamp -> Log.d(TAG, "Message update received via LiveData for $peerUri, timestamp: $timestamp") reloadMessages() } reloadMessages() // Initial load Log.d(TAG, "Observing message updates for $peerUri") BaresipService.messageUpdate.observe(lifecycleOwner, messagesObserver) onDispose { Log.d(TAG, "Removing message observer for $peerUri") BaresipService.messageUpdate.removeObserver(messagesObserver) } } BackHandler(enabled = true) { backAction(navController, account, peerUri) } Scaffold( modifier = Modifier .fillMaxSize() .imePadding(), containerColor = MaterialTheme.colorScheme.background, topBar = { Column(modifier = Modifier .fillMaxWidth() .background(MaterialTheme.colorScheme.background) .padding(top = WindowInsets.statusBars.asPaddingValues().calculateTopPadding()) ) { TopAppBar(ctx, navController, viewModel, account, peerUri) } }, bottomBar = { NewMessage( ctx = ctx, viewModel, account = account, peerUri = peerUri, addMessage = addMessage ) }, content = { contentPadding -> if (areMessagesLoaded) ChatContent(ctx, navController, contentPadding, account, peerUri, chatMessages, reloadMessages ) } ) } @OptIn(ExperimentalMaterial3Api::class) @Composable private fun TopAppBar( ctx: Context, navController: NavController, viewModel: ViewModel, account: Account, peerUri: String ) { val aor = account.aor TopAppBar( title = { Text( text = format(ctx.getString(R.string.chat_with), Utils.friendlyUri(ctx, peerUri, account)), fontSize = 22.sp, fontWeight = FontWeight.Bold ) }, colors = TopAppBarDefaults.topAppBarColors( containerColor = MaterialTheme.colorScheme.primary, navigationIconContentColor = MaterialTheme.colorScheme.onPrimary, titleContentColor = MaterialTheme.colorScheme.onPrimary, actionIconContentColor = MaterialTheme.colorScheme.onPrimary ), navigationIcon = { IconButton(onClick = { backAction(navController, account, peerUri) }) { Icon( imageVector = Icons.AutoMirrored.Filled.ArrowBack, contentDescription = "Back", ) } }, windowInsets = WindowInsets(0, 0, 0, 0), actions = { IconButton( onClick = { val ua = UserAgent.ofAor(account.aor) if (ua != null) { val intent = Intent(ctx, MainActivity::class.java) intent.putExtra("uap", ua.uap) intent.putExtra("peer", peerUri) handleIntent(ctx, viewModel, intent, "call") navController.navigate("main") { popUpTo("main") launchSingleTop = true } } else Log.w(TAG, "Call button onClick listener did not find UA for $aor") } ) { Icon( imageVector = Icons.Outlined.Call, contentDescription = "Call", ) } } ) } @Composable private fun ChatContent( ctx: Context, navController: NavController, contentPadding: PaddingValues, account: Account, peerUri: String, messages: List, onMessageDeleted: () -> Unit ) { Column( modifier = Modifier .fillMaxWidth() .padding(contentPadding), verticalArrangement = Arrangement.Bottom ) { Account(ctx, account) Spacer(modifier = Modifier.weight(1f)) Messages(ctx, navController, account, peerUri, messages, onMessageDeleted) } } @Composable private fun Account(ctx: Context, account: Account) { Text( text = ctx.getString(R.string.account) + " " + account.text(), modifier = Modifier .fillMaxWidth() .padding(top = 8.dp, bottom = 8.dp), fontSize = 18.sp, fontWeight = FontWeight.SemiBold, textAlign = TextAlign.Center ) } @Composable private fun Messages( ctx: Context, navController: NavController, account: Account, peerUri: String, messages: List, onMessageDeleted: () -> Unit ) { val peerName = Utils.friendlyUri(ctx, peerUri, account) val showDialog = remember { mutableStateOf(false) } val dialogMessage = remember { mutableStateOf("") } val secondButtonText = remember { mutableStateOf("") } val secondAction = remember { mutableStateOf({}) } val lastButtonText = remember { mutableStateOf("") } val lastAction = remember { mutableStateOf({}) } AlertDialog( showDialog = showDialog, title = stringResource(R.string.confirmation), message = dialogMessage.value, firstButtonText = stringResource(R.string.cancel), secondButtonText = secondButtonText.value, onSecondClicked = secondAction.value, lastButtonText = lastButtonText.value, onLastClicked = lastAction.value, ) val lazyListState = rememberLazyListState() val coroutineScope = rememberCoroutineScope() LaunchedEffect(messages) { // Scroll to the bottom when new messages are added if (messages.isNotEmpty()) { coroutineScope.launch { lazyListState.scrollToItem(0) } } } LazyColumn( modifier = Modifier .fillMaxWidth() .padding(start = 16.dp, end = 2.dp) .verticalScrollbar(state = lazyListState) .background(MaterialTheme.colorScheme.background), reverseLayout = true, state = lazyListState, verticalArrangement = Arrangement.spacedBy(16.dp), ) { items(items = messages, key = { message -> message.timeStamp }) { message -> val down = message.direction == MESSAGE_DOWN val sender: String = if (down) peerName else if (BaresipService.uas.value.size == 1) stringResource(R.string.you) else account.text() var info: String val cal = GregorianCalendar() cal.timeInMillis = message.timeStamp val fmt: DateFormat = if (isToday(message.timeStamp)) DateFormat.getTimeInstance(DateFormat.SHORT) else DateFormat.getDateInstance(DateFormat.SHORT) info = fmt.format(cal.time) if (info.length < 6) info = "${stringResource(R.string.today)} $info" if (message.direction == MESSAGE_UP_FAIL) { info = if (message.responseCode != 0) "$info - ${stringResource(R.string.message_failed)}: " + "${message.responseCode} ${message.responseReason}" else "$info - ${stringResource(R.string.sending_failed)}" } Row( verticalAlignment = Alignment.CenterVertically, modifier = Modifier.padding(end = 12.dp) ) { Button( onClick = { if (Contact.findContact(peerUri) == null) { dialogMessage.value = String.format( ctx.getString(R.string.long_message_question), peerUri ) secondButtonText.value = ctx.getString(R.string.add_contact) secondAction.value = { navController.navigate("baresip_contact/$peerUri/new") } lastButtonText.value = ctx.getString(R.string.delete) lastAction.value = { message.delete() onMessageDeleted() } } else { dialogMessage.value = ctx.getString(R.string.short_message_question) secondButtonText.value = "" lastButtonText.value = ctx.getString(R.string.delete) lastAction.value = { message.delete() onMessageDeleted() } } showDialog.value = true }, shape = if (message.direction == MESSAGE_DOWN) RoundedCornerShape(50.dp, 20.dp, 20.dp, 10.dp) else RoundedCornerShape(20.dp, 10.dp, 50.dp, 20.dp), colors = ButtonDefaults.buttonColors( containerColor = if (message.direction == MESSAGE_DOWN) MaterialTheme.colorScheme.secondaryContainer else MaterialTheme.colorScheme.primaryContainer ), modifier = Modifier .fillMaxWidth() .wrapContentHeight() .padding( start = if (message.direction == MESSAGE_DOWN) 0.dp else 24.dp, end = if (message.direction == MESSAGE_DOWN) 24.dp else 0.dp ) ) { Column { val textColor = if (message.direction == MESSAGE_DOWN) MaterialTheme.colorScheme.onSecondaryContainer else MaterialTheme.colorScheme.onPrimaryContainer Row { Text(text = sender, fontSize = 12.sp, color = textColor) Spacer(modifier = Modifier.weight(1f)) Text(text = info, fontSize = 12.sp, color = textColor) } Row { SelectionContainer { Text( text = message.message, color = textColor, fontWeight = if (message.direction == MESSAGE_DOWN && message.new) FontWeight.Bold else FontWeight.Normal ) } } } } } } } } @OptIn(ExperimentalFoundationApi::class) @Composable private fun NewMessage( ctx: Context, viewModel: ViewModel, account: Account, peerUri: String, addMessage: (message: Message) -> Unit ) { val aor = account.aor val ua = UserAgent.ofAor(aor)!! val newMessage = rememberSaveable(stateSaver = TextFieldValue.Saver) { mutableStateOf(TextFieldValue(viewModel.getAorPeerMessage(aor, peerUri))) } var textFieldLoaded by remember { mutableStateOf(false) } val focusRequester = remember { FocusRequester() } val showDialog = remember { mutableStateOf(false) } val dialogMessage = remember { mutableStateOf("") } AlertDialog( showDialog = showDialog, title = stringResource(R.string.notice), message = dialogMessage.value, lastButtonText = stringResource(R.string.ok), ) Row(modifier = Modifier .fillMaxWidth() .navigationBarsPadding() .padding(start = 16.dp, end = 8.dp, top = 10.dp, bottom = 16.dp), verticalAlignment = Alignment.CenterVertically ) { val keyboardController = LocalSoftwareKeyboardController.current OutlinedTextField( value = newMessage.value, placeholder = { Text(stringResource(R.string.new_message)) }, onValueChange = { newMessage.value = it viewModel.updateAorPeerMessage(aor, peerUri, it.text) }, modifier = Modifier .weight(1f) .padding(end = 8.dp) .verticalScroll(rememberScrollState()) .focusRequester(focusRequester) .onGloballyPositioned { if (!textFieldLoaded) textFieldLoaded = true }, singleLine = false, trailingIcon = { if (newMessage.value.text.isNotEmpty()) { Icon( Icons.Outlined.Clear, contentDescription = "Clear", modifier = Modifier.clickable { newMessage.value = TextFieldValue("") viewModel.updateAorPeerMessage(aor, peerUri, "") }, tint = MaterialTheme.colorScheme.onSurfaceVariant ) } }, label = { Text(stringResource(R.string.new_message)) }, textStyle = TextStyle(fontSize = 18.sp), keyboardOptions = KeyboardOptions( capitalization = KeyboardCapitalization.Sentences, keyboardType = KeyboardType.Text, autoCorrectEnabled = true ) ) LaunchedEffect(Unit) { if (newMessage.value.text.isNotEmpty()) focusRequester.requestFocus() } SmallFloatingActionButton( modifier = Modifier.offset(y = 2.dp), onClick = { val msgText = newMessage.value.text if (msgText.isNotEmpty()) { keyboardController?.hide() val time = System.currentTimeMillis() val msg = Message( aor, peerUri, msgText, time, MESSAGE_UP_WAIT, 0, "", true ) msg.add() var msgUri = "" addMessage(msg) if (Utils.isTelUri(peerUri)) if (ua.account.telProvider == "") { dialogMessage.value = String.format( ctx.getString(R.string.no_telephony_provider), Utils.plainAor(aor) ) showDialog.value = true } else { msgUri = Utils.telToSip(peerUri, ua.account) } else msgUri = peerUri if (msgUri != "") if (Api.message_send(ua.uap, msgUri, msgText, time.toString() ) != 0) { Toast.makeText( ctx, "${ctx.getString(R.string.message_failed)}!", Toast.LENGTH_SHORT ).show() msg.direction = MESSAGE_UP_FAIL msg.responseReason = ctx.getString(R.string.message_failed) } else { newMessage.value = TextFieldValue("") viewModel.updateAorPeerMessage(aor, peerUri, "") keyboardController?.hide() } } }, containerColor = MaterialTheme.colorScheme.secondary, contentColor = MaterialTheme.colorScheme.onSecondary ) { Icon( imageVector = Icons.AutoMirrored.Filled.Send, modifier = Modifier.size(28.dp), contentDescription = stringResource(R.string.add) ) } } } private fun backAction(navController: NavController, account: Account, peerUri: String) { val aor = account.aor Message.updateMessagesFromPearRead(aor, peerUri) account.unreadMessages = Message.unreadMessages(aor) navController.navigateUp() } private fun loadPeerMessages(aor: String, peerUri: String): List { val res = mutableListOf() for (m in BaresipService.messages.reversed()) if ((m.aor == aor) && (m.peerUri == peerUri)) res.add(m) return res } ================================================ FILE: app/src/main/kotlin/com/tutpro/baresip/ChatsScreen.kt ================================================ package com.tutpro.baresip import android.content.Context import android.text.format.DateUtils.isToday import androidx.compose.animation.animateContentSize import androidx.compose.foundation.BorderStroke import androidx.compose.foundation.background import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.WindowInsets import androidx.compose.foundation.layout.asPaddingValues import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.heightIn import androidx.compose.foundation.layout.imePadding import androidx.compose.foundation.layout.navigationBarsPadding import androidx.compose.foundation.layout.offset import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.statusBars import androidx.compose.foundation.layout.width import androidx.compose.foundation.layout.wrapContentHeight import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.items import androidx.compose.foundation.lazy.rememberLazyListState import androidx.compose.foundation.shape.CircleShape import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.text.BasicText import androidx.compose.foundation.text.KeyboardOptions import androidx.compose.material.icons.Icons import androidx.compose.material.icons.automirrored.filled.ArrowBack import androidx.compose.material.icons.filled.AccountCircle import androidx.compose.material.icons.filled.Add import androidx.compose.material.icons.filled.Menu import androidx.compose.material.icons.outlined.Clear import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.Icon import androidx.compose.material3.IconButton import androidx.compose.material3.MaterialTheme import androidx.compose.material3.OutlinedTextField import androidx.compose.material3.Scaffold import androidx.compose.material3.SmallFloatingActionButton import androidx.compose.material3.Text import androidx.compose.material3.TopAppBar import androidx.compose.material3.TopAppBarDefaults import androidx.compose.runtime.Composable import androidx.compose.runtime.DisposableEffect import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.MutableState import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableIntStateOf import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip import androidx.compose.ui.draw.scale import androidx.compose.ui.draw.shadow import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalFocusManager import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.AnnotatedString import androidx.compose.ui.text.TextStyle import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.input.ImeAction import androidx.compose.ui.text.input.KeyboardType import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import androidx.lifecycle.Lifecycle import androidx.lifecycle.LifecycleEventObserver import androidx.lifecycle.compose.LocalLifecycleOwner import androidx.navigation.NavController import androidx.navigation.NavGraphBuilder import androidx.navigation.NavType import androidx.navigation.compose.composable import androidx.navigation.navArgument import coil.compose.AsyncImage import com.tutpro.baresip.BaresipService.Companion.contactNames import com.tutpro.baresip.CustomElements.AlertDialog import com.tutpro.baresip.CustomElements.DropdownMenu import com.tutpro.baresip.CustomElements.SelectableAlertDialog import com.tutpro.baresip.CustomElements.verticalScrollbar import java.text.DateFormat import java.util.GregorianCalendar fun NavGraphBuilder.chatsScreenRoute(navController: NavController) { composable( route = "chats/{aor}", arguments = listOf(navArgument("aor") { type = NavType.StringType }) ) { backStackEntry -> val aor = backStackEntry.arguments?.getString("aor")!! ChatsScreen(navController, aor) } } @OptIn(ExperimentalMaterial3Api::class) @Composable private fun ChatsScreen(navController: NavController, aor: String) { val ctx = LocalContext.current val account = Account.ofAor(aor)!! val uaMessages: MutableState> = remember { mutableStateOf(emptyList()) } var areMessagesLoaded by remember { mutableStateOf(false) } var refreshTrigger by remember { mutableIntStateOf(0) } val lifecycleOwner = LocalLifecycleOwner.current LaunchedEffect(aor, refreshTrigger) { uaMessages.value = loadMessages(account) areMessagesLoaded = true } DisposableEffect(lifecycleOwner) { val observer = LifecycleEventObserver { _, event -> if (event == Lifecycle.Event.ON_RESUME) refreshTrigger++ } lifecycleOwner.lifecycle.addObserver(observer) onDispose { lifecycleOwner.lifecycle.removeObserver(observer) } } Scaffold( modifier = Modifier .fillMaxSize() .imePadding(), containerColor = MaterialTheme.colorScheme.background, topBar = { Column( modifier = Modifier .fillMaxWidth() .background(MaterialTheme.colorScheme.background) .padding(top = WindowInsets.statusBars.asPaddingValues().calculateTopPadding()) ) { TopAppBar(navController, account, uaMessages) } }, bottomBar = { NewChatPeer(ctx, navController, account) }, content = { contentPadding -> if (areMessagesLoaded) ChatsContent(LocalContext.current, navController, contentPadding, account, uaMessages) }, ) } @OptIn(ExperimentalMaterial3Api::class) @Composable private fun TopAppBar( navController: NavController, account: Account, uaMessages: MutableState> ) { var menuExpanded by remember { mutableStateOf(false) } val delete = stringResource(R.string.delete) val blocked = stringResource(R.string.blocked) val showDialog = remember { mutableStateOf(false) } val lastAction = remember { mutableStateOf({}) } AlertDialog( showDialog = showDialog, title = stringResource(R.string.confirmation), message = String.format(stringResource(R.string.delete_chats_alert), account.text()), firstButtonText = stringResource(R.string.cancel), lastButtonText = stringResource(R.string.delete), onLastClicked = lastAction.value, ) TopAppBar( title = { Text(text = stringResource(R.string.chats), fontWeight = FontWeight.Bold) }, colors = TopAppBarDefaults.topAppBarColors( containerColor = MaterialTheme.colorScheme.primary, navigationIconContentColor = MaterialTheme.colorScheme.onPrimary, titleContentColor = MaterialTheme.colorScheme.onPrimary, actionIconContentColor = MaterialTheme.colorScheme.onPrimary ), navigationIcon = { IconButton(onClick = { navController.navigateUp() }) { Icon( imageVector = Icons.AutoMirrored.Filled.ArrowBack, contentDescription = null, ) } }, windowInsets = WindowInsets(0, 0, 0, 0), actions = { IconButton( onClick = { menuExpanded = !menuExpanded } ) { Icon( imageVector = Icons.Filled.Menu, contentDescription = "Menu", ) } DropdownMenu ( expanded = menuExpanded, onDismissRequest = { menuExpanded = false }, items = listOf(delete, blocked), onItemClick = { selectedItem -> menuExpanded = false when (selectedItem) { delete -> { lastAction.value = { deleteMessages(uaMessages, account, "") account.unreadMessages = false } showDialog.value = true } blocked -> { navController.navigate("blocked/message/${account.aor}") } } } ) }, ) } @Composable private fun ChatsContent( ctx: Context, navController: NavController, contentPadding: PaddingValues, account: Account, uaMessages: MutableState> ) { Column( modifier = Modifier .fillMaxWidth() .padding(contentPadding), verticalArrangement = Arrangement.Top ) { Account(account) Chats(ctx, navController, account, uaMessages) } } @Composable private fun Account(account: Account) { Text( text = stringResource(R.string.account) + " " + account.text(), modifier = Modifier.fillMaxWidth().padding(top = 8.dp, bottom = 8.dp), fontSize = 18.sp, fontWeight = FontWeight.SemiBold, textAlign = TextAlign.Center ) } @Composable private fun Chats( ctx: Context, navController: NavController, account: Account, uaMessages: MutableState> ) { val aor = account.aor val showDialog = remember { mutableStateOf(false) } val dialogMessage = remember { mutableStateOf("") } val secondButtonText = remember { mutableStateOf("") } val secondAction = remember { mutableStateOf({}) } val lastButtonText = remember { mutableStateOf("") } val lastAction = remember { mutableStateOf({}) } if (showDialog.value) AlertDialog( showDialog = showDialog, title = stringResource(R.string.confirmation), message = dialogMessage.value, firstButtonText = stringResource(R.string.cancel), secondButtonText = secondButtonText.value, onSecondClicked = secondAction.value, lastButtonText = lastButtonText.value, onLastClicked = lastAction.value, ) val lazyListState = rememberLazyListState() LazyColumn( modifier = Modifier .fillMaxWidth() .padding(start = 8.dp, end = 4.dp) .verticalScrollbar(state = lazyListState) .background(MaterialTheme.colorScheme.background), reverseLayout = true, state = lazyListState, verticalArrangement = Arrangement.spacedBy(16.dp), ) { items(items = uaMessages.value, key = { message -> message.timeStamp }) { message -> Row(verticalAlignment = Alignment.CenterVertically) { when (val contact = Contact.findContact(message.peerUri)) { is Contact.BaresipContact -> { val avatarImage = contact.avatarImage if (avatarImage != null) CustomElements.ImageAvatar(avatarImage) else CustomElements.TextAvatar(contact.name, contact.color) } is Contact.AndroidContact -> { val thumbNailUri = contact.thumbnailUri if (thumbNailUri != null) AsyncImage( model = thumbNailUri, contentDescription = "Avatar", contentScale = ContentScale.Crop, modifier = Modifier.size(36.dp).clip(CircleShape), ) else CustomElements.TextAvatar(contact.name, contact.color) } null -> { Icon( imageVector = Icons.Filled.AccountCircle, contentDescription = "Avatar", modifier = Modifier.size(36.dp).scale(1.2f), tint = MaterialTheme.colorScheme.secondary ) } } Spacer(modifier = Modifier.width(6.dp)) val buttonShape = if (message.direction == MESSAGE_DOWN) { RoundedCornerShape(50.dp, 20.dp, 20.dp, 10.dp) } else { RoundedCornerShape(20.dp, 10.dp, 50.dp, 20.dp) } val borderStroke = if (account.unreadMessages && Message.unreadMessagesFromPeer(aor, message.peerUri)) { BorderStroke(width = 2.dp, color = MaterialTheme.colorScheme.error) } else { null } CustomElements.Button( onClick = { navController.navigate("chat/${aor}/${message.peerUri}") }, onLongClick = { val peer = Utils.friendlyUri(ctx, message.peerUri, account) val contactExists = Contact.nameExists(peer, BaresipService.contacts, false) if (contactExists) { dialogMessage.value = String.format( ctx.getString(R.string.short_chat_question), peer ) secondButtonText.value = "" lastButtonText.value = ctx.getString(R.string.delete) lastAction.value = { deleteMessages(uaMessages, account, message.peerUri) } } else { dialogMessage.value = String.format(ctx.getString(R.string.long_chat_question), peer) secondButtonText.value = ctx.getString(R.string.delete) secondAction.value = { deleteMessages(uaMessages, account, message.peerUri) } lastButtonText.value = ctx.getString(R.string.add_contact) lastAction.value = { navController.navigate("baresip_contact/${message.peerUri}/new") } } showDialog.value = true }, modifier = Modifier.fillMaxWidth().wrapContentHeight().padding(end = 6.dp), shape = buttonShape, border = borderStroke, color = if (message.direction == MESSAGE_DOWN) MaterialTheme.colorScheme.secondaryContainer else MaterialTheme.colorScheme.primaryContainer ) { val peer = Utils.friendlyUri(ctx, message.peerUri, account) val cal = GregorianCalendar() cal.timeInMillis = message.timeStamp val fmt: DateFormat = if (isToday(message.timeStamp)) DateFormat.getTimeInstance(DateFormat.SHORT) else DateFormat.getDateInstance(DateFormat.SHORT) val info = fmt.format(cal.time) Column { val textColor = if (message.direction == MESSAGE_DOWN) MaterialTheme.colorScheme.onSecondaryContainer else MaterialTheme.colorScheme.onPrimaryContainer Row { Text(text = peer, color = textColor, fontSize = 12.sp) Spacer(modifier = Modifier.weight(1f)) Text(text = info, color = textColor, fontSize = 12.sp) } Row { BasicText( text = message.message, maxLines = 1, overflow = TextOverflow.Ellipsis, style = TextStyle( color = textColor, fontWeight = if (message.direction == MESSAGE_DOWN && message.new) FontWeight.Bold else FontWeight.Normal, fontSize = 16.sp ) ) } } } } } } } @Composable private fun NewChatPeer(ctx: Context, navController: NavController, account: Account) { val alertTitle = remember { mutableStateOf("") } val alertMessage = remember { mutableStateOf("") } val showAlert = remember { mutableStateOf(false) } fun makeChat(ctx: Context, navController: NavController, account: Account, chatPeer: String) { val peerUri = if (Utils.isTelNumber(chatPeer)) "tel:$chatPeer" else chatPeer val uri = if (Utils.isTelUri(peerUri)) { if (account.telProvider == "") { alertTitle.value = ctx.getString(R.string.notice) alertMessage.value = String.format(ctx.getString(R.string.no_telephony_provider), account.aor) showAlert.value = true "" } else Utils.telToSip(peerUri, account) } else Utils.uriComplete(peerUri, account.aor) if (alertMessage.value.isEmpty()) { if (!Utils.checkUri(uri)) { alertTitle.value = ctx.getString(R.string.notice) alertMessage.value = String.format(ctx.getString(R.string.invalid_sip_or_tel_uri), uri) showAlert.value = true } else navController.navigate("chat/${account.aor}/${uri}") } } if (showAlert.value) { AlertDialog( showDialog = showAlert, title = alertTitle.value, message = alertMessage.value, lastButtonText = stringResource(R.string.ok), ) } val showDialog = remember { mutableStateOf(false) } val items = remember { mutableStateOf(listOf()) } val itemAction = remember { mutableStateOf<(Int) -> Unit>({ _ -> run {} }) } SelectableAlertDialog( openDialog = showDialog, title = stringResource(R.string.choose_destination_uri), items = items.value, onItemClicked = itemAction.value, neutralButtonText = stringResource(R.string.cancel), onNeutralClicked = {} ) val suggestions by remember { contactNames } var filteredSuggestions by remember { mutableStateOf>(emptyList()) } var showSuggestions by remember { mutableStateOf(false) } val lazyListState = rememberLazyListState() val focusManager = LocalFocusManager.current Row( modifier = Modifier .fillMaxWidth() .navigationBarsPadding() .padding(start = 16.dp, end = 8.dp, top = 10.dp, bottom = 16.dp), verticalAlignment = Alignment.CenterVertically, ) { var newPeer by remember { mutableStateOf("") } Column( horizontalAlignment = Alignment.Start, modifier = Modifier.weight(1f) ) { if (showSuggestions && filteredSuggestions.isNotEmpty()) { Column( modifier = Modifier .shadow(8.dp, RoundedCornerShape(8.dp)) .background( color = MaterialTheme.colorScheme.surfaceVariant, shape = RoundedCornerShape(8.dp) ) .animateContentSize() ) { Box(modifier = Modifier.fillMaxWidth().heightIn(max = 150.dp) ) { LazyColumn( modifier = Modifier .fillMaxWidth() .heightIn(max = 180.dp) .verticalScrollbar(state = lazyListState, width = 6.dp), horizontalAlignment = Alignment.Start, state = lazyListState ) { items( items = filteredSuggestions, key = { suggestion -> suggestion.toString() } ) { suggestion -> Box( modifier = Modifier .fillMaxWidth() .clickable { newPeer = suggestion.toString() showSuggestions = false } .padding(12.dp) ) { Text( text = suggestion, modifier = Modifier.fillMaxWidth(), color = MaterialTheme.colorScheme.onSurfaceVariant, fontSize = 18.sp ) } } } } } Spacer(modifier = Modifier.height(8.dp)) } OutlinedTextField( value = newPeer, placeholder = { Text(stringResource(R.string.new_chat_peer)) }, onValueChange = { newPeer = it showSuggestions = newPeer.length > 1 filteredSuggestions = if (it.isEmpty()) { emptyList() } else { val normalizedInput = Utils.unaccent(it) suggestions .filter { suggestion -> it.length > 1 && Utils.unaccent(suggestion).contains(normalizedInput, ignoreCase = true) } .map { suggestion -> Utils.buildAnnotatedStringWithHighlight(suggestion, it) } } }, modifier = Modifier.padding(end = 6.dp).fillMaxWidth(), singleLine = true, trailingIcon = { if (newPeer.isNotEmpty()) Icon( Icons.Outlined.Clear, contentDescription = null, modifier = Modifier.clickable { if (showSuggestions) showSuggestions = false newPeer = "" }, tint = MaterialTheme.colorScheme.onSurfaceVariant ) }, label = { Text(stringResource(R.string.new_chat_peer)) }, textStyle = TextStyle(fontSize = 18.sp), keyboardOptions = KeyboardOptions( keyboardType = KeyboardType.Text, imeAction = ImeAction.Done ) ) } Spacer(Modifier.width(4.dp)) SmallFloatingActionButton( modifier = Modifier.padding(end = 4.dp).offset(y = 2.dp), onClick = { showSuggestions = false val peerText = newPeer.trim() if (peerText.isNotEmpty()) { val uris = Contact.contactUris(peerText) if (uris.isEmpty()) makeChat(ctx, navController, account, peerText) else if (uris.size == 1) makeChat(ctx, navController, account, uris[0]) else { items.value = uris itemAction.value = { index -> makeChat(ctx, navController, account, uris[index]) } showDialog.value = true } } newPeer = "" focusManager.clearFocus() }, containerColor = MaterialTheme.colorScheme.secondary, contentColor = MaterialTheme.colorScheme.onSecondary ) { Icon( imageVector = Icons.Filled.Add, modifier = Modifier.size(36.dp), contentDescription = stringResource(R.string.add) ) } } } private fun loadMessages(account: Account) : List { val res = mutableListOf() account.unreadMessages = false for (m in BaresipService.messages.reversed()) { if (m.aor != account.aor) continue var found = false for (r in res) if (r.peerUri == m.peerUri) { found = true break } if (!found) { res.add(0, m) if (m.new) account.unreadMessages = true } } return res.toList() } private fun deleteMessages(uaMessages: MutableState>, account: Account, peerUri: String) { val updatedMessages = BaresipService.messages.toMutableList() updatedMessages.removeAll { it.aor == account.aor && (peerUri == "" || it.peerUri == peerUri) } BaresipService.messages = updatedMessages.toList() Message.save() uaMessages.value = loadMessages(account) } ================================================ FILE: app/src/main/kotlin/com/tutpro/baresip/Codec.kt ================================================ package com.tutpro.baresip import androidx.compose.runtime.MutableState data class Codec(val name: String, var enabled: MutableState) ================================================ FILE: app/src/main/kotlin/com/tutpro/baresip/CodecsScreen.kt ================================================ package com.tutpro.baresip import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.foundation.background import androidx.compose.foundation.combinedClickable import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.WindowInsets import androidx.compose.foundation.layout.asPaddingValues import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.imePadding import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.statusBars import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.material.icons.Icons import androidx.compose.material.icons.automirrored.filled.ArrowBack import androidx.compose.material.icons.filled.Check import androidx.compose.material.icons.filled.Reorder import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.HorizontalDivider import androidx.compose.material3.Icon import androidx.compose.material3.IconButton import androidx.compose.material3.ListItem import androidx.compose.material3.ListItemDefaults import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Scaffold import androidx.compose.material3.Text import androidx.compose.material3.TopAppBar import androidx.compose.material3.TopAppBarDefaults import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.mutableStateListOf import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.snapshots.SnapshotStateList import androidx.compose.ui.Modifier import androidx.compose.ui.draw.alpha import androidx.compose.ui.graphics.Color import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.unit.dp import androidx.navigation.NavController import androidx.navigation.NavGraphBuilder import androidx.navigation.NavType import androidx.navigation.compose.composable import androidx.navigation.navArgument import com.tutpro.baresip.CustomElements.verticalScrollbar fun NavGraphBuilder.codecsScreenRoute(navController: NavController) { composable( route = "codecs/{aor}/{media}", arguments = listOf( navArgument("aor") { type = NavType.StringType }, navArgument("media") { type = NavType.StringType }) ) { backStackEntry -> val aor = backStackEntry.arguments?.getString("aor")!! val media = backStackEntry.arguments?.getString("media")!! val account = UserAgent.ofAor(aor)?.account!! CodecsScreen( onBack = { navController.navigateUp() }, checkOnClick = { updatedCodecs -> val enabledCodecNames = updatedCodecs.filter { it.enabled.value }.map { it.name } val codecList = Utils.implode(enabledCodecNames, ",") Log.d(TAG, "Saving codecs for ${account.aor} (${media}): $codecList") val success = if (media == "audio") Api.account_set_audio_codecs(account.accp, codecList) else Api.account_set_video_codecs(account.accp, codecList) if (success == 0) { if (media == "audio") account.audioCodec = ArrayList(enabledCodecNames) else account.videoCodec = ArrayList(enabledCodecNames) Account.saveAccounts() Log.d("CodecsSave", "Codecs saved successfully.") } else Log.e(TAG, "Failed to set $aor codecs.") navController.navigateUp() }, aor = aor, media = media ) } } @OptIn(ExperimentalMaterial3Api::class) @Composable private fun CodecsScreen( onBack: () -> Unit, checkOnClick: (List) -> Unit, aor: String, media: String ) { val ua = UserAgent.ofAor(aor)!! val acc = ua.account val codecs = remember { mutableStateListOf() } LaunchedEffect(acc, media) { val allCodecs: List = if (media == "audio") { Api.audio_codecs().split(",") } else { Api.video_codecs().split(",").distinct() } val accCodecs: List = if (media == "audio") { acc.audioCodec } else { acc.videoCodec } val currentCodecs = mutableListOf() for (codec in accCodecs) currentCodecs.add(Codec(codec, mutableStateOf(true))) for (codec in allCodecs) if (codec !in accCodecs) currentCodecs.add(Codec(codec, mutableStateOf(false))) codecs.clear() codecs.addAll(currentCodecs) } Scaffold( modifier = Modifier.fillMaxSize().imePadding(), containerColor = MaterialTheme.colorScheme.background, topBar = { Column( modifier = Modifier .fillMaxWidth() .background(MaterialTheme.colorScheme.background) .padding( top = WindowInsets.statusBars.asPaddingValues().calculateTopPadding() ) ) { TopAppBar( title = { Text( text = if (media == "audio") stringResource(R.string.audio_codecs) else stringResource(R.string.video_codecs), fontWeight = FontWeight.Bold ) }, colors = TopAppBarDefaults.topAppBarColors( containerColor = MaterialTheme.colorScheme.primary, navigationIconContentColor = MaterialTheme.colorScheme.onPrimary, titleContentColor = MaterialTheme.colorScheme.onPrimary, actionIconContentColor = MaterialTheme.colorScheme.onPrimary ), windowInsets = WindowInsets(0, 0, 0, 0), navigationIcon = { IconButton(onClick = onBack) { Icon( imageVector = Icons.AutoMirrored.Filled.ArrowBack, contentDescription = "Back", ) } }, actions = { IconButton(onClick = { checkOnClick(codecs) }) { Icon( imageVector = Icons.Filled.Check, contentDescription = "Check" ) } } ) } }, content = { contentPadding -> CodecsContent( contentPadding, codecs ) }, ) } @Composable private fun CodecsContent( contentPadding: PaddingValues, codecs: SnapshotStateList ) { Column( modifier = Modifier .fillMaxWidth() .background(MaterialTheme.colorScheme.background) .padding(contentPadding) .padding(bottom = 16.dp), ) { Codecs(codecs = codecs) } } @OptIn(ExperimentalFoundationApi::class) @Composable private fun Codecs(codecs: SnapshotStateList) { val draggableState = rememberDraggableListState( onMove = { fromIndex, toIndex -> codecs.add(toIndex, codecs.removeAt(fromIndex)) } ) LazyColumn( modifier = Modifier .padding(end = 4.dp) .verticalScrollbar(state = draggableState.listState), state = draggableState.listState, contentPadding = PaddingValues(start = 12.dp, end = 12.dp), ) { draggableItems( state = draggableState, items = codecs, key = { item -> item.name } ) { item, isDragging -> ListItem( colors = ListItemDefaults.colors( containerColor = if (isDragging) MaterialTheme.colorScheme.surfaceContainer else Color.Transparent ), headlineContent = { Text(text = item.name, modifier = Modifier .fillMaxWidth() .alpha(if (item.enabled.value) 1.0f else 0.5f) .padding(start = 6.dp) .combinedClickable( onClick = {}, onLongClick = { item.enabled.value = !item.enabled.value if (item.enabled.value) { val index = codecs.indexOf(item) codecs.removeAt(index) codecs.add(0, item) } else { val index = codecs.indexOf(item) codecs.removeAt(index) codecs.add(item) } } ) ) }, trailingContent = { Icon( modifier = Modifier.dragHandle( state = draggableState, key = item.name ), imageVector =Icons.Filled.Reorder, contentDescription = null ) }, ) if (codecs.indexOf(item) > 0) HorizontalDivider( color = MaterialTheme.colorScheme.outlineVariant, modifier = Modifier.padding(horizontal = 12.dp), thickness = 1.dp ) } } } ================================================ FILE: app/src/main/kotlin/com/tutpro/baresip/Colors.kt ================================================ package com.tutpro.baresip import androidx.compose.ui.graphics.Color val Primary = Color(0xFF0CA1FD) val OnPrimary = Color(0xFFFFFFFF) val PrimaryContainer = Color(0xFFA1CBE6) val OnPrimaryContainer = Color(0xFF032133) val Secondary = Color(0xFF00B9A1) val OnSecondary = Color(0xFFFFFFFF) val SecondaryContainer = Color(0xFF9DE6DC) val OnSecondaryContainer = Color(0xFF00332C) val Tertiary = Color(0xFF8A8378) val OnTertiary = Color(0xFFFFFFFF) val TertiaryContainer = Color(0xFFE6E2DC) val OnTertiaryContainer = Color(0xFF33302C) val Error = Color(0xFFBA1A1A) val OnError = Color(0xFFFFFFFF) val ErrorContainer = Color(0xFFFFDAD6) val OnErrorContainer = Color(0xFF93000A) val Background = Color(0xFFfbfcfc) val OnBackground = Color(0xFF313233) val Surface = Color(0xFFfbfcfc) val OnSurface = Color(0xFF313233) val SurfaceVariant = Color(0xFFd8e0e6) val OnSurfaceVariant = Color(0xFF535f66) val SurfaceContainerLowest = Color(0xFFFFFFFF) val SurfaceContainerLow = Color(0xFFF2F3F9) val SurfaceContainer = Color(0xFFECEEF4) val SurfaceContainerHigh = Color(0xFFE6E8EE) val SurfaceContainerHighest = Color(0xFFE0E2E8) val Outline = Color(0xFF7c8e99) val OutlineVariant = Color(0xFFC2C7CF) val PrimaryDark = Color(0xFF84C1E6) val OnPrimaryDark = Color(0xFF04314C) val PrimaryContainerDark = Color(0xFF054166) val OnPrimaryContainerDark = Color(0xFFA1CBE6) val SecondaryDark = Color(0xFF7FE6D8) val OnSecondaryDark = Color(0xFF004C43) val SecondaryContainerDark = Color(0xFF006659) val OnSecondaryContainerDark = Color(0xFF9DE6DC) val TertiaryDark = Color(0xFFE6E0D8) val OnTertiaryDark = Color(0xFF4C4943) val TertiaryContainerDark = Color(0xFF666159) val OnTertiaryContainerDark = Color(0xFFE6E2DC) val ErrorDark = Color(0xFFFFB4AB) val OnErrorDark = Color(0xFF690005) val ErrorContainerDark = Color(0xFF93000A) val OnErrorContainerDark = Color(0xFFFFDAD6) val BackgroundDark = Color(0xFF313233) val OnBackgroundDark = Color(0xFFe2e4e6) val SurfaceDark = Color(0xFF313233) val OnSurfaceDark = Color(0xFFe2e4e6) val SurfaceVariantDark = Color(0xFF535f66) val OnSurfaceVariantDark = Color(0xFFd2dee6) val SurfaceContainerLowestDark = Color(0xFF0B0E12) val SurfaceContainerLowDark = Color(0xFF181C20) val SurfaceContainerDark = Color(0xFF1D2024) val SurfaceContainerHighDark = Color(0xFF272A2F) val SurfaceContainerHighestDark = Color(0xFF32353A) val OutlineDark = Color(0xFF9daab3) val OutlineVariantDark = Color(0xFF42474E) ================================================ FILE: app/src/main/kotlin/com/tutpro/baresip/Config.kt ================================================ package com.tutpro.baresip import android.Manifest import android.content.Context import androidx.appcompat.app.AppCompatDelegate import java.io.File import java.net.InetAddress import java.nio.charset.StandardCharsets object Config { private val configPath = BaresipService.filesPath + "/config" val audioModules = listOf("opus", "amr", "libg722", "g7221", "g729", "codec2", "g711") private lateinit var config: String private lateinit var previousConfig: String private lateinit var previousLines: List fun initialize(ctx: Context) { config = ctx.assets.open("config.static").bufferedReader().use { it.readText() } if (!File(configPath).exists()) { for (module in audioModules) config = "${config}module ${module}.so\n" previousConfig = config } else { previousConfig = String(Utils.getFileContents(configPath)!!, StandardCharsets.ISO_8859_1) } previousLines = previousConfig.split("\n") val logLevel = previousVariable("log_level") if (logLevel == "") { config = "${config}log_level 2\n" Log.logLevel = Log.LogLevel.WARN } else { config = "${config}log_level $logLevel\n" BaresipService.logLevel = logLevel.toInt() Log.logLevelSet(BaresipService.logLevel) } val autoStart = previousVariable("auto_start") config = if (autoStart != "") "${config}auto_start $autoStart\n" else "${config}auto_start no\n" val sipListen = previousVariable("sip_listen") if (sipListen != "") config = "${config}sip_listen $sipListen\n" val addressFamily = previousVariable("net_af") if (addressFamily != "") { config = "${config}net_af $addressFamily\n" BaresipService.addressFamily = addressFamily } val transportProtocols = previousVariable("sip_transports") if (transportProtocols != "") config = "${config}sip_transports $transportProtocols\n" val sipCertificate = previousVariable("sip_certificate") if (sipCertificate != "") config = "${config}sip_certificate $sipCertificate\n" val sipVerifyServer = previousVariable("sip_verify_server") if (sipVerifyServer != "") config = "${config}sip_verify_server $sipVerifyServer\n" val caBundlePath = "${BaresipService.filesPath}/ca_bundle.crt" val caBundleFile = File(caBundlePath) val caFilePath = "${BaresipService.filesPath}/ca_certs.crt" val caFile = File(caFilePath) if (caFile.exists()) caFile.copyTo(caBundleFile, true) else caBundleFile.writeBytes(byteArrayOf()) Log.d(TAG, "Size of caFile = ${caBundleFile.length()}") val cacertsPath = "/system/etc/security/cacerts" val cacertsDir = File(cacertsPath) var caCount = 0 if (cacertsDir.exists()) { cacertsDir.walk().forEach { if (it.isFile) { caBundleFile.appendBytes( it.readBytes() .toString(Charsets.UTF_8) .substringBefore("Certificate:") .toByteArray(Charsets.UTF_8) ) caCount++ } } Log.d(TAG, "Added $caCount ca certificates from $cacertsPath") } else { Log.w(TAG, "Directory $cacertsDir does not exist!") } Log.d(TAG, "Size of caBundleFile = ${caBundleFile.length()}") config = "${config}sip_cafile $caBundlePath\n" val dynamicDns = previousVariable("dyn_dns") if (dynamicDns == "no") { config = "${config}dyn_dns no\n" for (server in previousVariables("dns_server")) config = "${config}dns_server $server\n" } else { config = "${config}dyn_dns yes\n" for (dnsServer in BaresipService.dnsServers) config = if (Utils.checkIpV4(dnsServer.hostAddress!!)) "${config}dns_server ${dnsServer.hostAddress}:53\n" else "${config}dns_server [${dnsServer.hostAddress}]:53\n" BaresipService.dynDns = true } val userAgent = previousVariable("user_agent") if (userAgent != "") config = "${config}user_agent $userAgent\n" val sipCuserRandom = previousVariable("sip_cuser_random") config = if (sipCuserRandom != "") "${config}sip_cuser_random $sipCuserRandom\n" else "${config}sip_cuser_random yes\n" val darkTheme = previousVariable("dark_theme") Preferences(ctx).displayTheme = if (darkTheme == "yes") { config = "${config}dark_theme yes\n" AppCompatDelegate.MODE_NIGHT_YES } else { AppCompatDelegate.MODE_NIGHT_FOLLOW_SYSTEM } val dynamicColors = previousVariable("dynamic_colors") BaresipService.dynamicColors.value = if (dynamicColors == "yes") { config = "${config}dynamic_colors yes\n" true } else { AppCompatDelegate.MODE_NIGHT_FOLLOW_SYSTEM false } val colorblind = previousVariable("colorblind") config = if (colorblind != "") "${config}colorblind $colorblind\n" else "${config}colorblind no\n" BaresipService.colorblind = colorblind == "yes" val proximitySensing = previousVariable("proximity_sensing") config = if (proximitySensing != "") "${config}proximity_sensing $proximitySensing\n" else "${config}proximity_sensing yes\n" BaresipService.proximitySensing = proximitySensing != "no" var contactsMode = previousVariable("contacts_mode").lowercase() if (contactsMode != "") { if (contactsMode != "baresip" && !Utils.checkPermissions(ctx, arrayOf(Manifest.permission.READ_CONTACTS, Manifest.permission.WRITE_CONTACTS))) contactsMode = "baresip" } else { contactsMode = "baresip" } config = "${config}contacts_mode $contactsMode\n" BaresipService.contactsMode = contactsMode config = "${config}snd_path ${BaresipService.filesPath}/recordings\n" val callVolume = previousVariable("call_volume") if (callVolume != "") { config = "${config}call_volume $callVolume\n" BaresipService.callVolume = callVolume.toInt() } else { config = "${config}call_volume ${BaresipService.callVolume}\n" } val speakerPhone = previousVariable("speaker_phone") if (speakerPhone != "") { config = "${config}speaker_phone $speakerPhone\n" BaresipService.speakerPhone = speakerPhone == "yes" } else { config = "${config}speaker_phone no\n" } val previousModules = previousVariables("module") for (module in audioModules) if ("${module}.so" in previousModules || (module == "libg722" && "g722.so" in previousModules)) config = "${config}module ${module}.so\n" Utils.aecAgcCheck() val micGain = previousVariable("augain") config = if (BaresipService.agcAvailable || micGain == "" || micGain == "1.0") "${config}augain 1.0\n" else "${config}module augain.so\naugain $micGain\n" val opusBitRate = previousVariable("opus_bitrate") config = if (opusBitRate == "") "${config}opus_bitrate 28000\n" else "${config}opus_bitrate $opusBitRate\n" val opusPacketLoss = previousVariable("opus_packet_loss") config = if (opusPacketLoss == "") "${config}opus_packet_loss 1\n" else "${config}opus_packet_loss $opusPacketLoss\n" val audioDelay = previousVariable("audio_delay") if (audioDelay != "") { config = "${config}audio_delay $audioDelay\n" BaresipService.audioDelay = audioDelay.toLong() } else { config = "${config}audio_delay ${BaresipService.audioDelay}\n" } val toneCountry = previousVariable("tone_country") if (toneCountry != "") BaresipService.toneCountry = toneCountry config = "${config}tone_country ${BaresipService.toneCountry}\n" save() BaresipService.isConfigInitialized = true } private fun previousVariable(name: String): String { for (line in previousLines) { val nameValue = line.split(" ", limit = 2) if (nameValue.size == 2 && nameValue[0] == name) return nameValue[1].trim() } return "" } private fun previousVariables(name: String): ArrayList { val result = ArrayList() for (line in previousLines) { val nameValue = line.split(" ", limit = 2) if (nameValue.size == 2 && nameValue[0] == name) result.add(nameValue[1].trim()) } return result } fun variable(name: String): String { for (line in config.split("\n")) { val nameValue = line.split(" ", limit = 2) if (nameValue.size == 2 && nameValue[0] == name) return nameValue[1].trim() } return "" } fun variables(name: String): ArrayList { val result = ArrayList() for (line in config.split("\n")) { val nameValue = line.split(" ", limit = 2) if (nameValue.size == 2 && nameValue[0] == name) result.add(nameValue[1].trim()) } return result } fun addVariable(name: String, value: String) { config += "$name $value\n" } fun removeVariable(variable: String) { config = Utils.removeLinesStartingWithString(config, "$variable ") } fun removeVariableValue(variable: String, value: String) { config = Utils.removeLinesStartingWithString(config, "$variable $value") } fun replaceVariable(variable: String, value: String) { removeVariable(variable) if (value != "") addVariable(variable, value) } fun reset() { Utils.deleteFile(File(configPath)) } fun save() { Utils.putFileContents(configPath, config.toByteArray()) Log.d(TAG, "Saved new config '$config'") // Api.reload_config() } fun dnsServers(): String { return if (variable("dyn_dns") == "yes") "" else { val servers = variables("dns_server") var serverList = "" for (server in servers) serverList += ", $server" serverList.trimStart(',').trimStart(' ') } } fun updateDnsServers(dnsServers: List): Int { var servers = "" for (dnsServer in dnsServers) { if (dnsServer.hostAddress == null) continue var address = dnsServer.hostAddress!!.removePrefix("/") address = if (Utils.checkIpV4(address)) "${address}:53" else "[${address}]:53" servers = if (servers == "") address else "${servers},${address}" } return Api.net_use_nameserver(servers) } } ================================================ FILE: app/src/main/kotlin/com/tutpro/baresip/ConnectionService.kt ================================================ package com.tutpro.baresip import android.content.Intent import android.telecom.CallAudioState import android.telecom.Connection import android.telecom.ConnectionRequest import android.telecom.ConnectionService import android.telecom.DisconnectCause import android.telecom.PhoneAccountHandle import android.telecom.TelecomManager import android.net.Uri import java.util.concurrent.ConcurrentHashMap class ConnectionService : ConnectionService() { private val TAG = "BaresipConnection" companion object { val connections = ConcurrentHashMap() var pendingOutgoingConnection: BaresipConnection? = null var lastDisconnectTime = 0L fun promoteOutgoingConnection(callp: Long) { pendingOutgoingConnection?.let { it.callp = callp connections[callp] = it pendingOutgoingConnection = null } } fun onCallClosed(callp: Long) { connections[callp]?.let { it.setDisconnected(DisconnectCause(DisconnectCause.REMOTE)) it.destroy() connections.remove(callp) } } } override fun onCreateIncomingConnection( connectionManagerPhoneAccount: PhoneAccountHandle?, request: ConnectionRequest? ): Connection { val extras = request?.extras val uap = extras?.getLong("uap") ?: 0L val callp = extras?.getLong("callp") ?: 0L val peerUri = extras?.getString("peerUri") ?: "" Log.d(TAG, "onCreateIncomingConnection for $peerUri") val connection = BaresipConnection(uap, callp) connections[callp] = connection connection.setAddress(Uri.fromParts("sip", peerUri, null), TelecomManager.PRESENTATION_ALLOWED) connection.connectionCapabilities = Connection.CAPABILITY_SUPPORT_HOLD or Connection.CAPABILITY_HOLD val call = Call.ofCallp(callp) if (call != null) BaresipService.instance?.handleIncomingCall(call) val ua = UserAgent.ofUap(uap) if (ua != null) { if (BaresipService.speakerPhone) { @Suppress("DEPRECATION") connection.setAudioRoute(CallAudioState.ROUTE_SPEAKER) } if (ua.account.answerMode == Api.ANSWERMODE_AUTO) { Log.d(TAG, "Auto-answering call $callp") connection.onAnswer() } else { connection.setRinging() } } return connection } override fun onCreateIncomingConnectionFailed( connectionManagerPhoneAccount: PhoneAccountHandle?, request: ConnectionRequest? ) { Log.e(TAG, "onCreateIncomingConnectionFailed") } override fun onCreateOutgoingConnection( connectionManagerPhoneAccount: PhoneAccountHandle?, request: ConnectionRequest? ): Connection { val rootExtras = request?.extras val nestedExtras = rootExtras?.getBundle(TelecomManager.EXTRA_OUTGOING_CALL_EXTRAS) val uap = rootExtras?.getLong("uap", 0L).takeIf { it != 0L } ?: nestedExtras?.getLong("uap") ?: 0L val conferenceCall = rootExtras?.getBoolean("conferenceCall", false) ?: nestedExtras?.getBoolean("conferenceCall") ?: false val onHoldCallp = rootExtras?.getLong("onHoldCallp", 0L).takeIf { it != 0L } ?: nestedExtras?.getLong("onHoldCallp") ?: 0L val destination = request?.address?.schemeSpecificPart ?: "" Log.d(TAG, "onCreateOutgoingConnection to $destination (uap=$uap)") val connection = BaresipConnection(uap, 0L) pendingOutgoingConnection = connection if (BaresipService.speakerPhone) { @Suppress("DEPRECATION") connection.setAudioRoute(CallAudioState.ROUTE_SPEAKER) } connection.setAddress(request?.address, TelecomManager.PRESENTATION_ALLOWED) connection.connectionCapabilities = Connection.CAPABILITY_SUPPORT_HOLD or Connection.CAPABILITY_HOLD // Start the SIP connection logic if (uap != 0L) { val sipUri = if (destination.startsWith("sip:")) destination else "sip:$destination" BaresipService.instance?.runCall(uap, sipUri, conferenceCall, onHoldCallp) } else { Log.e(TAG, "Cannot start outgoing call: uap is 0") connection.setDisconnected(DisconnectCause(DisconnectCause.ERROR, "No Account")) connection.destroy() pendingOutgoingConnection = null } connection.setDialing() return connection } override fun onCreateOutgoingConnectionFailed( connectionManagerPhoneAccount: PhoneAccountHandle?, request: ConnectionRequest? ) { Log.e(TAG, "onCreateOutgoingConnectionFailed") pendingOutgoingConnection = null } inner class BaresipConnection(val uap: Long, var callp: Long) : Connection() { private var isDisconnecting = false override fun onAnswer() { Log.d(TAG, "Telecom Connection onAnswer $callp") val intent = Intent(this@ConnectionService, MainActivity::class.java) intent.flags = Intent.FLAG_ACTIVITY_SINGLE_TOP or Intent.FLAG_ACTIVITY_CLEAR_TOP or Intent.FLAG_ACTIVITY_NEW_TASK intent.putExtra("action", "call answer") intent.putExtra("callp", callp) startActivity(intent) BaresipService.instance?.updateStatusNotification() setActive() } override fun onReject() { Log.d(TAG, "Telecom Connection onReject $callp") Api.ua_hangup(uap, callp, 486, "Rejected") setDisconnected(DisconnectCause(DisconnectCause.REJECTED)) connections.remove(callp) destroy() } override fun onDisconnect() { if (isDisconnecting) return if (System.currentTimeMillis() - lastDisconnectTime < 500) { Log.d(TAG, "Ignoring cascaded onDisconnect for $callp") return } Log.d(TAG, "Telecom Connection onDisconnect $callp") isDisconnecting = true lastDisconnectTime = System.currentTimeMillis() if (callp == 0L) { pendingOutgoingConnection = null setDisconnected(DisconnectCause(DisconnectCause.CANCELED)) destroy() return } val call = Call.ofCallp(callp) if (call != null) Api.ua_hangup(uap, callp, 0, "") setDisconnected(DisconnectCause(DisconnectCause.LOCAL)) destroy() // Allow other disconnects after a short period to prevent the "Telecom Cascade" effect android.os.Handler(android.os.Looper.getMainLooper()).postDelayed({ isDisconnecting = false }, 1000) } override fun onAbort() { Log.d(TAG, "Telecom Connection onAbort $callp") if (callp != 0L) { Api.ua_hangup(uap, callp, 0, "") connections.remove(callp) } else { pendingOutgoingConnection = null } setDisconnected(DisconnectCause(DisconnectCause.CANCELED)) destroy() } @Deprecated("Deprecated in Java") @Suppress("DEPRECATION") override fun onCallAudioStateChanged(state: CallAudioState?) { super.onCallAudioStateChanged(state) Log.d(TAG, "onCallAudioStateChanged: $state") state?.let { if (BaresipService.isMicMuted != it.isMuted) { BaresipService.isMicMuted = it.isMuted Api.calls_mute(it.isMuted) BaresipService.postServiceEvent( ServiceEvent("mic muted,${it.isMuted}", arrayListOf(uap, callp), System.nanoTime()) ) } val isSpeaker = it.route == CallAudioState.ROUTE_SPEAKER if (BaresipService.speakerPhone != isSpeaker) { BaresipService.speakerPhone = isSpeaker BaresipService.postServiceEvent( ServiceEvent("speaker update,${isSpeaker}", arrayListOf(uap, callp), System.nanoTime() ) ) } } } override fun onHold() { Log.d(TAG, "Telecom Connection onHold $callp") val call = Call.ofCallp(callp) if (call != null && !call.conferenceCall) { // 1. Force SIP Signaling if (Api.call_hold(call.callp, true)) { // 2. Sync Call object state call.onhold = true call.callOnHold.value = true call.showOnHoldNotice.value = true // 3. Tell Telecom the move is complete setOnHold() } else { Log.e(TAG, "SIP Hold failed for $callp") } } } override fun onUnhold() { Log.d(TAG, "Telecom Connection onUnhold $callp") val call = Call.ofCallp(callp) if (call != null && !call.conferenceCall) { // 1. Force SIP Signaling if (Api.call_hold(call.callp, false)) { // 2. Sync Call object state call.onhold = false call.callOnHold.value = false call.showOnHoldNotice.value = false // 3. Tell Telecom we are active setActive() } else { Log.e(TAG, "SIP Resume failed for $callp") } } } override fun onPlayDtmfTone(c: Char) { Log.d(TAG, "Telecom Connection onPlayDtmfTone $c") if (callp != 0L) { val call = Call.ofCallp(callp) call?.sendDigit(c) } } } } ================================================ FILE: app/src/main/kotlin/com/tutpro/baresip/Constants.kt ================================================ package com.tutpro.baresip const val TAG = "Baresip" const val LOW_CHANNEL_ID = "com.tutpro.baresip.low" const val MEDIUM_CHANNEL_ID = "com.tutpro.baresip.medium" const val HIGH_CHANNEL_ID = "com.tutpro.baresip.high" const val STATUS_NOTIFICATION_ID = 101 const val CALL_NOTIFICATION_ID = 102 const val CALL_MISSED_NOTIFICATION_ID = 103 const val TRANSFER_NOTIFICATION_ID = 104 const val MESSAGE_NOTIFICATION_ID = 105 const val STATUS_REQ_CODE = 1 const val CALL_REQ_CODE = 2 const val ANSWER_REQ_CODE = 3 const val REJECT_REQ_CODE = 4 const val TRANSFER_REQ_CODE = 5 const val ACCEPT_REQ_CODE = 6 const val DENY_REQ_CODE = 7 const val MESSAGE_REQ_CODE = 8 // const val REPLY_REQ_CODE = 9 const val SAVE_REQ_CODE = 10 const val DELETE_REQ_CODE = 11 const val DIRECT_REPLY_REQ_CODE = 12 const val REGISTRATION_INTERVAL = 900 const val NO_AUTH_PASS = "t%Qa?~?J8,~6" const val MESSAGE_DOWN = 2131165304 const val MESSAGE_UP = 2131165306 const val MESSAGE_UP_FAIL = 2131165307 const val MESSAGE_UP_WAIT = 2131165308 const val CALL_UP_GREEN = 2131165323 const val CALL_DOWN_GREEN = 2131165316 const val CALL_UP_RED = 2131165324 const val CALL_DOWN_BLUE = 2131165315 const val CALL_DOWN_RED = 2131165317 const val CALL_MISSED_OUT = 2131165320 const val CALL_MISSED_IN = 2131165319 val mediaEncMap = mapOf("zrtp" to "ZRTP", "dtls_srtp" to "DTLS-SRTPF", "srtp-mand" to "SRTP-MAND", "srtp" to "SRTP", "" to "--") val mediaNatMap = mapOf("stun" to "STUN", "turn" to "TURN", "ice" to "ICE", "" to "--") ================================================ FILE: app/src/main/kotlin/com/tutpro/baresip/Contact.kt ================================================ package com.tutpro.baresip import android.content.Context import android.database.Cursor import android.graphics.Bitmap import android.graphics.BitmapFactory import android.net.Uri import android.provider.ContactsContract import androidx.core.net.toUri import com.tutpro.baresip.BaresipService.Companion.contactNames import java.io.File import java.util.ArrayList sealed class Contact { class BaresipContact(var name: String, var uri: String, var color: Int, var id: Long, var favorite: Boolean): Contact() { var avatarImage: Bitmap? = null } class AndroidContact(var name: String, var color: Int, var thumbnailUri: Uri?, var id: Long, var favorite: Boolean): Contact() { val uris = ArrayList() } fun name(): String { return when (this) { is AndroidContact -> name is BaresipContact -> name } } fun id(): Long { return when (this) { is AndroidContact -> id is BaresipContact -> id } } fun favorite(): Boolean { return when (this) { is AndroidContact -> favorite is BaresipContact -> favorite } } fun color(): String { val intColor = when (this) { is AndroidContact -> color is BaresipContact -> color } // Mask with 0xFFFFFF to ensure we get the standard 6-character hex code (RRGGBB) // ignoring the alpha channel for compatibility with simple string parsers. return String.format("#%06X", 0xFFFFFF and intColor) } fun copy(): Contact { val copy = when (this) { is BaresipContact -> BaresipContact(name, uri, color, id, favorite) is AndroidContact -> AndroidContact(name, color, thumbnailUri, id, favorite) } when (this) { is BaresipContact -> (copy as BaresipContact).avatarImage = this.avatarImage is AndroidContact -> (copy as AndroidContact).uris.addAll(this.uris) } return copy } companion object { // Return contact name of uri or uri itself if contact with uri is not found fun contactName(uri: String): String { var contact = findContact(uri) if (contact == null) { val userPart = Utils.uriUserPart(uri) if (Utils.isTelNumber(userPart)) contact = findContact("tel:$userPart") } if (contact != null) return contact.name() return uri } fun baresipContact(name: String): BaresipContact? { for (c in BaresipService.baresipContacts.value) if (c.name == name) return c return null } fun androidContact(name: String): AndroidContact? { for (c in BaresipService.androidContacts.value) if (c.name == name) return c return null } // Return URIs of contact name fun contactUris(name: String): ArrayList { val uris = ArrayList() for (c in BaresipService.contacts) when (c) { is BaresipContact -> { if (c.name.equals(name, ignoreCase = true)) { uris.add(c.uri.removePrefix("<") .replaceAfter(">", "") .replace(">", "")) return uris } } is AndroidContact -> { if (c.name == name) { for (u in c.uris) uris.add(u) return uris } } } return uris } fun findContact(uri: String): Contact? { for (c in BaresipService.contacts) when (c) { is BaresipContact -> { if (Utils.uriMatch(c.uri, uri)) return c } is AndroidContact -> { val cleanUri = uri.filterNot{setOf('-', ' ', '(', ')').contains(it)} for (u in c.uris) if (Utils.uriMatch(u.filterNot{setOf('-', ' ', '(', ')').contains(it)}, cleanUri)) return c } } return null } fun nameExists(name: String, list: List, ignoreCase: Boolean): Boolean { for (c in list) if (c.name().equals(name, ignoreCase = ignoreCase)) return true return false } fun saveBaresipContacts() { val avatarFiles = avatarFileNames() var contents = "" for (c in BaresipService.baresipContacts.value) { contents += "\"${c.name}\" <${c.uri}>;id=${c.id};color=${c.color}" + ";favorite=${if (c.favorite) "yes" else "no"}\n" avatarFiles.remove(c.id.toString() + ".png") } Utils.putFileContents(BaresipService.filesPath + "/contacts", contents.toByteArray()) for (f in avatarFiles) File(BaresipService.filesPath + "/" + f).delete() } fun loadAndroidContacts(ctx: Context) { // If phone type is needed, add DATA2 to projection. Then phone type can be get from // cursor using getColumnIndex(ContactsContract.CommonDataKinds.Phone.TYPE val projection = arrayOf(ContactsContract.Data.CONTACT_ID, ContactsContract.Data.DISPLAY_NAME, ContactsContract.Data.MIMETYPE, ContactsContract.Data.DATA1, /* ContactsContract.Data.DATA2 ,*/ ContactsContract.Data.PHOTO_THUMBNAIL_URI, ContactsContract.Contacts.STARRED) val selection = ContactsContract.Data.MIMETYPE + "='" + ContactsContract.CommonDataKinds.SipAddress.CONTENT_ITEM_TYPE + "' OR " + ContactsContract.Data.MIMETYPE + "='" + ContactsContract.CommonDataKinds.Phone.CONTENT_ITEM_TYPE + "'" val cur: Cursor? = ctx.contentResolver.query(ContactsContract.Data.CONTENT_URI, projection, selection, null, null) BaresipService.androidContacts.value = listOf() val contacts = HashMap() while (cur != null && cur.moveToNext()) { val id = cur.getLong(0) val name = cur.getString(1) ?: "" val mime = cur.getString(2) val data = cur.getString(3) val thumb = cur.getString(4)?.toUri() val starred = cur.getInt(5) val contact = if (contacts.containsKey(id)) contacts[id]!! else AndroidContact(name, Utils.randomColor(), thumb, id, starred == 1) if (contact.name == "" && name != "") contact.name = name if (contact.thumbnailUri == null && thumb != null) contact.thumbnailUri = thumb if (mime == ContactsContract.CommonDataKinds.Phone.CONTENT_ITEM_TYPE) { val uri = "tel:${data.filterNot { setOf('-', ' ', '(', ')').contains(it) }}" if (uri !in contact.uris) contact.uris.add(uri) // contact.types.add(typeToString(cur.getColumnIndex(ContactsContract.CommonDataKinds.Phone.TYPE))) } else if (mime == ContactsContract.CommonDataKinds.SipAddress.CONTENT_ITEM_TYPE) contact.uris.add("sip:$data") else continue if (!contacts.containsKey(id)) contacts[id] = contact } cur?.close() val newList = mutableListOf() for ((_, value) in contacts) if (value.name != "" && value.uris.isNotEmpty()) newList.add(value) BaresipService.androidContacts.value = newList.toList() } @Suppress("unused") private fun typeToString(type: Int): String { return when(type) { ContactsContract.CommonDataKinds.Phone.TYPE_HOME -> "Home" ContactsContract.CommonDataKinds.Phone.TYPE_MOBILE -> "Mobile" ContactsContract.CommonDataKinds.Phone.TYPE_WORK -> "Work" else -> "Unknown" } } fun restoreBaresipContacts(): Boolean { val content = Utils.getFileContents(BaresipService.filesPath + "/contacts") ?: return false val contacts = String(content) var contactNo = 0 val baseId = System.currentTimeMillis() BaresipService.baresipContacts.value = mutableListOf() contacts.lines().forEach { val parts = it.split("\"") if (parts.size == 3) { contactNo++ val name = parts[1] val uriParams = parts[2].trim() val uri = uriParams.substringAfter("<").substringBefore(">") val params = uriParams.substringAfter(">;") val colorValue = Utils.paramValue(params, "color" ) val color: Int = if (colorValue != "") colorValue.toInt() else Utils.randomColor() val idValue = Utils.paramValue(params, "id" ) val id: Long = if (idValue != "") idValue.toLong() else baseId + contactNo val favorite = Utils.paramValue(params, "favorite" ) == "yes" Log.d(TAG, "Restoring contact $name, $uri, $color, $id") val contact = BaresipContact(name, uri, color, id, favorite) val avatarFilePath = BaresipService.filesPath + "/$id.png" if (File(avatarFilePath).exists()) { try { contact.avatarImage = BitmapFactory.decodeFile(avatarFilePath) Log.d(TAG, "Set avatarImage") if (contact.avatarImage == null) Log.d(TAG, "Contact $id avatarImage is null") } catch (e: Exception) { Log.e(TAG, "Could not read avatar image from file $id.png: ${e.message}") } } BaresipService.baresipContacts.value += contact } } return true } fun contactsUpdate() { BaresipService.contacts = mutableListOf() if (BaresipService.contactsMode != "android") for (c in BaresipService.baresipContacts.value) BaresipService.contacts.add(c.copy()) if (BaresipService.contactsMode != "baresip") for (c in BaresipService.androidContacts.value) if (!nameExists(c.name, BaresipService.contacts, true)) BaresipService.contacts.add(c.copy()) BaresipService.contacts.sortBy{ when (it) { is BaresipContact -> if (it.favorite) "0" + it.name else "1" + it.name is AndroidContact -> if (it.favorite) "0" + it.name else "1" + it.name }} generateContactNames() } fun addBaresipContact(contact: BaresipContact) { BaresipService.baresipContacts.value += contact saveBaresipContacts() contactsUpdate() } fun updateBaresipContact(id: Long, contact: BaresipContact) { val updatedContacts = BaresipService.baresipContacts.value.toMutableList() updatedContacts.removeIf { it.id == id } updatedContacts.add(contact) BaresipService.baresipContacts.value = updatedContacts.toList() saveBaresipContacts() contactsUpdate() } fun removeBaresipContact(contact: BaresipContact) { val removed = BaresipService.baresipContacts.value.toMutableList() removed.removeIf { it.id == contact.id } BaresipService.baresipContacts.value = removed.toList() saveBaresipContacts() contactsUpdate() } private fun generateContactNames () { val newList = mutableListOf() for (c in BaresipService.contacts) when (c) { is BaresipContact -> newList.add(c.name) is AndroidContact -> newList.add(c.name) } contactNames.value = newList.toList() } private fun avatarFileNames(): MutableList { return File(BaresipService.filesPath).list()!!.filter{ it.endsWith(".png")}.toMutableList() } } } ================================================ FILE: app/src/main/kotlin/com/tutpro/baresip/ContactsScreen.kt ================================================ package com.tutpro.baresip import android.content.Context import android.content.Intent import android.provider.ContactsContract import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.foundation.Image import androidx.compose.foundation.background import androidx.compose.foundation.clickable import androidx.compose.foundation.combinedClickable import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.WindowInsets import androidx.compose.foundation.layout.asPaddingValues import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.imePadding import androidx.compose.foundation.layout.navigationBarsPadding import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.statusBars import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.items import androidx.compose.foundation.lazy.rememberLazyListState import androidx.compose.foundation.shape.CircleShape import androidx.compose.foundation.text.KeyboardOptions import androidx.compose.material.icons.Icons import androidx.compose.material.icons.automirrored.filled.ArrowBack import androidx.compose.material.icons.filled.Add import androidx.compose.material.icons.filled.Edit import androidx.compose.material.icons.outlined.Clear import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.Icon import androidx.compose.material3.IconButton import androidx.compose.material3.MaterialTheme import androidx.compose.material3.OutlinedTextField import androidx.compose.material3.Scaffold import androidx.compose.material3.SmallFloatingActionButton import androidx.compose.material3.Text import androidx.compose.material3.TopAppBar import androidx.compose.material3.TopAppBarDefaults import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip import androidx.compose.ui.graphics.asImageBitmap import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalSoftwareKeyboardController import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.TextStyle import androidx.compose.ui.text.buildAnnotatedString import androidx.compose.ui.text.font.FontStyle import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.input.ImeAction import androidx.compose.ui.text.input.KeyboardType import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import androidx.navigation.NavController import androidx.navigation.NavGraphBuilder import androidx.navigation.compose.composable import coil.compose.AsyncImage import com.tutpro.baresip.CustomElements.AlertDialog import com.tutpro.baresip.CustomElements.TextAvatar import com.tutpro.baresip.CustomElements.verticalScrollbar import java.io.File import java.io.IOException const val avatarSize: Int = 96 fun NavGraphBuilder.contactsScreenRoute( navController: NavController, viewModel: ViewModel ) { composable("contacts") { _ -> ContactsScreen(navController = navController, viewModel = viewModel) } } @OptIn(ExperimentalMaterial3Api::class) @Composable private fun ContactsScreen( navController: NavController, viewModel: ViewModel ) { val ctx = LocalContext.current var searchContactName by remember { mutableStateOf("") } Scaffold( modifier = Modifier.fillMaxSize().imePadding(), containerColor = MaterialTheme.colorScheme.background, topBar = { Column( modifier = Modifier .fillMaxWidth() .background(MaterialTheme.colorScheme.background) .padding( top = WindowInsets.statusBars.asPaddingValues().calculateTopPadding() ) ) { TopAppBar( title = { Text( text = stringResource(R.string.contacts), fontWeight = FontWeight.Bold ) }, colors = TopAppBarDefaults.topAppBarColors( containerColor = MaterialTheme.colorScheme.primary, navigationIconContentColor = MaterialTheme.colorScheme.onPrimary, titleContentColor = MaterialTheme.colorScheme.onPrimary, ), navigationIcon = { IconButton( onClick = { navController.navigateUp() } ) { Icon( imageVector = Icons.AutoMirrored.Filled.ArrowBack, contentDescription = "Back", ) } }, windowInsets = WindowInsets(0, 0, 0, 0), ) } }, bottomBar = { BottomBar( navController = navController, searchContactName = searchContactName, onSearchContactNameChange = { searchContactName = it } ) }, content = { contentPadding -> ContactsContent(ctx, viewModel, navController, contentPadding, searchContactName) } ) } @Composable private fun BottomBar( navController: NavController, searchContactName: String, onSearchContactNameChange: (String) -> Unit ) { val keyboardController = LocalSoftwareKeyboardController.current Row( modifier = Modifier .fillMaxWidth() .navigationBarsPadding() .padding(start = 16.dp, end = 16.dp, bottom = 16.dp), verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(8.dp) ) { OutlinedTextField( value = searchContactName, onValueChange = { onSearchContactNameChange(it) if (it.isBlank()) { keyboardController?.hide() } }, modifier = Modifier.weight(1f), singleLine = true, trailingIcon = { if (searchContactName.isNotEmpty()) Icon( Icons.Outlined.Clear, contentDescription = null, modifier = Modifier.clickable { onSearchContactNameChange("") keyboardController?.hide() }, tint = MaterialTheme.colorScheme.onSurfaceVariant ) }, label = { Text(stringResource(R.string.search)) }, textStyle = TextStyle(fontSize = 18.sp), keyboardOptions = KeyboardOptions( keyboardType = KeyboardType.Text, imeAction = ImeAction.Done ) ) SmallFloatingActionButton( onClick = { navController.navigate("baresip_contact//new") }, containerColor = MaterialTheme.colorScheme.secondary, contentColor = MaterialTheme.colorScheme.onSecondary ) { Icon( imageVector = Icons.Filled.Add, modifier = Modifier.size(36.dp), contentDescription = stringResource(R.string.add) ) } } } @OptIn(ExperimentalFoundationApi::class) @Composable private fun ContactsContent( ctx: Context, viewModel: ViewModel, navController: NavController, contentPadding: PaddingValues, searchQuery: String ) { val showDialog = remember { mutableStateOf(false) } val dialogMessage = remember { mutableStateOf("") } val secondText = remember { mutableStateOf("") } val secondAction = remember { mutableStateOf({}) } val lastText = remember { mutableStateOf("") } val lastAction = remember { mutableStateOf({}) } if (showDialog.value) AlertDialog( showDialog = showDialog, title = stringResource(R.string.confirmation), message = dialogMessage.value, firstButtonText = stringResource(R.string.cancel), secondButtonText = secondText.value, onSecondClicked = secondAction.value, lastButtonText = lastText.value, onLastClicked = lastAction.value, ) val lazyListState = rememberLazyListState() LaunchedEffect(searchQuery) { if (searchQuery.isBlank()) { lazyListState.scrollToItem(0) } } val filteredContacts = remember(BaresipService.contacts, searchQuery) { if (searchQuery.isBlank()) { BaresipService.contacts.map { contact -> Pair(contact, buildAnnotatedString { append(contact.name()) }) } } else { val normalizedQuery = Utils.unaccent(searchQuery) BaresipService.contacts .filter { contact -> Utils.unaccent(contact.name()).contains(normalizedQuery, ignoreCase = true) } .map { contact -> Pair(contact, Utils.buildAnnotatedStringWithHighlight(contact.name(), searchQuery)) } } } LazyColumn( modifier = Modifier .fillMaxWidth() .background(MaterialTheme.colorScheme.background) .padding(contentPadding) .padding(start = 16.dp, end = 4.dp, top = 16.dp, bottom = 10.dp) .verticalScrollbar(state = lazyListState), state = lazyListState, verticalArrangement = Arrangement.spacedBy(10.dp), ) { items(filteredContacts, key = { it.first.id() }) { (contact, annotatedName) -> Row( verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.Start ) { when (contact) { is Contact.BaresipContact -> { val avatarImage = contact.avatarImage if (avatarImage != null) Image( bitmap = avatarImage.asImageBitmap(), contentDescription = "Avatar", contentScale = ContentScale.Crop, modifier = Modifier .size(36.dp) .clip(CircleShape) ) else TextAvatar(contact.name(), contact.color) } is Contact.AndroidContact -> { val thumbNailUri = contact.thumbnailUri if (thumbNailUri != null) AsyncImage( model = thumbNailUri, contentDescription = "Avatar", contentScale = ContentScale.Crop, modifier = Modifier .size(36.dp) .clip(CircleShape), ) else TextAvatar(contact.name(), contact.color) } } when (contact) { is Contact.BaresipContact -> { Text(text = annotatedName, fontSize = 20.sp, fontStyle = if (contact.favorite()) FontStyle.Italic else FontStyle.Normal, modifier = Modifier .weight(1f) .padding(start = 10.dp) .combinedClickable( onClick = { val aor = viewModel.selectedAor.value val ua = UserAgent.ofAor(aor) val intent = Intent(ctx, MainActivity::class.java) if (ua != null) { intent.putExtra("uap", ua.uap) intent.putExtra("peer", contact.uri) } else Log.w(TAG, "onClickListener did not find UA for $aor") dialogMessage.value = String.format( ctx.getString(R.string.contact_action_question), contact.name() ) secondText.value = ctx.getString(R.string.call) secondAction.value = { if (ua != null) { handleIntent(ctx, viewModel, intent, "call") navController.navigate("main") { popUpTo("main") launchSingleTop = true } } } lastText.value = ctx.getString(R.string.send_message) lastAction.value = { if (ua != null) { handleIntent(ctx, viewModel, intent, "message") navController.navigateUp() } } showDialog.value = true }, onLongClick = { dialogMessage.value = String.format( ctx.getString(R.string.contact_delete_question), contact.name() ) secondText.value = "" lastText.value = ctx.getString(R.string.delete) lastAction.value = { val id = contact.id val avatarFile = File( BaresipService.filesPath, "$id.png" ) if (avatarFile.exists()) { try { avatarFile.delete() } catch (e: IOException) { Log.e( TAG, "Could not delete file $id.png: ${e.message}" ) } } Contact.removeBaresipContact(contact) } showDialog.value = true } ) ) SmallFloatingActionButton( modifier = Modifier.padding(end = 10.dp), onClick = { navController.navigate("baresip_contact/${contact.name()}/old") }, containerColor = MaterialTheme.colorScheme.tertiaryContainer, contentColor = MaterialTheme.colorScheme.onTertiaryContainer ) { Icon( imageVector = Icons.Filled.Edit, modifier = Modifier.size(28.dp), contentDescription = stringResource(R.string.edit) ) } } is Contact.AndroidContact -> { Text(text = annotatedName, fontSize = 20.sp, fontStyle = if (contact.favorite()) FontStyle.Italic else FontStyle.Normal, modifier = Modifier .weight(1f) .padding(start = 10.dp, top = 4.dp, bottom = 4.dp) .combinedClickable( onClick = { navController.navigate("android_contact/${contact.name()}") }, onLongClick = { dialogMessage.value = String.format( ctx.getString(R.string.contact_delete_question), contact.name() ) secondText.value = "" lastText.value = ctx.getString(R.string.delete) lastAction.value = { ctx.contentResolver.delete( ContactsContract.RawContacts.CONTENT_URI, ContactsContract.Contacts.DISPLAY_NAME + "='" + contact.name() + "'", null ) } showDialog.value = true } ) ) } } } } } } ================================================ FILE: app/src/main/kotlin/com/tutpro/baresip/CustomElements.kt ================================================ package com.tutpro.baresip import android.content.Context import android.graphics.Bitmap import android.widget.Toast import androidx.compose.animation.core.animateFloatAsState import androidx.compose.animation.core.tween import androidx.compose.foundation.BorderStroke import androidx.compose.foundation.Canvas import androidx.compose.foundation.Image import androidx.compose.foundation.ScrollState import androidx.compose.foundation.gestures.detectTapGestures import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.RowScope import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.width import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.LazyListState import androidx.compose.foundation.lazy.itemsIndexed import androidx.compose.foundation.shape.CircleShape import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.text.KeyboardOptions import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.Visibility import androidx.compose.material.icons.filled.VisibilityOff import androidx.compose.material3.BasicAlertDialog import androidx.compose.material3.ButtonDefaults import androidx.compose.material3.Card import androidx.compose.material3.CardDefaults import androidx.compose.material3.DropdownMenu import androidx.compose.material3.DropdownMenuItem import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.HorizontalDivider import androidx.compose.material3.Icon import androidx.compose.material3.IconButton import androidx.compose.material3.MaterialTheme import androidx.compose.material3.OutlinedTextField import androidx.compose.material3.OutlinedTextFieldDefaults import androidx.compose.material3.Surface import androidx.compose.material3.Text import androidx.compose.material3.TextButton import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.MutableState import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip import androidx.compose.ui.draw.drawWithContent import androidx.compose.ui.focus.FocusRequester import androidx.compose.ui.focus.focusRequester import androidx.compose.ui.geometry.CornerRadius import androidx.compose.ui.geometry.Offset import androidx.compose.ui.geometry.Size import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Shape import androidx.compose.ui.graphics.SolidColor import androidx.compose.ui.graphics.asImageBitmap import androidx.compose.ui.input.pointer.pointerInput import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.platform.SoftwareKeyboardController import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.TextStyle import androidx.compose.ui.text.input.KeyboardType import androidx.compose.ui.text.input.PasswordVisualTransformation import androidx.compose.ui.text.input.VisualTransformation import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import androidx.compose.ui.window.DialogProperties object CustomElements { @Composable fun Button( onClick: () -> Unit, onLongClick: () -> Unit, modifier: Modifier = Modifier, shape: Shape, border: BorderStroke? = null, color: Color, content: @Composable RowScope.() -> Unit ) { Surface( shape = shape, color = color, border = border, modifier = modifier .pointerInput(Unit) { detectTapGestures( onTap = { onClick() }, onLongPress = { onLongClick() }, ) } .then(modifier), ) { Row( modifier = Modifier.padding(ButtonDefaults.ContentPadding), verticalAlignment = Alignment.CenterVertically, content = content ) } } @Composable fun DropdownMenu( expanded: Boolean, onDismissRequest: () -> Unit, items: List, onItemClick: (String) -> Unit ) { DropdownMenu( expanded = expanded, onDismissRequest = onDismissRequest, containerColor = MaterialTheme.colorScheme.surfaceVariant ) { val itemsIterator = items.iterator() while (itemsIterator.hasNext()) { val item = itemsIterator.next() DropdownMenuItem( text = { Text( text = item, color = MaterialTheme.colorScheme.onSurfaceVariant, fontSize = 16.sp ) }, onClick = { onItemClick(item) } ) if (itemsIterator.hasNext()) HorizontalDivider(color = MaterialTheme.colorScheme.outlineVariant) } } } @Composable fun TextAvatar(name: String, color: Int) { Box( modifier = Modifier.size(36.dp), contentAlignment = Alignment.Center ) { Canvas(modifier = Modifier.fillMaxSize()) { drawCircle(SolidColor(Color(color))) } val text = if (name == "") "" else name[0].toString() Text(text, color = Color.White, fontSize = 20.sp) } } @Composable fun ImageAvatar(bitmap: Bitmap) { Image( bitmap = bitmap.asImageBitmap(), contentDescription = "Avatar", contentScale = ContentScale.Crop, modifier = Modifier .size(36.dp) .clip(CircleShape) ) } @Composable fun Modifier.verticalScrollbar( state: ScrollState, scrollbarWidth: Dp = 4.dp, alwaysShow: Boolean = true, color: Color = MaterialTheme.colorScheme.outlineVariant ): Modifier { val alpha by animateFloatAsState( targetValue = if(state.isScrollInProgress || alwaysShow) 1f else 0f, animationSpec = tween(400, delayMillis = if(state.isScrollInProgress) 0 else 700), label = "scrollbarAlpha" ) return this then Modifier.drawWithContent { drawContent() val viewHeight = state.viewportSize.toFloat() if (viewHeight <= 0f) return@drawWithContent // Safety check for zero height val contentHeight = state.maxValue + viewHeight val minHeight = 10.dp.toPx() // Ensure the 'max' of coerceIn is at least as large as 'min' val scrollbarHeight = (viewHeight * (viewHeight / contentHeight)) .coerceIn(minHeight.coerceAtMost(viewHeight) .. viewHeight) val variableZone = viewHeight - scrollbarHeight // Prevent division by zero if maxValue is 0 (no scrolling needed) val scrollbarOffsetY = if (state.maxValue > 0) (state.value.toFloat() / state.maxValue) * variableZone else 0f drawRoundRect( cornerRadius = CornerRadius(scrollbarWidth.toPx() / 2, scrollbarWidth.toPx() / 2), color = color, topLeft = Offset(this.size.width - scrollbarWidth.toPx(), scrollbarOffsetY), size = Size(scrollbarWidth.toPx(), scrollbarHeight), alpha = alpha ) } } @Composable fun Modifier.verticalScrollbar( state: LazyListState, width: Dp = 4.dp, alwaysShow: Boolean = true, color: Color = MaterialTheme.colorScheme.outlineVariant ): Modifier { val alpha by animateFloatAsState( targetValue = if (state.isScrollInProgress || alwaysShow) 1f else 0f, animationSpec = tween(durationMillis = if (state.isScrollInProgress) 150 else 500), label = "lazyScrollbarAlpha" ) return this.drawWithContent { drawContent() val totalItems = state.layoutInfo.totalItemsCount val visibleItemsInfo = state.layoutInfo.visibleItemsInfo // Check if there are items and if they actually exceed the viewport if (totalItems > 0 && visibleItemsInfo.isNotEmpty()) { val firstVisibleElementIndex = visibleItemsInfo.first().index val needDrawScrollbar = state.isScrollInProgress || alpha > 0.0f if (needDrawScrollbar) { val elementHeight = this.size.height / totalItems val scrollbarOffsetY = firstVisibleElementIndex * elementHeight val scrollbarHeight = visibleItemsInfo.size * elementHeight // Only draw if the scrollbar is actually smaller than the track if (scrollbarHeight < this.size.height) drawRoundRect( cornerRadius = CornerRadius(width.toPx() / 2, width.toPx() / 2), color = color, topLeft = Offset(this.size.width - width.toPx(), scrollbarOffsetY), size = Size(width.toPx(), scrollbarHeight), alpha = alpha ) } } } } @OptIn(ExperimentalMaterial3Api::class) @Composable fun AlertDialog( showDialog: MutableState, title: String, message: String, firstButtonText: String = "", onFirstClicked: () -> Unit = {}, secondButtonText: String = "", onSecondClicked: () -> Unit = {}, thirdButtonText: String = "", onThirdClicked: () -> Unit = {}, lastButtonText: String = "", onLastClicked: () -> Unit = {} ) { if (showDialog.value) { BasicAlertDialog( onDismissRequest = { showDialog.value = false }, content = { Card( modifier = Modifier .fillMaxWidth() .padding(top = 16.dp, start = 16.dp, end = 16.dp, bottom = 0.dp), shape = RoundedCornerShape(16.dp), colors = CardDefaults.cardColors( containerColor = MaterialTheme.colorScheme.surfaceVariant ) ) { Column(modifier = Modifier.padding(16.dp)) { Text( text = title, modifier = Modifier.fillMaxWidth(), textAlign = TextAlign.Start, fontSize = 20.sp, color = MaterialTheme.colorScheme.onSurface ) Spacer(modifier = Modifier.height(16.dp)) Text( text = message, modifier = Modifier.fillMaxWidth(), textAlign = TextAlign.Start, fontSize = 16.sp, color = MaterialTheme.colorScheme.onSurfaceVariant ) Spacer(modifier = Modifier.height(16.dp)) if (lastButtonText.isNotEmpty()) { val buttonCount = listOf( firstButtonText, secondButtonText, thirdButtonText, lastButtonText ).count { it.isNotEmpty() } if (buttonCount >= 3) { // Use a Column for 3-4 buttons, aligned to the end (right) Column( modifier = Modifier.fillMaxWidth(), horizontalAlignment = Alignment.End ) { TextButton(onClick = { onFirstClicked() showDialog.value = false }) { Text( text = firstButtonText.uppercase(), fontSize = 14.sp, color = MaterialTheme.colorScheme.onSurfaceVariant ) } TextButton(onClick = { onSecondClicked() showDialog.value = false }) { Text( text = secondButtonText.uppercase(), fontSize = 14.sp, color = MaterialTheme.colorScheme.primary, ) } if (thirdButtonText.isNotEmpty()) TextButton(onClick = { onThirdClicked() showDialog.value = false }) { Text( text = thirdButtonText.uppercase(), fontSize = 14.sp, color = MaterialTheme.colorScheme.primary, ) } TextButton(onClick = { onLastClicked() showDialog.value = false }) { Text( text = lastButtonText.uppercase(), fontSize = 14.sp, color = MaterialTheme.colorScheme.primary ) } } } else { // Use the existing Row for 1 or 2 buttons Row( modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.End ) { if (firstButtonText.isNotEmpty()) TextButton(onClick = { onFirstClicked() showDialog.value = false }) { Text( text = firstButtonText.uppercase(), fontSize = 14.sp, color = MaterialTheme.colorScheme.onSurfaceVariant ) } if (secondButtonText.isNotEmpty()) TextButton(onClick = { onSecondClicked() showDialog.value = false }) { Text( text = secondButtonText.uppercase(), fontSize = 14.sp, color = MaterialTheme.colorScheme.primary ) } TextButton(onClick = { onLastClicked() showDialog.value = false }) { Text( text = lastButtonText.uppercase(), fontSize = 14.sp, color = MaterialTheme.colorScheme.primary ) } } } } } } } ) } } @OptIn(ExperimentalMaterial3Api::class) @Composable fun SelectableAlertDialog( openDialog: MutableState, title: String, items: List, onItemClicked: (Int) -> Unit, neutralButtonText: String = "", onNeutralClicked: () -> Unit = {} ) { if (openDialog.value) { BasicAlertDialog( onDismissRequest = { openDialog.value = false }, content = { Card( modifier = Modifier .fillMaxWidth() .padding(top = 16.dp, start = 16.dp, end = 16.dp, bottom = 0.dp), shape = RoundedCornerShape(16.dp), colors = CardDefaults.cardColors( containerColor = MaterialTheme.colorScheme.surfaceContainer ) ) { Column(modifier = Modifier.padding(16.dp)) { Text( text = title, fontSize = 20.sp, color = MaterialTheme.colorScheme.onSurface, textAlign = TextAlign.Start, modifier = Modifier.fillMaxWidth() ) Spacer(modifier = Modifier.height(16.dp)) LazyColumn( modifier = Modifier.fillMaxWidth(), horizontalAlignment = Alignment.End ) { itemsIndexed(items) { index, item -> TextButton( onClick = { onItemClicked(index) openDialog.value = false }, modifier = Modifier.fillMaxWidth() ) { Text( text = item, color = MaterialTheme.colorScheme.onSurface, modifier = Modifier.fillMaxWidth(), textAlign = TextAlign.Start ) } } } if (neutralButtonText.isNotEmpty()) { Row( modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.End ) { TextButton( onClick = { onNeutralClicked() openDialog.value = false } ) { Text( text = neutralButtonText.uppercase(), color = MaterialTheme.colorScheme.onSurfaceVariant, ) } } } } } } ) } } @OptIn(ExperimentalMaterial3Api::class) @Composable fun PasswordDialog( ctx: Context, showPasswordDialog: MutableState, password: MutableState, keyboardController: SoftwareKeyboardController?, title: String, message: String = "", okAction: () -> Unit, cancelAction: () -> Unit ) { val showPassword = remember { mutableStateOf(false) } val focusRequester = remember { FocusRequester() } if (showPasswordDialog.value) { BasicAlertDialog( properties = DialogProperties( dismissOnBackPress = false, dismissOnClickOutside = false, ), onDismissRequest = { keyboardController?.hide() showPasswordDialog.value = false } ) { Card( modifier = Modifier .fillMaxWidth() .padding(top = 16.dp, start = 16.dp, end = 16.dp, bottom = 0.dp), shape = RoundedCornerShape(16.dp), colors = CardDefaults.cardColors( containerColor = MaterialTheme.colorScheme.surfaceVariant ) ) { Column(modifier = Modifier.padding(16.dp)) { Text( text = title, fontSize = 20.sp, modifier = Modifier.padding(top = 16.dp, bottom = 16.dp), color = MaterialTheme.colorScheme.onSurface, ) if (message.isNotEmpty()) Text( text = message, fontSize = 16.sp, modifier = Modifier.padding(16.dp), color = MaterialTheme.colorScheme.onSurfaceVariant, ) OutlinedTextField( value = password.value, singleLine = true, colors = OutlinedTextFieldDefaults.colors( cursorColor = MaterialTheme.colorScheme.primary, ), onValueChange = { password.value = it }, visualTransformation = if (showPassword.value) VisualTransformation.None else PasswordVisualTransformation(), trailingIcon = { IconButton(onClick = { showPassword.value = !showPassword.value }) { Icon( imageVector = if (showPassword.value) Icons.Filled.Visibility else Icons.Filled.VisibilityOff, contentDescription = "Visibility", tint = MaterialTheme.colorScheme.onSurfaceVariant ) } }, modifier = Modifier .fillMaxWidth() .padding(start = 4.dp, end = 4.dp, top = 12.dp, bottom = 2.dp) .focusRequester(focusRequester), textStyle = TextStyle( fontSize = 18.sp, color = MaterialTheme.colorScheme.onSurface ), keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Text) ) LaunchedEffect(key1 = Unit) { focusRequester.requestFocus() keyboardController?.show() } Row( modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.End ) { TextButton( onClick = { keyboardController?.hide() showPasswordDialog.value = false cancelAction() }, ) { Text( text = stringResource(R.string.cancel), color = MaterialTheme.colorScheme.onSurfaceVariant ) } Spacer(modifier = Modifier.width(8.dp)) TextButton( onClick = { keyboardController?.hide() showPasswordDialog.value = false password.value = password.value.trim() if (!Account.checkAuthPass(password.value)) { Toast.makeText( ctx, String.format( ctx.getString(R.string.invalid_authentication_password), password.value ), Toast.LENGTH_SHORT ).show() password.value = "" } okAction() }, ) { Text( text = stringResource(R.string.ok), color = MaterialTheme.colorScheme.primary ) } } } } } } } } ================================================ FILE: app/src/main/kotlin/com/tutpro/baresip/DraggableLazyList.kt ================================================ /* * Copyright 2024 Allan Veloso Lopes * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ // Temporarily implementation until this issue is made available on the official library // https://issuetracker.google.com/issues/356619939 // Based on https://cs.android.com/androidx/platform/frameworks/support/+/androidx-main:compose/foundation/foundation/integration-tests/foundation-demos/src/main/java/androidx/compose/foundation/demos/LazyColumnDragAndDropDemo.kt // https://issuetracker.google.com/issues/181282427 package com.tutpro.baresip import androidx.compose.animation.core.Animatable import androidx.compose.animation.core.Spring import androidx.compose.animation.core.spring import androidx.compose.foundation.gestures.detectDragGestures import androidx.compose.foundation.gestures.detectDragGesturesAfterLongPress import androidx.compose.foundation.gestures.scrollBy import androidx.compose.foundation.layout.Box import androidx.compose.foundation.lazy.LazyItemScope import androidx.compose.foundation.lazy.LazyListItemInfo import androidx.compose.foundation.lazy.LazyListScope import androidx.compose.foundation.lazy.LazyListState import androidx.compose.foundation.lazy.itemsIndexed import androidx.compose.foundation.lazy.rememberLazyListState import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableFloatStateOf import androidx.compose.runtime.mutableIntStateOf import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier import androidx.compose.ui.geometry.Offset import androidx.compose.ui.graphics.graphicsLayer import androidx.compose.ui.input.pointer.pointerInput import androidx.compose.ui.zIndex import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.channels.Channel import kotlinx.coroutines.launch /** * Creates a [DraggableListState] that is remembered across compositions. * * @param lazyListState the [LazyListState] whose items are being dragged. * @param onMove the callback that is triggered when an item is moved. */ @Composable fun rememberDraggableListState( lazyListState: LazyListState = rememberLazyListState(), onMove: (Int, Int) -> Unit ): DraggableListState { val scope = rememberCoroutineScope() val state = remember(lazyListState) { DraggableListState( listState = lazyListState, onMove = onMove, scope = scope ) } LaunchedEffect(state) { while (true) { val diff = state.scrollChannel.receive() lazyListState.scrollBy(diff) } } return state } /** * A state object that can be hoisted to control and observe dragging within a list. * * In most cases, this will be created via [rememberLazyListState]. * * @param listState the state of the list where the item is being dragged. * @param scope a coroutine scope for performing animations. * @param onMove the callback that is triggered when an item is moved. The callback is * invoked whenever the dragged item middle point passes another item top or bottom offset, * it does not depend on the drag to be completed. */ class DraggableListState internal constructor( val listState: LazyListState, private val scope: CoroutineScope, private val onMove: (Int, Int) -> Unit ) { var draggingItemIndex by mutableStateOf(null) private set internal val scrollChannel = Channel() private var draggingItemDraggedDelta by mutableFloatStateOf(0f) private var draggingItemInitialOffset by mutableIntStateOf(0) val draggingItemOffset: Float get() = draggingItemLayoutInfo?.let { item -> draggingItemInitialOffset + draggingItemDraggedDelta - item.offset } ?: 0f private val draggingItemLayoutInfo: LazyListItemInfo? get() = listState.layoutInfo.visibleItemsInfo.firstOrNull { it.index == draggingItemIndex } var previousIndexOfDraggedItem by mutableStateOf(null) private set var previousItemOffset = Animatable(0f) private set internal fun onDragStart(index: Int) { listState.layoutInfo.visibleItemsInfo .find { it.index == index } ?.also { draggingItemIndex = it.index draggingItemInitialOffset = it.offset } } internal fun onDragStart(key: Any) { listState.layoutInfo.visibleItemsInfo .find { it.key == key } ?.also { draggingItemIndex = it.index draggingItemInitialOffset = it.offset } } internal fun onDragInterrupted() { if (draggingItemIndex != null) { previousIndexOfDraggedItem = draggingItemIndex val startOffset = draggingItemOffset scope.launch { previousItemOffset.snapTo(startOffset) previousItemOffset.animateTo( 0f, spring(stiffness = Spring.StiffnessMediumLow, visibilityThreshold = 1f) ) previousIndexOfDraggedItem = null } } draggingItemDraggedDelta = 0f draggingItemIndex = null draggingItemInitialOffset = 0 } internal fun onDrag(offset: Offset) { draggingItemDraggedDelta += offset.y val draggingItem = draggingItemLayoutInfo ?: return val startOffset = draggingItem.offset + draggingItemOffset val endOffset = startOffset + draggingItem.size val middleOffset = startOffset + (endOffset - startOffset) / 2f val targetItem = listState.layoutInfo.visibleItemsInfo.find { item -> middleOffset.toInt() in item.offset..item.offsetEnd && draggingItem.index != item.index } if (targetItem != null) { if ( draggingItem.index == listState.firstVisibleItemIndex || targetItem.index == listState.firstVisibleItemIndex ) { listState.requestScrollToItem( listState.firstVisibleItemIndex, listState.firstVisibleItemScrollOffset ) } onMove(draggingItem.index, targetItem.index) draggingItemIndex = targetItem.index } else { val overscroll = when { draggingItemDraggedDelta > 0 -> (endOffset - listState.layoutInfo.viewportEndOffset).coerceAtLeast(0f) draggingItemDraggedDelta < 0 -> (startOffset - listState.layoutInfo.viewportStartOffset).coerceAtMost(0f) else -> 0f } if (overscroll != 0f) { scrollChannel.trySend(overscroll) } } } private val LazyListItemInfo.offsetEnd: Int get() = this.offset + this.size } /** * Defines the area that will act as the handle for dragging operations. * * @param state The draggable state of the list where the handler is being used. * @param index The index of the item for which the handler is being used. * @param onlyAfterLongPress Indicates whether dragging should begin only after a long press. */ fun Modifier.dragHandle( state: DraggableListState, index: Int, onlyAfterLongPress: Boolean = false ): Modifier { return pointerInput(state) { if (onlyAfterLongPress) { detectDragGesturesAfterLongPress( onDrag = { change, offset -> change.consume() state.onDrag(offset = offset) }, onDragStart = { state.onDragStart(index = index) }, onDragEnd = { state.onDragInterrupted() }, onDragCancel = { state.onDragInterrupted() } ) } else { detectDragGestures( onDrag = { change, offset -> change.consume() state.onDrag(offset = offset) }, onDragStart = { state.onDragStart(index = index) }, onDragEnd = { state.onDragInterrupted() }, onDragCancel = { state.onDragInterrupted() } ) } } } /** * Defines the area that will act as the handle for dragging operations. * * If you use this version of the [dragHandle], you must implement the key parameter on the * [draggableItems] or [draggableItemsIndexed]. * * @param state The draggable state of the list where the handler is being used. * @param key The unique identifier for the item within the list for which the handler is being used. * @param onlyAfterLongPress Indicates whether dragging should begin only after a long press. */ fun Modifier.dragHandle( state: DraggableListState, key: Any, onlyAfterLongPress: Boolean = false ): Modifier { return pointerInput(state) { if (onlyAfterLongPress) { detectDragGesturesAfterLongPress( onDrag = { change, offset -> change.consume() state.onDrag(offset = offset) }, onDragStart = { state.onDragStart(key = key) }, onDragEnd = { state.onDragInterrupted() }, onDragCancel = { state.onDragInterrupted() } ) } else { detectDragGestures( onDrag = { change, offset -> change.consume() state.onDrag(offset = offset) }, onDragStart = { state.onDragStart(key = key) }, onDragEnd = { state.onDragInterrupted() }, onDragCancel = { state.onDragInterrupted() } ) } } } /** * Adds a list of draggable items. * * To the items to be draggable, it's also necessary to add the modifier [dragHandle] to * the preferable UI element of the item which will be the target of the drag operation. * * @param items the data list * @param key a factory of stable and unique keys representing the item. Using the same key * for multiple items in the list is not allowed. Type of the key should be saveable * via Bundle on Android. If null is passed the position in the list will represent the key. * When you specify the key the scroll position will be maintained based on the key, which * means if you add/remove items before the current visible item the item with the given key * will be kept as the first visible one. This can be overridden by calling 'requestScrollToItem' * on the 'LazyListState'. * @param contentType a factory of the content types for the item. The item compositions of * the same type could be reused more efficiently. Note that null is a valid type and items of such * type will be considered compatible. * @param itemContent the content displayed by a single item */ inline fun LazyListScope.draggableItemsIndexed( state: DraggableListState, items: List, noinline key: ((index: Int, item: T) -> Any)? = null, crossinline contentType: (index: Int, item: T) -> Any? = { _, _ -> null }, crossinline itemContent: @Composable LazyItemScope.(index: Int, item: T, isDragging: Boolean) -> Unit ) = itemsIndexed( items = items, key = key, contentType = contentType ) { index, item -> val isDragging = index == state.draggingItemIndex val draggingModifier = if (isDragging) { Modifier .zIndex(1f) .graphicsLayer { translationY = state.draggingItemOffset } } else if (index == state.previousIndexOfDraggedItem) { Modifier .zIndex(1f) .graphicsLayer { translationY = state.previousItemOffset.value } } else { Modifier.animateItem(fadeInSpec = null, fadeOutSpec = null) } Box(modifier = draggingModifier) { itemContent(index, item, isDragging) } } /** * Adds a list of draggable items. * * To the items to be draggable, it's also necessary to add the modifier [dragHandle] to * the preferable UI element of the item which will be the target of the drag operation. * * @param items the data list * @param key a factory of stable and unique keys representing the item. Using the same key * for multiple items in the list is not allowed. Type of the key should be saveable * via Bundle on Android. If null is passed the position in the list will represent the key. * When you specify the key the scroll position will be maintained based on the key, which * means if you add/remove items before the current visible item the item with the given key * will be kept as the first visible one. This can be overridden by calling 'requestScrollToItem' * on the 'LazyListState'. * @param contentType a factory of the content types for the item. The item compositions of * the same type could be reused more efficiently. Note that null is a valid type and items of such * type will be considered compatible. * @param itemContent the content displayed by a single item */ inline fun LazyListScope.draggableItems( state: DraggableListState, items: List, noinline key: ((item: T) -> Any)? = null, crossinline contentType: (item: T) -> Any? = { _ -> null }, crossinline itemContent: @Composable LazyItemScope.(item: T, isDragging: Boolean) -> Unit ) = draggableItemsIndexed( state = state, items = items, key = key?.let { block -> { _, item -> block(item) } }, contentType = { _, item -> contentType(item) }, itemContent = { _, item, isDragging -> itemContent(item, isDragging) } ) ================================================ FILE: app/src/main/kotlin/com/tutpro/baresip/Event.kt ================================================ package com.tutpro.baresip import java.util.concurrent.atomic.AtomicBoolean open class Event(private val content: T) { private val hasBeenHandled = AtomicBoolean(false) fun getContentIfNotHandled(): T? { return if (hasBeenHandled.get()) { null } else { hasBeenHandled.set(true) content } } } ================================================ FILE: app/src/main/kotlin/com/tutpro/baresip/InCallService.kt ================================================ package com.tutpro.baresip import android.app.Service import android.content.Intent import android.os.IBinder // This is needed in order to allow choosing baresip as default Phone app class InCallService : Service() { override fun onBind(intent: Intent): IBinder? { Log.d(TAG, "InCallService onBind with intent: ${intent.action}") return null } } /*import android.telecom.InCallService import android.telecom.Call class InCallService : InCallService() { override fun onCallAdded(call: Call) { super.onCallAdded(call) // This is triggered when the system wants YOU to show the call UI Log.d("Baresip", "InCallService: Call added") } }*/ ================================================ FILE: app/src/main/kotlin/com/tutpro/baresip/Log.kt ================================================ package com.tutpro.baresip object Log { enum class LogLevel { DEBUG, INFO, WARN, ERROR, OFF } var logLevel: LogLevel = LogLevel.INFO fun logLevelSet(value: Int) { when (value) { 0 -> logLevel = LogLevel.DEBUG 1 -> logLevel = LogLevel.INFO 2 -> logLevel = LogLevel.WARN 3 -> logLevel = LogLevel.ERROR 4 -> logLevel = LogLevel.OFF } } fun d(tag: String, msg: String) { if (logLevel < LogLevel.INFO) android.util.Log.d(tag, msg) } fun i(tag: String, msg: String) { if (logLevel < LogLevel.WARN) android.util.Log.i(tag, msg) } fun w(tag: String, msg: String) { if (logLevel < LogLevel.ERROR) android.util.Log.w(tag, msg) } fun e(tag: String, msg: String) { if (logLevel < LogLevel.OFF) android.util.Log.w(tag, msg) } } ================================================ FILE: app/src/main/kotlin/com/tutpro/baresip/MainActivity.kt ================================================ @file:OptIn(ExperimentalMaterial3Api::class) package com.tutpro.baresip import android.Manifest.permission.BLUETOOTH_CONNECT import android.Manifest.permission.POST_NOTIFICATIONS import android.Manifest.permission.READ_EXTERNAL_STORAGE import android.Manifest.permission.RECORD_AUDIO import android.Manifest.permission.WRITE_EXTERNAL_STORAGE import android.annotation.SuppressLint import android.app.KeyguardManager import android.app.NotificationManager import android.content.BroadcastReceiver import android.content.Context import android.content.Intent import android.content.Intent.ACTION_CALL import android.content.Intent.ACTION_DIAL import android.content.Intent.ACTION_VIEW import android.content.IntentFilter import android.media.AudioManager import android.os.Build import android.os.Bundle import android.view.KeyEvent import android.view.WindowManager import android.view.inputmethod.InputMethodManager import androidx.activity.ComponentActivity import androidx.activity.compose.setContent import androidx.activity.result.ActivityResultLauncher import androidx.activity.result.contract.ActivityResultContracts import androidx.activity.viewModels import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.runtime.LaunchedEffect import androidx.lifecycle.Observer import androidx.navigation.NavHostController import androidx.navigation.compose.NavHost import androidx.navigation.compose.rememberNavController import androidx.core.content.ContextCompat import kotlin.system.exitProcess class MainActivity : ComponentActivity() { private lateinit var imm: InputMethodManager private lateinit var nm: NotificationManager private lateinit var am: AudioManager private lateinit var kgm: KeyguardManager private lateinit var screenEventReceiver: BroadcastReceiver private lateinit var serviceEventObserver: Observer> private lateinit var requestPermissionLauncher: ActivityResultLauncher private lateinit var requestPermissionsLauncher: ActivityResultLauncher> private lateinit var comDevChangedListener: AudioManager.OnCommunicationDeviceChangedListener private lateinit var baresipService: Intent private var restart = false private var atStartup = false private var initialized = false private val viewModel: ViewModel by viewModels() private lateinit var navController: NavHostController @SuppressLint("ClickableViewAccessibility") override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) val extraAction = intent.getStringExtra("action") Log.i(TAG, "Main onCreate ${intent.action}/${intent.data}/$extraAction") window.addFlags(WindowManager.LayoutParams.FLAG_IGNORE_CHEEK_PRESSES) BaresipService.darkTheme.value = Utils.isThemeDark(this) // Must be done after view has been created this.setShowWhenLocked(true) this.setTurnScreenOn( true) Utils.requestDismissKeyguard(this) imm = getSystemService(INPUT_METHOD_SERVICE) as InputMethodManager nm = getSystemService(NOTIFICATION_SERVICE) as NotificationManager am = getSystemService(AUDIO_SERVICE) as AudioManager kgm = getSystemService(KEYGUARD_SERVICE) as KeyguardManager serviceEventObserver = Observer { val event = it.getContentIfNotHandled() Log.d(TAG, "Observed event $event") if (event != null && BaresipService.serviceEvents.isNotEmpty()) { val first = BaresipService.serviceEvents.removeAt(0) if (taskId != -1) { if (first.event == "started" && !initialized) // Android has restarted baresip when permission has been denied in app settings recreate() else { if (first.event == "stopped") { Log.d( TAG, "Handling service event 'stopped' with start error '${first.params[0]}'" ) if (first.params[0] != "") handleDialog( ctx = applicationContext, title = getString(R.string.notice), message =getString(R.string.start_failed) ) else { finishAndRemoveTask() if (restart) reStart() else exitProcess(0) } } else handleServiceEvent(this, viewModel, first.event, first.params) } } else Log.d(TAG, "Omit service event '$event' for task -1") } } BaresipService.serviceEvent.observeForever(serviceEventObserver) screenEventReceiver = object : BroadcastReceiver() { override fun onReceive(contxt: Context, intent: Intent) { if (kgm.isKeyguardLocked) { Log.d(TAG, "Screen on when locked") this@MainActivity.setShowWhenLocked(Call.inCall()) } } } this.registerReceiver(screenEventReceiver, IntentFilter().apply { addAction(Intent.ACTION_SCREEN_ON) }) if (Build.VERSION.SDK_INT >= 31) { comDevChangedListener = AudioManager.OnCommunicationDeviceChangedListener { device -> if (device != null) { Log.d(TAG, "Com device changed to type ${device.type} in mode ${am.mode}") } } am.addOnCommunicationDeviceChangedListener(mainExecutor, comDevChangedListener) } initialized = true val restartApp = { Log.i(TAG, "Restarting baresip") window.setFlags(WindowManager.LayoutParams.FLAG_NOT_TOUCHABLE, WindowManager.LayoutParams.FLAG_NOT_TOUCHABLE) if (BaresipService.isServiceRunning) { restart = true baresipService.action = "Stop" ContextCompat.startForegroundService(this, baresipService) } else { finishAndRemoveTask() val pm = applicationContext.packageManager val intent = pm.getLaunchIntentForPackage(applicationContext.packageName) if (intent != null) { applicationContext.startActivity(intent) exitProcess(0) } else { Log.e(TAG, "Failed to restart: Launch intent is null") } } } val quitApp = { Log.i(TAG, "Quiting baresip") window.setFlags(WindowManager.LayoutParams.FLAG_NOT_TOUCHABLE, WindowManager.LayoutParams.FLAG_NOT_TOUCHABLE) if (BaresipService.isServiceRunning) { restart = false baresipService.action = "Stop" ContextCompat.startForegroundService(this, baresipService) } else { finishAndRemoveTask() exitProcess(0) } } baresipService = Intent(this@MainActivity, BaresipService::class.java) atStartup = intent.hasExtra("onStartup") when (intent?.action) { ACTION_DIAL, ACTION_CALL, ACTION_VIEW -> if (BaresipService.isServiceRunning) callAction(applicationContext, viewModel, intent.data, if (intent?.action == ACTION_CALL) "call" else "dial") else BaresipService.callActionUri = intent.data.toString().replace("%2B", "+") .replace("%20", "").filterNot{setOf('-', ' ', '(', ')').contains(it)} } val permissions = if (Build.VERSION.SDK_INT >= 33) arrayOf(POST_NOTIFICATIONS, RECORD_AUDIO, BLUETOOTH_CONNECT) else if (Build.VERSION.SDK_INT >= 31) arrayOf(RECORD_AUDIO, BLUETOOTH_CONNECT) else if (Build.VERSION.SDK_INT < 29) arrayOf(RECORD_AUDIO, READ_EXTERNAL_STORAGE, WRITE_EXTERNAL_STORAGE) else arrayOf(RECORD_AUDIO) requestPermissionLauncher = registerForActivityResult(ActivityResultContracts.RequestPermission()) { isGranted -> Log.i(TAG, "Permission granted: $isGranted") } requestPermissionsLauncher = registerForActivityResult(ActivityResultContracts.RequestMultiplePermissions()) { val denied = mutableListOf() val shouldShow = mutableListOf() it.forEach { permission -> if (!permission.value) { denied.add(permission.key) if (shouldShowRequestPermissionRationale(permission.key)) shouldShow.add(permission.key) } } if (denied.contains(POST_NOTIFICATIONS) && !shouldShow.contains(POST_NOTIFICATIONS)) { handleDialog( ctx = applicationContext, title = getString(R.string.notice), message = getString(R.string.no_notifications), action = { quitRestart(false) } ) } else { if (shouldShow.isNotEmpty()) { handleDialog( ctx = applicationContext, title = getString(R.string.permissions_rationale), message = getString(R.string.audio_permissions), action = { requestPermissionsLauncher.launch(permissions) } ) } else { if (!BaresipService.isStartReceived) { baresipService.action = "Start" ContextCompat.startForegroundService(this, baresipService) if (atStartup) moveTaskToBack(true) } } } } setContent { AppTheme { navController = rememberNavController() LaunchedEffect(key1 = viewModel) { viewModel.navigationCommand.collect { command -> Log.d(TAG, "MainActivity: Received NavigationCommand: $command") when (command) { is NavigationCommand.NavigateToChat -> { val route = "chat/${command.aor}/${command.peerUri}" navController.navigate(route) } is NavigationCommand.NavigateToCalls -> { val route = "calls/${command.aor}" navController.navigate(route) } is NavigationCommand.NavigateToHome -> { navController.navigate("main") } } } } NavHost(navController, startDestination = "main") { mainScreenRoute( navController = navController, viewModel = viewModel, onRequestPermissions = { requestPermissionsLauncher.launch(permissions) }, onRestartApp = { restartApp() }, onQuitApp = { quitApp() } ) aboutScreenRoute(navController) settingsScreenRoute( navController = navController, onRestartApp = { restartApp() } ) accountsScreenRoute(navController) audioScreenRoute(navController) accountScreenRoute(navController) codecsScreenRoute(navController) contactsScreenRoute(navController, viewModel) baresipContactScreenRoute(navController) androidContactScreenRoute(navController, viewModel) callsScreenRoute(navController, viewModel) callDetailsScreenRoute(navController, viewModel) blockedScreenRoute(navController) chatsScreenRoute(navController) chatScreenRoute(navController, viewModel) } } } } // OnCreate override fun onStart() { super.onStart() Log.i(TAG, "Main onStart") val action = intent.getStringExtra("action") if (action != null) { // MainActivity was not visible when call, message, or transfer request came in intent.removeExtra("action") handleIntent(applicationContext, viewModel, intent, action) } } override fun onResume() { super.onResume() Log.d(TAG, "Main onResume") nm.cancelAll() } override fun onPause() { super.onPause() Log.d(TAG, "Main onPause") } override fun onDestroy() { Log.d(TAG, "Main onDestroy") this.unregisterReceiver(screenEventReceiver) if (Build.VERSION.SDK_INT >= 31) am.removeOnCommunicationDeviceChangedListener(comDevChangedListener) BaresipService.serviceEvent.removeObserver(serviceEventObserver) BaresipService.serviceEvents.clear() super.onDestroy() } override fun onNewIntent(intent: Intent) { // Called when MainActivity already exists at the top of current task super.onNewIntent(intent) this.setShowWhenLocked(true) this.setTurnScreenOn(true) Log.d(TAG, "onNewIntent with action/data '${intent.action}/${intent.data}'") when (intent.action) { ACTION_DIAL, ACTION_CALL, ACTION_VIEW -> callAction( applicationContext, viewModel, intent.data, if (intent.action == ACTION_CALL) "call" else "dial" ) else -> { val action = intent.getStringExtra("action") if (action != null) { intent.removeExtra("action") handleIntent(applicationContext, viewModel, intent, action) } } } } override fun onKeyDown(keyCode: Int, event: KeyEvent?): Boolean { val stream = if (am.mode == AudioManager.MODE_RINGTONE) AudioManager.STREAM_RING else AudioManager.STREAM_VOICE_CALL when (keyCode) { KeyEvent.KEYCODE_VOLUME_DOWN, KeyEvent.KEYCODE_VOLUME_UP -> { am.adjustStreamVolume(stream, if (keyCode == KeyEvent.KEYCODE_VOLUME_DOWN) AudioManager.ADJUST_LOWER else AudioManager.ADJUST_RAISE, AudioManager.FLAG_SHOW_UI) Log.d(TAG, "Adjusted volume $keyCode of stream $stream to ${am.getStreamVolume(stream)}") return true } } return super.onKeyDown(keyCode, event) } private fun quitRestart(reStart: Boolean) { Log.i(TAG, "quitRestart Restart = $reStart") window.setFlags(WindowManager.LayoutParams.FLAG_NOT_TOUCHABLE, WindowManager.LayoutParams.FLAG_NOT_TOUCHABLE) if (BaresipService.isServiceRunning) { restart = reStart baresipService.action = "Stop" ContextCompat.startForegroundService(this, baresipService) } else { finishAndRemoveTask() if (reStart) quitRestart(true) else exitProcess(0) } } private fun reStart() { Log.d(TAG, "Trigger restart") val pm = applicationContext.packageManager val intent = pm.getLaunchIntentForPackage(this.packageName) this.startActivity(intent) exitProcess(0) } } ================================================ FILE: app/src/main/kotlin/com/tutpro/baresip/MainScreen.kt ================================================ package com.tutpro.baresip import android.Manifest.permission.READ_EXTERNAL_STORAGE import android.Manifest.permission.RECORD_AUDIO import android.Manifest.permission.WRITE_EXTERNAL_STORAGE import android.app.Activity import android.app.Activity.RESULT_OK import android.app.KeyguardManager import android.content.Context import android.content.Intent import android.content.res.Configuration import android.media.AudioManager import android.net.Uri import android.os.Build import android.os.Handler import android.os.Looper import android.os.Process import android.os.SystemClock import android.provider.DocumentsContract import android.provider.MediaStore import android.widget.Toast import androidx.activity.compose.rememberLauncherForActivityResult import androidx.activity.result.contract.ActivityResultContracts import androidx.appcompat.app.AppCompatDelegate import androidx.compose.animation.animateContentSize import androidx.compose.foundation.BorderStroke import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.foundation.background import androidx.compose.foundation.clickable import androidx.compose.foundation.combinedClickable import androidx.compose.foundation.gestures.detectHorizontalDragGestures import androidx.compose.foundation.gestures.detectTapGestures import androidx.compose.foundation.interaction.MutableInteractionSource import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.WindowInsets import androidx.compose.foundation.layout.asPaddingValues import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.heightIn import androidx.compose.foundation.layout.imePadding import androidx.compose.foundation.layout.navigationBarsPadding import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.statusBars import androidx.compose.foundation.layout.width import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.items import androidx.compose.foundation.lazy.rememberLazyListState import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.shape.CircleShape import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.text.BasicTextField import androidx.compose.foundation.text.KeyboardOptions import androidx.compose.foundation.verticalScroll import androidx.compose.material.icons.Icons import androidx.compose.material.icons.automirrored.filled.Chat import androidx.compose.material.icons.filled.AddIcCall import androidx.compose.material.icons.filled.Call import androidx.compose.material.icons.filled.CallEnd import androidx.compose.material.icons.filled.Dialpad import androidx.compose.material.icons.filled.History import androidx.compose.material.icons.filled.KeyboardArrowDown import androidx.compose.material.icons.filled.KeyboardArrowUp import androidx.compose.material.icons.filled.Lock import androidx.compose.material.icons.filled.LockOpen import androidx.compose.material.icons.filled.Menu import androidx.compose.material.icons.filled.Mic import androidx.compose.material.icons.filled.MicOff import androidx.compose.material.icons.filled.Person import androidx.compose.material.icons.filled.RecordVoiceOver import androidx.compose.material.icons.filled.SpeakerPhone import androidx.compose.material.icons.filled.VoiceOverOff import androidx.compose.material.icons.filled.Voicemail import androidx.compose.material.icons.outlined.ArrowCircleRight import androidx.compose.material.icons.outlined.Clear import androidx.compose.material.icons.outlined.Info import androidx.compose.material.icons.outlined.PauseCircle import androidx.compose.material3.BasicAlertDialog import androidx.compose.material3.ButtonColors import androidx.compose.material3.Card import androidx.compose.material3.CardDefaults import androidx.compose.material3.DropdownMenuItem import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.HorizontalDivider import androidx.compose.material3.Icon import androidx.compose.material3.IconButton import androidx.compose.material3.MaterialTheme import androidx.compose.material3.OutlinedButton import androidx.compose.material3.OutlinedTextField import androidx.compose.material3.OutlinedTextFieldDefaults import androidx.compose.material3.Scaffold import androidx.compose.material3.Switch import androidx.compose.material3.Text import androidx.compose.material3.TextButton import androidx.compose.material3.TopAppBar import androidx.compose.material3.TopAppBarDefaults import androidx.compose.material3.pulltorefresh.PullToRefreshDefaults.Indicator import androidx.compose.material3.pulltorefresh.pullToRefresh import androidx.compose.material3.pulltorefresh.rememberPullToRefreshState import androidx.compose.runtime.Composable import androidx.compose.runtime.DisposableEffect import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue import androidx.compose.runtime.key import androidx.compose.runtime.mutableFloatStateOf import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip import androidx.compose.ui.draw.shadow import androidx.compose.ui.focus.FocusRequester import androidx.compose.ui.focus.focusRequester import androidx.compose.ui.focus.onFocusChanged import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.SolidColor import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.input.pointer.pointerInput import androidx.compose.ui.platform.LocalConfiguration import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalSoftwareKeyboardController import androidx.compose.ui.res.colorResource import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.vectorResource import androidx.compose.ui.text.AnnotatedString import androidx.compose.ui.text.TextStyle import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.input.KeyboardType import androidx.compose.ui.text.input.VisualTransformation import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import androidx.core.content.ContextCompat import androidx.core.net.toUri import androidx.lifecycle.Lifecycle import androidx.lifecycle.LifecycleEventObserver import androidx.lifecycle.compose.LocalLifecycleOwner import androidx.navigation.NavController import androidx.navigation.NavGraphBuilder import androidx.navigation.compose.composable import androidx.navigation.compose.currentBackStackEntryAsState import com.tutpro.baresip.BaresipService.Companion.contactNames import com.tutpro.baresip.BaresipService.Companion.uas import com.tutpro.baresip.BaresipService.Companion.uasStatus import com.tutpro.baresip.CustomElements.AlertDialog import com.tutpro.baresip.CustomElements.DropdownMenu import com.tutpro.baresip.CustomElements.PasswordDialog import com.tutpro.baresip.CustomElements.SelectableAlertDialog import com.tutpro.baresip.CustomElements.verticalScrollbar import kotlinx.coroutines.delay import java.io.File import java.text.SimpleDateFormat import java.util.Date import java.util.Locale private val dialpadButtonEnabled = mutableStateOf(true) private var pullToRefreshEnabled = mutableStateOf(true) private var downloadsInputUri: Uri? = null private var downloadsOutputUri: Uri? = null private val passwordTitle = mutableStateOf("") private val showPasswordDialog = mutableStateOf(false) private val showPasswordsDialog = mutableStateOf(false) private var passwordAccounts = mutableListOf() private var password = mutableStateOf("") private val selectItems = mutableStateOf(listOf()) private val selectItemAction = mutableStateOf<(Int) -> Unit>({ _ -> run {} }) private val showSelectItemDialog = mutableStateOf(false) fun NavGraphBuilder.mainScreenRoute( navController: NavController, viewModel: ViewModel, onRequestPermissions: () -> Unit, onRestartApp: () -> Unit, onQuitApp: () -> Unit ) { composable("main") { MainScreen( navController = navController, viewModel = viewModel, onRequestPermissions = onRequestPermissions, onRestartClick = onRestartApp, onQuitClick = onQuitApp ) } } @Composable private fun MainScreen( navController: NavController, viewModel: ViewModel, onRequestPermissions: () -> Unit, onRestartClick: () -> Unit, onQuitClick: () -> Unit ) { val ctx = LocalContext.current val lifecycleOwner = LocalLifecycleOwner.current val configuration = LocalConfiguration.current val keyboardController = LocalSoftwareKeyboardController.current val ua = uas.value.find { it.account.aor == viewModel.selectedAor.value } val call = ua?.currentCall() val showKeyboard by viewModel.showKeyboard.collectAsState() val hideKeyboard by viewModel.hideKeyboard.collectAsState() LaunchedEffect(showKeyboard) { if (viewModel.showKeyboard.value > 0) keyboardController?.show() } LaunchedEffect(hideKeyboard) { if (viewModel.hideKeyboard.value > 0) keyboardController?.hide() } DisposableEffect(lifecycleOwner) { val observer = LifecycleEventObserver { _, event -> when (event) { Lifecycle.Event.ON_RESUME -> { Log.d(TAG, "Resumed to MainScreen") BaresipService.isMainVisible = true viewModel.updateCalls(Call.calls().toList()) (Call.call("incoming") ?: Call.calls().lastOrNull())?.let { spinToAor(viewModel, it.ua.account.aor) } ?: run { if (uas.value.isNotEmpty() && viewModel.selectedAor.value == "") spinToAor(viewModel, uas.value.first().account.aor) } val ua = UserAgent.ofAor(viewModel.selectedAor.value) if (ua != null) { showCall(ctx, viewModel, ua) viewModel.triggerAccountUpdate() } } Lifecycle.Event.ON_PAUSE -> { Log.d(TAG, "Paused from MainScreen") BaresipService.isMainVisible = false } else -> {} } } lifecycleOwner.lifecycle.addObserver(observer) onDispose { Log.d(TAG, "onDispose for MainScreen") lifecycleOwner.lifecycle.removeObserver(observer) BaresipService.isMainVisible = false } } val encryptPasswordTitle = stringResource(R.string.encrypt_password) val backupRequestLauncher = rememberLauncherForActivityResult( contract = ActivityResultContracts.StartActivityForResult() ) { result -> if (result.resultCode == RESULT_OK) { result.data?.data?.also { uri -> downloadsOutputUri = uri passwordTitle.value = encryptPasswordTitle showPasswordDialog.value = true } } } val noticeTitle = stringResource(R.string.notice) val noBackupMessage = stringResource(R.string.no_backup) fun launchBackupRequest() { if (Build.VERSION.SDK_INT < 29) { if (!Utils.checkPermissions(ctx, arrayOf(WRITE_EXTERNAL_STORAGE))) { alertTitle.value = noticeTitle alertMessage.value = noBackupMessage showAlert.value = true } else { val path = Utils.downloadsPath("baresip.bs") downloadsOutputUri = File(path).toUri() passwordTitle.value = encryptPasswordTitle showPasswordDialog.value = true } } else { val intent = Intent(Intent.ACTION_CREATE_DOCUMENT).apply { addCategory(Intent.CATEGORY_OPENABLE) type = "application/octet-stream" putExtra( Intent.EXTRA_TITLE, "baresip_" + SimpleDateFormat( "yyyy_MM_dd_HH_mm_ss", Locale.getDefault() ).format(Date()) ) putExtra(DocumentsContract.EXTRA_INITIAL_URI, MediaStore.Downloads.EXTERNAL_CONTENT_URI) } backupRequestLauncher.launch(intent) } } val decryptPasswordTitle = stringResource(R.string.decrypt_password) val restoreRequestLauncher = rememberLauncherForActivityResult( contract = ActivityResultContracts.StartActivityForResult() ) { result -> if (result.resultCode == RESULT_OK) { result.data?.data?.also { uri -> downloadsInputUri = uri passwordTitle.value = decryptPasswordTitle showPasswordDialog.value = true } } } val noRestoreMessage = stringResource(R.string.no_restore) fun launchRestoreRequest() { if (Build.VERSION.SDK_INT < 29) { if (!Utils.checkPermissions(ctx, arrayOf(READ_EXTERNAL_STORAGE))) { alertTitle.value = noticeTitle alertMessage.value = noRestoreMessage showAlert.value = true } else { val path = Utils.downloadsPath("baresip.bs") downloadsInputUri = File(path).toUri() passwordTitle.value = decryptPasswordTitle showPasswordDialog.value = true } } else { val intent = Intent(Intent.ACTION_OPEN_DOCUMENT).apply { addCategory(Intent.CATEGORY_OPENABLE) type = "application/octet-stream" putExtra(DocumentsContract.EXTRA_INITIAL_URI, MediaStore.Downloads.EXTERNAL_CONTENT_URI) } restoreRequestLauncher.launch(intent) } } val logcatRequestLauncher = rememberLauncherForActivityResult( contract = ActivityResultContracts.StartActivityForResult() ) { result -> if (result.resultCode == RESULT_OK) result.data?.data?.also { uri -> try { val out = ctx.contentResolver.openOutputStream(uri) val process = Runtime.getRuntime().exec("logcat -d --pid=${Process.myPid()}") val bufferedReader = process.inputStream.bufferedReader() bufferedReader.forEachLine { line -> out!!.write(line.toByteArray()) out.write('\n'.code.toByte().toInt()) } out!!.close() } catch (e: Exception) { Log.e(TAG, "Failed to write logcat to file: $e") } } } fun launchLogcatRequest() { if (Build.VERSION.SDK_INT >= 29) { val intent = Intent(Intent.ACTION_CREATE_DOCUMENT).apply { addCategory(Intent.CATEGORY_OPENABLE) type = "text/plain" putExtra( Intent.EXTRA_TITLE, "baresip_logcat_" + SimpleDateFormat( "yyyy_MM_dd_HH_mm_ss", Locale.getDefault() ).format(Date()) ) putExtra(DocumentsContract.EXTRA_INITIAL_URI, MediaStore.Downloads.EXTERNAL_CONTENT_URI) } logcatRequestLauncher.launch(intent) } } LaunchedEffect(key1 = call?.status, key2 = configuration.orientation) { val isConnected = call != null && call.status.value == "connected" && !call.held if (isConnected) { if (configuration.orientation == Configuration.ORIENTATION_PORTRAIT) { call.focusDtmf.value = true delay(300) keyboardController?.show() } else keyboardController?.hide() } } if (showPasswordDialog.value) PasswordDialog( ctx = ctx, showPasswordDialog = showPasswordDialog, password = password, keyboardController = keyboardController, title = passwordTitle.value, okAction = { if (password.value != "") { if (passwordTitle.value == encryptPasswordTitle) backup(ctx, password.value) else restore(ctx, password.value, onRestartClick) password.value = "" } }, cancelAction = { if (downloadsOutputUri != null) { Utils.deleteFile(ctx, downloadsOutputUri!!) } } ) if (showPasswordsDialog.value) { if (passwordAccounts.isNotEmpty()) { val account = passwordAccounts.removeAt(0) val params = account.substringAfter(">") if (Utils.paramValue(params, "auth_user") != "" && Utils.paramValue(params, "auth_pass") == "") { val aor = account.substringAfter("<").substringBefore(">") PasswordDialog( ctx = ctx, showPasswordDialog = showPasswordsDialog, password = password, keyboardController = keyboardController, title = stringResource(R.string.authentication_password), message = stringResource(R.string.account) + " " + Utils.plainAor(aor), okAction = { if (password.value != "") BaresipService.aorPasswords[aor] = password.value showPasswordsDialog.value = true }, cancelAction = { showPasswordsDialog.value = true } ) } else { showPasswordsDialog.value = false showPasswordsDialog.value = true } } else onRequestPermissions() } LaunchedEffect(Unit) { if (!BaresipService.isServiceRunning) { val path = ctx.filesDir.absolutePath + "/accounts" if (File(path).exists()) { passwordAccounts = String( Utils.getFileContents(path)!!, Charsets.UTF_8 ).lines().toMutableList() showPasswordsDialog.value = true } else { // Baresip is started for the first time onRequestPermissions() } } } val navBackStackEntry by navController.currentBackStackEntryAsState() val currentRoute = navBackStackEntry?.destination?.route LaunchedEffect(currentRoute, viewModel.selectedAor.collectAsState()) { if (currentRoute == "main") { Log.d(TAG, "Updating icons for AOR: ${viewModel.selectedAor.value}") viewModel.triggerAccountUpdate() } } Scaffold( modifier = Modifier .fillMaxSize() .imePadding(), containerColor = MaterialTheme.colorScheme.background, topBar = { Column( modifier = Modifier .fillMaxWidth() .background(MaterialTheme.colorScheme.background) .padding( top = WindowInsets.statusBars.asPaddingValues().calculateTopPadding() ) ) { TopAppBar( viewModel = viewModel, navController = navController, onBackupClick = { launchBackupRequest() }, onRestoreClick = { launchRestoreRequest() }, onLogcatClick = { launchLogcatRequest() }, onRestartClick = onRestartClick, onQuitClick = onQuitClick ) } }, bottomBar = { BottomBar(ctx, viewModel, navController) }, content = { contentPadding -> MainContent(navController, viewModel, contentPadding) } ) } @OptIn(ExperimentalMaterial3Api::class) @Composable private fun TopAppBar( viewModel: ViewModel, navController: NavController, onBackupClick: () -> Unit, onRestoreClick: () -> Unit, onLogcatClick: () -> Unit, onRestartClick: () -> Unit, onQuitClick: () -> Unit ) { val ctx = LocalContext.current val currentMicIcon by viewModel.micIcon.collectAsState() val am = ctx.getSystemService(Context.AUDIO_SERVICE) as AudioManager val recOffImage = Icons.Filled.VoiceOverOff val recOnImage = Icons.Filled.RecordVoiceOver var isRecOn by remember { mutableStateOf(BaresipService.isRecOn) } val isSpeakerOn by viewModel.isSpeakerOn.collectAsState() var menuExpanded by remember { mutableStateOf(false) } val about = stringResource(R.string.about) val settings = stringResource(R.string.configuration) val accounts = stringResource(R.string.accounts) val backup = stringResource(R.string.backup) val restore = stringResource(R.string.restore) val logcat = stringResource(R.string.logcat) val restart = stringResource(R.string.restart) val quit = stringResource(R.string.quit) TopAppBar( title = { Text( text = stringResource(R.string.baresip), fontSize = 22.sp, fontWeight = FontWeight.Bold ) }, colors = TopAppBarDefaults.topAppBarColors( containerColor = MaterialTheme.colorScheme.primary, titleContentColor = MaterialTheme.colorScheme.onPrimary, actionIconContentColor = MaterialTheme.colorScheme.onPrimary ), windowInsets = WindowInsets(0, 0, 0, 0), actions = { Spacer(modifier = Modifier.width(8.dp)) val callRecordingTitle = stringResource(R.string.call_recording_title) val callRecordingMessage = stringResource(R.string.call_recording_tip) Box(contentAlignment = Alignment.Center, modifier = Modifier .size(48.dp) .clip(CircleShape) .combinedClickable( onClick = { if (Call.call("connected") == null) { BaresipService.isRecOn = !BaresipService.isRecOn if (BaresipService.isRecOn) { Api.module_load("sndfile") } else { Api.module_unload("sndfile") } isRecOn = BaresipService.isRecOn } else { Toast.makeText(ctx, R.string.rec_in_call, Toast.LENGTH_SHORT).show() } }, onLongClick = { alertTitle.value = callRecordingTitle alertMessage.value = callRecordingMessage showAlert.value = true } ) ) { Icon( imageVector = if (isRecOn) recOnImage else recOffImage, contentDescription = null, tint = if (isRecOn) MaterialTheme.colorScheme.error else MaterialTheme.colorScheme.onPrimary, modifier = Modifier.size(40.dp) ) } Spacer(modifier = Modifier.width(22.dp)) val microPhoneTitle = stringResource(R.string.microphone_title) val microPhoneMessage = stringResource(R.string.microphone_tip) Box( contentAlignment = Alignment.Center, modifier = Modifier .size(48.dp) .clip(CircleShape) .combinedClickable( onClick = { if (Call.call("connected") != null) { BaresipService.isMicMuted = !BaresipService.isMicMuted if (BaresipService.isMicMuted) { viewModel.updateMicIcon(Icons.Filled.MicOff) Api.calls_mute(true) } else { viewModel.updateMicIcon(Icons.Filled.Mic) Api.calls_mute(false) } } }, onLongClick = { alertTitle.value = microPhoneTitle alertMessage.value = microPhoneMessage showAlert.value = true } ) ) { Icon( imageVector = currentMicIcon, contentDescription = null, tint = if (BaresipService.isMicMuted) MaterialTheme.colorScheme.error else MaterialTheme.colorScheme.onPrimary, modifier = Modifier.size(40.dp) ) } Spacer(modifier = Modifier.width(16.dp)) val speakerPhoneTitle = stringResource(R.string.speakerphone_title) val speakerPhoneMessage = stringResource(R.string.speakerphone_tip) Box( contentAlignment = Alignment.Center, modifier = Modifier .size(48.dp) .clip(CircleShape) .combinedClickable( onClick = { val isCurrentlyOn = isSpeakerOn val aor = viewModel.selectedAor.value val ua = uas.value.find { it.account.aor == aor } val call = ua?.currentCall() val connection = if (call != null) ConnectionService.connections[call.callp] else null if (connection != null) { @Suppress("DEPRECATION") connection.setAudioRoute( if (isCurrentlyOn) android.telecom.CallAudioState.ROUTE_EARPIECE else android.telecom.CallAudioState.ROUTE_SPEAKER ) } else { Utils.toggleSpeakerPhone(ContextCompat.getMainExecutor(ctx), am) } }, onLongClick = { alertTitle.value = speakerPhoneTitle alertMessage.value = speakerPhoneMessage showAlert.value = true } ) ) { Icon( imageVector = Icons.Filled.SpeakerPhone, contentDescription = null, tint = if (isSpeakerOn) MaterialTheme.colorScheme.error else MaterialTheme.colorScheme.onPrimary, modifier = Modifier.size(40.dp) ) } IconButton( onClick = { menuExpanded = !menuExpanded } ) { Icon( imageVector = Icons.Filled.Menu, contentDescription = "Menu", tint = MaterialTheme.colorScheme.onPrimary ) } DropdownMenu( expanded = menuExpanded, onDismissRequest = { menuExpanded = false }, items = if (Build.VERSION.SDK_INT >= 29) listOf(about, settings, accounts, backup, restore, logcat, restart, quit) else listOf(about, settings, accounts, backup, restore, restart, quit), onItemClick = { selectedItem -> menuExpanded = false when (selectedItem) { about -> { navController.navigate("about") } settings -> { navController.navigate("settings") } accounts -> { navController.navigate("accounts") } backup -> onBackupClick() restore -> onRestoreClick() logcat -> onLogcatClick() restart -> onRestartClick() quit -> onQuitClick() } } ) } ) } @Composable private fun BottomBar(ctx: Context, viewModel: ViewModel, navController: NavController) { val aor by viewModel.selectedAor.collectAsState() val accountUpdate by viewModel.accountUpdate.collectAsState() val showVmIcon = remember(aor, accountUpdate) { if (aor.isNotEmpty()) Account.ofAor(aor)?.vmUri?.isNotEmpty() ?: false else false } val hasNewVoicemail = remember(aor, accountUpdate) { if (aor.isNotEmpty()) (Account.ofAor(aor)?.vmNew ?: 0) > 0 else false } val hasUnreadMessages = remember(aor, accountUpdate) { if (aor.isNotEmpty()) Account.ofAor(aor)?.unreadMessages ?: false else false } val hasMissedCalls = remember(aor, accountUpdate) { if (aor.isNotEmpty()) Account.ofAor(aor)?.missedCalls ?: false else false } val isDialpadVisible by viewModel.isDialpadVisible.collectAsState() val buttonSize = 48.dp Row( modifier = Modifier .fillMaxWidth() .navigationBarsPadding() .padding(bottom = 16.dp), horizontalArrangement = Arrangement.SpaceEvenly, verticalAlignment = Alignment.CenterVertically ) { if (showVmIcon) IconButton( // Disable the button if no account is selected enabled = aor.isNotEmpty(), onClick = { val ua = UserAgent.ofAor(aor)!! val acc = ua.account if (acc.vmUri.isNotEmpty()) { dialogTitle.value = ctx.getString(R.string.voicemail_messages) dialogMessage.value = acc.vmMessages(ctx) firstText.value = ctx.getString(R.string.cancel) onFirstClicked.value = {} secondText.value = "" lastText.value = ctx.getString(R.string.listen) onLastClicked.value = { val intent = Intent(ctx, MainActivity::class.java) intent.putExtra("uap", ua.uap) intent.putExtra("peer", acc.vmUri) handleIntent(ctx, viewModel, intent, "call") } showDialog.value = true } }, modifier = Modifier .weight(1f) .size(buttonSize) ) { Icon( imageVector = Icons.Filled.Voicemail, contentDescription = null, Modifier.size(buttonSize), tint = if (hasNewVoicemail) MaterialTheme.colorScheme.error else MaterialTheme.colorScheme.secondary ) } IconButton( onClick = { navController.navigate("contacts") }, modifier = Modifier .weight(1f) .size(buttonSize) ) { Icon( imageVector = Icons.Filled.Person, contentDescription = null, Modifier.size(buttonSize), tint = MaterialTheme.colorScheme.secondary ) } IconButton( enabled = aor.isNotEmpty(), onClick = { navController.navigate("chats/$aor") }, modifier = Modifier .weight(1f) .size(buttonSize) ) { Icon( imageVector = Icons.AutoMirrored.Filled.Chat, contentDescription = null, Modifier.size(buttonSize), tint = if (hasUnreadMessages) MaterialTheme.colorScheme.error else MaterialTheme.colorScheme.secondary ) } IconButton( enabled = aor.isNotEmpty(), onClick = { navController.navigate("calls/$aor") }, modifier = Modifier .weight(1f) .size(buttonSize) ) { Icon( imageVector = Icons.Filled.History, contentDescription = null, Modifier.size(buttonSize), tint = if (hasMissedCalls) MaterialTheme.colorScheme.error else MaterialTheme.colorScheme.secondary ) } IconButton( onClick = { viewModel.toggleDialpadVisibility() }, modifier = Modifier .weight(1f) .size(buttonSize), enabled = dialpadButtonEnabled.value ) { Icon( imageVector = Icons.Filled.Dialpad, contentDescription = null, modifier = Modifier.size(buttonSize), tint = if (isDialpadVisible) MaterialTheme.colorScheme.error else MaterialTheme.colorScheme.secondary ) } } } private val alertTitle = mutableStateOf("") private val alertMessage = mutableStateOf("") private val showAlert = mutableStateOf(false) private val dialogTitle = mutableStateOf("") private val dialogMessage = mutableStateOf("") private val firstText = mutableStateOf("") private val onFirstClicked = mutableStateOf({}) private val secondText = mutableStateOf("") private val onSecondClicked = mutableStateOf({}) private val lastText = mutableStateOf("") private val onLastClicked = mutableStateOf({}) private val showDialog = mutableStateOf(false) @Composable private fun CallCard( ctx: Context, viewModel: ViewModel, call: Call?, dialerState: ViewModel.DialerState? ) { Column { CallUriRow(ctx, viewModel, call, dialerState) CallRow(ctx, viewModel, call, dialerState) if (call != null && call.showOnHoldNotice.value) OnHoldNotice() } } @OptIn(ExperimentalMaterial3Api::class) @Composable private fun MainContent(navController: NavController, viewModel: ViewModel, contentPadding: PaddingValues) { var isRefreshing by remember { mutableStateOf(false) } val refreshState = rememberPullToRefreshState() var offset by remember { mutableFloatStateOf(0f) } val swipeThreshold = 200 val ctx = LocalContext.current val calls by viewModel.calls.collectAsState() val selectedAor by viewModel.selectedAor.collectAsState() val aorCalls = calls.filter { it.ua.account.aor == selectedAor } val hasActiveCalls = aorCalls.any { !it.callOnHold.value } val conferenceCall = aorCalls.any { it.conferenceCall } LaunchedEffect(isRefreshing) { if (isRefreshing) { delay(1000) isRefreshing = false } } if (showAlert.value) AlertDialog( showDialog = showAlert, title = alertTitle.value, message = alertMessage.value, lastButtonText = stringResource(R.string.ok), ) if (showDialog.value) AlertDialog( showDialog = showDialog, title = stringResource(R.string.confirmation), message = dialogMessage.value, firstButtonText = firstText.value, onFirstClicked = onFirstClicked.value, secondButtonText = secondText.value, onSecondClicked = onSecondClicked.value, lastButtonText = lastText.value, onLastClicked = onLastClicked.value, ) SelectableAlertDialog( openDialog = showSelectItemDialog, title = stringResource(R.string.choose_destination_uri), items = selectItems.value, onItemClicked = selectItemAction.value, neutralButtonText = stringResource(R.string.cancel), onNeutralClicked = {} ) Column( modifier = Modifier .fillMaxWidth() .padding(contentPadding) .padding(top = 18.dp, bottom = 6.dp, start = 16.dp, end = 16.dp) .fillMaxSize() .pullToRefresh( state = refreshState, isRefreshing = isRefreshing, onRefresh = { isRefreshing = true if (uas.value.isNotEmpty()) { if (viewModel.selectedAor.value == "") spinToAor(viewModel, uas.value.first().account.aor) val ua = UserAgent.ofAor(viewModel.selectedAor.value)!! if (ua.account.regint > 0) Api.ua_register(ua.uap) } }, enabled = pullToRefreshEnabled.value, ) .pointerInput(Unit) { detectHorizontalDragGestures( onDragStart = { offset = 0f }, onDragEnd = { if (offset < -swipeThreshold) { if (uas.value.isNotEmpty()) { val curPos = UserAgent.findAorIndex(viewModel.selectedAor.value) val newPos = if (curPos == null) 0 else (curPos + 1) % uas.value.size if (curPos != newPos) { val ua = uas.value[newPos] spinToAor(viewModel, ua.account.aor) showCall(ctx, viewModel, ua) } } } else if (offset > swipeThreshold) { if (uas.value.isNotEmpty()) { val curPos = UserAgent.findAorIndex(viewModel.selectedAor.value) val newPos = when (curPos) { null -> 0 0 -> uas.value.size - 1 else -> curPos - 1 } if (curPos != newPos) { val ua = uas.value[newPos] spinToAor(viewModel, ua.account.aor) showCall(ctx, viewModel, ua) } } } } ) { _, dragAmount -> offset += dragAmount } } .verticalScroll(rememberScrollState()), verticalArrangement = Arrangement.Top, horizontalAlignment = Alignment.CenterHorizontally, ) { AccountSpinner(ctx, viewModel, navController) aorCalls.forEach { call -> key(call.callp) { CallCard(ctx = ctx, viewModel = viewModel, call = call, dialerState = null) } } if (!hasActiveCalls || conferenceCall) CallCard(ctx = ctx, viewModel = viewModel, call = null, dialerState = viewModel.dialerState) Indicator( modifier = Modifier.align(Alignment.CenterHorizontally), isRefreshing = isRefreshing, state = refreshState, ) } } @OptIn(ExperimentalFoundationApi::class) @Composable private fun AccountSpinner(ctx: Context, viewModel: ViewModel, navController: NavController) { var expanded by rememberSaveable { mutableStateOf(false) } val selected: String by viewModel.selectedAor.collectAsState() if (uas.value.isEmpty()) viewModel.updateSelectedAor("") else if (selected == "" || UserAgent.ofAor(selected) == null) { viewModel.updateSelectedAor(uas.value.first().account.aor) } showCall(ctx, viewModel, UserAgent.ofAor(selected)) viewModel.triggerAccountUpdate() if (selected == "") { OutlinedButton( onClick = { navController.navigate("accounts") }, modifier = Modifier .padding(horizontal = 4.dp) .height(50.dp) .fillMaxWidth(), colors = ButtonColors( containerColor = MaterialTheme.colorScheme.surfaceVariant, contentColor = MaterialTheme.colorScheme.onSurfaceVariant, disabledContainerColor = MaterialTheme.colorScheme.surfaceVariant, disabledContentColor = MaterialTheme.colorScheme.onSurfaceVariant ), shape = RoundedCornerShape(12.dp), contentPadding = PaddingValues(horizontal = 10.dp) ) { Text(text = "") } } else OutlinedButton( onClick = { expanded = !expanded }, enabled = true, modifier = Modifier .padding(horizontal = 4.dp) .height(50.dp) .pointerInput(Unit) { detectTapGestures( onPress = { expanded = true }, onLongPress = { val ua = UserAgent.ofAor(selected) if (ua != null) { val acc = ua.account if (Api.account_regint(acc.accp) > 0) { Api.account_set_regint(acc.accp, 0) Api.ua_unregister(ua.uap) } else { Api.account_set_regint( acc.accp, acc.configuredRegInt ) Api.ua_register(ua.uap) } acc.regint = Api.account_regint(acc.accp) Account.saveAccounts() } } ) }, colors = ButtonColors( containerColor = MaterialTheme.colorScheme.surfaceVariant, contentColor = MaterialTheme.colorScheme.onSurfaceVariant, disabledContainerColor = MaterialTheme.colorScheme.surfaceVariant, disabledContentColor = MaterialTheme.colorScheme.onSurfaceVariant ), shape = RoundedCornerShape(12.dp), contentPadding = PaddingValues(horizontal = 10.dp) ) { Icon( imageVector = ImageVector.vectorResource( uasStatus.value[selected] ?: R.drawable.circle_yellow ), contentDescription = null, tint = Color.Unspecified, modifier = Modifier .padding(end = 10.dp) .clickable(onClick = { navController.navigate("account/$selected/old") }) ) Text( text = Account.ofAor(selected)?.text() ?: "", fontSize = 17.sp, fontWeight = FontWeight.Bold, overflow = TextOverflow.Ellipsis, maxLines = 1, modifier = Modifier .weight(1f) .combinedClickable( onClick = { expanded = true }, onLongClick = { val ua = UserAgent.ofAor(selected) if (ua != null) { val acc = ua.account if (Api.account_regint(acc.accp) > 0) { Api.account_set_regint(acc.accp, 0) Api.ua_unregister(ua.uap) } else { Api.account_set_regint( acc.accp, acc.configuredRegInt ) Api.ua_register(ua.uap) } acc.regint = Api.account_regint(acc.accp) Account.saveAccounts() } } ) ) Icon( imageVector = if (expanded) Icons.Filled.KeyboardArrowUp else Icons.Filled.KeyboardArrowDown, contentDescription = null ) androidx.compose.material3.DropdownMenu( expanded = expanded, onDismissRequest = { expanded = false }, containerColor = MaterialTheme.colorScheme.surfaceContainer, ) { uas.value.forEachIndexed { index, ua -> val acc = ua.account DropdownMenuItem( onClick = { expanded = false viewModel.updateSelectedAor(acc.aor) showCall(ctx, viewModel, ua) viewModel.triggerAccountUpdate() }, text = { Text( text = acc.text(), fontSize = 17.sp, fontWeight = FontWeight.Bold ) }, leadingIcon = { Icon( imageVector = ImageVector.vectorResource(uasStatus.value[acc.aor]!!), contentDescription = null, tint = Color.Unspecified, ) } ) if (index < uas.value.size - 1) { HorizontalDivider(color = MaterialTheme.colorScheme.outlineVariant) } } } } } @Composable private fun CallUriRow( ctx: Context, viewModel: ViewModel, call: Call?, dialerState: ViewModel.DialerState? ) { val isDialer = dialerState != null val suggestions by remember { contactNames } var filteredSuggestions by remember { mutableStateOf>(emptyList()) } val focusRequester = remember { FocusRequester() } val lazyListState = rememberLazyListState() val isDialpadVisible by viewModel.isDialpadVisible.collectAsState() Row( modifier = Modifier .fillMaxWidth() .padding(top = 4.dp, bottom = 8.dp), verticalAlignment = Alignment.CenterVertically ) { Column( modifier = Modifier.weight(1f), horizontalAlignment = Alignment.CenterHorizontally ) { OutlinedTextField( value = if (isDialer) dialerState.callUri.value else call!!.callUri.value, readOnly = if (isDialer) !dialerState.callUriEnabled.value else !call!!.callUriEnabled.value, singleLine = true, onValueChange = { if (isDialer) { if (it != dialerState.callUri.value) { dialerState.callUri.value = it if (it == "") { dialerState.showCallButton.value = true dialerState.showCallConferenceButton.value = true } val normalizedInput = Utils.unaccent(it) filteredSuggestions = suggestions .filter { suggestion -> it.length > 1 && Utils.unaccent(suggestion) .contains(normalizedInput, ignoreCase = true) } .map { suggestion -> Utils.buildAnnotatedStringWithHighlight(suggestion, it) } dialerState.showSuggestions.value = it.length > 1 } } }, trailingIcon = { if (isDialer && dialerState.callUriEnabled.value && dialerState.callUri.value.isNotEmpty()) Icon(Icons.Outlined.Clear, contentDescription = null, modifier = Modifier.clickable { if (dialerState.showSuggestions.value) dialerState.showSuggestions.value = false dialerState.callUri.value = "" dialerState.showCallButton.value = true dialerState.showCallConferenceButton.value = true }, tint = MaterialTheme.colorScheme.onSurfaceVariant ) }, modifier = Modifier .fillMaxWidth() .padding(start = 4.dp, end = 4.dp, top = 12.dp, bottom = 2.dp) .focusRequester(focusRequester) .onFocusChanged { if (isDialer) { val account = Account.ofAor(viewModel.selectedAor.value) if (account != null && account.numericKeypad) if (!isDialpadVisible) viewModel.toggleDialpadVisibility() } }, label = { Text( text = if (isDialer) dialerState.callUriLabel.value else call!!.callUriLabel.value, fontSize = 18.sp ) }, textStyle = TextStyle(fontSize = 18.sp), keyboardOptions = if (isDialpadVisible) KeyboardOptions(keyboardType = KeyboardType.Phone) else KeyboardOptions(keyboardType = KeyboardType.Text) ) Spacer(modifier = Modifier.height(8.dp)) Column( modifier = Modifier .fillMaxWidth() .shadow(8.dp, RoundedCornerShape(8.dp)) .background( color = MaterialTheme.colorScheme.surfaceVariant, shape = RoundedCornerShape(8.dp) ) .animateContentSize() ) { if (isDialer && dialerState.showSuggestions.value && filteredSuggestions.isNotEmpty()) { Box( modifier = Modifier .fillMaxWidth() .heightIn(max = 150.dp) ) { LazyColumn( modifier = Modifier .fillMaxWidth() .verticalScrollbar(state = lazyListState, width = 6.dp), horizontalAlignment = Alignment.Start, state = lazyListState ) { items( items = filteredSuggestions, key = { suggestion -> suggestion.toString() } ) { suggestion -> Box( modifier = Modifier .fillMaxWidth() .clickable { dialerState.callUri.value = suggestion.toString() dialerState.showSuggestions.value = false } .padding(12.dp) ) { Text( text = suggestion, modifier = Modifier.fillMaxWidth(), color = MaterialTheme.colorScheme.onSurfaceVariant, fontSize = 18.sp ) } } } } } } } if (call != null && call.showCallTimer.value) { CallTimer( initialDurationSeconds = call.callDuration.toLong(), modifier = Modifier.padding( start = 6.dp, top = 6.dp, end = if (call.securityIconTint.value != -1) 6.dp else 0.dp ) ) } if (call != null && call.securityIconTint.value != -1) Box( modifier = Modifier .padding(top = 4.dp) .size(32.dp) .clip(CircleShape) .clickable { when (call.securityIconTint.value) { R.color.colorTrafficRed -> { alertTitle.value = ctx.getString(R.string.alert) alertMessage.value = ctx.getString(R.string.call_not_secure) showAlert.value = true } R.color.colorTrafficYellow -> { alertTitle.value = ctx.getString(R.string.alert) alertMessage.value = ctx.getString(R.string.peer_not_verified) showAlert.value = true } R.color.colorTrafficGreen -> { dialogTitle.value = ctx.getString(R.string.info) dialogMessage.value = ctx.getString(R.string.call_is_secure) firstText.value = ctx.getString(R.string.cancel) onFirstClicked.value = {} secondText.value = "" lastText.value = ctx.getString(R.string.unverify) onLastClicked.value = { if (Api.cmd_exec("zrtp_unverify " + call.zid) != 0) Log.e( TAG, "Command 'zrtp_unverify ${call.zid}' failed" ) else call.securityIconTint.value = R.color.colorTrafficYellow } showDialog.value = true } } }, contentAlignment = Alignment.Center ) { Icon( imageVector = if (call.securityIconTint.value == R.color.colorTrafficRed) Icons.Filled.LockOpen else Icons.Filled.Lock, contentDescription = null, modifier = Modifier.size(28.dp), tint = colorResource(call.securityIconTint.value) ) } } } @Composable private fun CallTimer( initialDurationSeconds: Long, modifier: Modifier = Modifier ) { val startTime = remember(initialDurationSeconds) { SystemClock.elapsedRealtime() - (initialDurationSeconds * 1000L) } var timeText by remember { mutableStateOf("") } LaunchedEffect(startTime) { while (true) { val now = SystemClock.elapsedRealtime() val elapsedMillis = now - startTime val seconds = if (elapsedMillis > 0) elapsedMillis / 1000 else 0 timeText = android.text.format.DateUtils.formatElapsedTime(seconds) delay(1000L) } } Text( text = timeText, fontSize = 16.sp, color = MaterialTheme.colorScheme.onBackground, modifier = modifier ) } @OptIn(ExperimentalMaterial3Api::class) @Composable private fun CallRow( ctx: Context, viewModel: ViewModel, call: Call?, dialerState: ViewModel.DialerState? ) { val isDialer = dialerState != null val isDialpadVisible by viewModel.isDialpadVisible.collectAsState() Row( modifier = Modifier .fillMaxWidth(), verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.Absolute.SpaceBetween ) { if (isDialer) { if (dialerState.showCallButton.value) IconButton( modifier = Modifier.size(48.dp), enabled = dialerState.callButtonsEnabled.value, onClick = { if (!dialerState.callButtonsEnabled.value) return@IconButton dialerState.showCallConferenceButton.value = false dialerState.showSuggestions.value = false callClick(ctx, viewModel, dialerState) }, ) { Icon( imageVector = Icons.Filled.Call, modifier = Modifier.size(42.dp), tint = colorResource(if (dialerState.callButtonsEnabled.value) R.color.colorTrafficGreen else R.color.colorTrafficYellow), contentDescription = null, ) } if (dialerState.showCallConferenceButton.value) { Spacer(modifier = Modifier.weight(1f, true)) IconButton( modifier = Modifier.size(48.dp), enabled = dialerState.callButtonsEnabled.value, onClick = { if (!dialerState.callButtonsEnabled.value) return@IconButton dialerState.showCallButton.value = false dialerState.showSuggestions.value = false callClick(ctx, viewModel, dialerState) } ) { Icon( imageVector = Icons.Filled.AddIcCall, modifier = Modifier.size(42.dp), tint = colorResource( if (dialerState.callButtonsEnabled.value) R.color.colorTrafficGreen else R.color.colorTrafficYellow ), contentDescription = null, ) } } } else { if (call!!.showCancelButton.value) { if (!call.conferenceCall) Spacer(modifier = Modifier.weight(1f)) IconButton( modifier = Modifier.size(48.dp), enabled = !call.terminated.value, onClick = { if (call.terminated.value) return@IconButton call.terminated.value = true val connection = ConnectionService.connections[call.callp] if (connection != null) connection.onDisconnect() else { Log.d(TAG, "AoR ${call.ua.account.aor} canceling call ${call.callp}") Api.ua_hangup(call.ua.uap, call.callp, 487, "Request Terminated") } }, ) { Icon( imageVector = Icons.Filled.CallEnd, modifier = Modifier.size(42.dp), tint = colorResource(R.color.colorTrafficRed), contentDescription = null, ) } Spacer(modifier = Modifier.width(12.dp)) } if (call.showHangupButton.value) { IconButton( modifier = Modifier.size(48.dp), enabled = !call.terminated.value, onClick = { if (call.terminated.value) return@IconButton call.terminated.value = true val connection = ConnectionService.connections[call.callp] if (connection != null) connection.onDisconnect() else { Log.d(TAG, "AoR ${call.ua.account.aor} hanging up call ${call.callp}") Api.ua_hangup(call.ua.uap, call.callp, 487, "Request Terminated") } } ) { Icon( imageVector = Icons.Filled.CallEnd, modifier = Modifier.size(42.dp), tint = colorResource(R.color.colorTrafficRed), contentDescription = null, ) } if (!call.conferenceCall) IconButton( modifier = Modifier.size(48.dp), onClick = { if (call.callOnHold.value) { Log.d(TAG, "User requested resume for ${call.callp}") call.resume() // This now automatically holds other calls } else { Log.d(TAG, "User requested hold for ${call.callp}") call.hold() } }, ) { Icon( imageVector = Icons.Outlined.PauseCircle, modifier = Modifier.size(42.dp), tint = if (call.callOnHold.value) MaterialTheme.colorScheme.error else MaterialTheme.colorScheme.secondary, contentDescription = null, ) } var showTransferDialog by remember { mutableStateOf(false) } if (!call.conferenceCall) IconButton( modifier = Modifier.size(48.dp), enabled = call.transferButtonEnabled.value, onClick = { if (call.onHoldCall != null) { if (!Api.call_supported(call.callp, Api.REPLACES)) { alertTitle.value = ctx.getString(R.string.notice) alertMessage.value = ctx.getString(R.string.replaces_not_supported) showAlert.value = true } else { val connection = ConnectionService.connections[call.callp] connection?.onHold() if (!call.executeTransfer()) { alertTitle.value = ctx.getString(R.string.notice) alertMessage.value = ctx.getString(R.string.transfer_failed) showAlert.value = true } } } else showTransferDialog = true }, ) { Icon( imageVector = Icons.Outlined.ArrowCircleRight, modifier = Modifier.size(42.dp), tint = if (call.callTransfer.value) MaterialTheme.colorScheme.error else MaterialTheme.colorScheme.secondary, contentDescription = null, ) } if (showTransferDialog) { val showDialog = remember { mutableStateOf(true) } val blindChecked = remember { mutableStateOf(true) } if (showDialog.value) BasicAlertDialog( onDismissRequest = { viewModel.requestHideKeyboard() showDialog.value = false showTransferDialog = false } ) { Card( modifier = Modifier .fillMaxWidth() .padding(top = 16.dp, start = 16.dp, end = 16.dp, bottom = 0.dp), shape = RoundedCornerShape(16.dp), colors = CardDefaults.cardColors( containerColor = MaterialTheme.colorScheme.surfaceContainerHigh ) ) { Column(modifier = Modifier.padding(16.dp)) { Text( text = stringResource(R.string.call_transfer), fontSize = 20.sp, color = MaterialTheme.colorScheme.onSurface, ) var transferUri by remember { mutableStateOf("") } val suggestions by remember { contactNames } var filteredSuggestions by remember { mutableStateOf>(emptyList()) } val focusRequester = remember { FocusRequester() } val lazyListState = rememberLazyListState() OutlinedTextField( value = transferUri, singleLine = true, onValueChange = { if (it != transferUri) { transferUri = it if (it.length > 1) { val normalizedInput = Utils.unaccent(it) filteredSuggestions = suggestions.filter { suggestion -> Utils.unaccent(suggestion) .contains(normalizedInput, ignoreCase = true) } .map { suggestion -> Utils.buildAnnotatedStringWithHighlight(suggestion, it) } } call.showSuggestions.value = transferUri.length > 1 } }, trailingIcon = { if (transferUri.isNotEmpty()) Icon( Icons.Outlined.Clear, contentDescription = null, modifier = Modifier.clickable { if (call.showSuggestions.value) call.showSuggestions.value = false else transferUri = "" }, tint = MaterialTheme.colorScheme.onSurfaceVariant ) }, modifier = Modifier .fillMaxWidth() .padding( start = 4.dp, end = 4.dp, top = 12.dp, bottom = 2.dp ) .focusRequester(focusRequester), label = { Text(stringResource(R.string.transfer_destination)) }, textStyle = TextStyle(fontSize = 18.sp), keyboardOptions = if (isDialpadVisible) KeyboardOptions(keyboardType = KeyboardType.Phone) else KeyboardOptions(keyboardType = KeyboardType.Text) ) Spacer(modifier = Modifier.height(8.dp)) Column( modifier = Modifier .fillMaxWidth() .shadow(8.dp, RoundedCornerShape(8.dp)) .background( color = MaterialTheme.colorScheme.surfaceVariant, shape = RoundedCornerShape(8.dp) ) .animateContentSize() ) { if (call.showSuggestions.value && filteredSuggestions.isNotEmpty()) { Box(modifier = Modifier .fillMaxWidth() .heightIn(max = 150.dp)) { LazyColumn( modifier = Modifier .fillMaxWidth() .verticalScrollbar( state = lazyListState, width = 6.dp, ), horizontalAlignment = Alignment.Start, state = lazyListState, ) { items( items = filteredSuggestions, key = { suggestion -> suggestion.toString() } ) { suggestion -> Box( modifier = Modifier .fillMaxWidth() .clickable { transferUri = suggestion.toString() call.showSuggestions.value = false } .padding(12.dp) ) { Text( text = suggestion, modifier = Modifier.fillMaxWidth(), color = MaterialTheme.colorScheme.onSurfaceVariant, fontSize = 18.sp ) } } } } } } if (call.replaces()) Row( modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.Start, ) { Row(verticalAlignment = Alignment.CenterVertically) { Text( text = stringResource(R.string.blind), color = MaterialTheme.colorScheme.onSurfaceVariant, modifier = Modifier.padding(8.dp), ) Switch( checked = blindChecked.value, onCheckedChange = { blindChecked.value = true } ) } Spacer(modifier = Modifier.width(8.dp)) Row(verticalAlignment = Alignment.CenterVertically) { Text( text = stringResource(R.string.attended), color = MaterialTheme.colorScheme.onSurfaceVariant, modifier = Modifier.padding(8.dp), ) Switch( checked = !blindChecked.value, onCheckedChange = { blindChecked.value = false } ) } } Row( modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.End, verticalAlignment = Alignment.CenterVertically ) { TextButton( onClick = { viewModel.requestHideKeyboard() showDialog.value = false showTransferDialog = false }, modifier = Modifier.padding(end = 32.dp), ) { Text( text = stringResource(R.string.cancel), color = MaterialTheme.colorScheme.onSurfaceVariant ) } TextButton( onClick = { call.showSuggestions.value = false var uriText = transferUri.trim() if (uriText.isNotEmpty()) { val uris = Contact.contactUris(uriText) if (uris.size > 1) { selectItems.value = uris selectItemAction.value = { index -> val uri = uris[index] transfer( ctx, viewModel, call.ua, if (Utils.isTelNumber(uri)) "tel:$uri" else uri, !blindChecked.value ) showSelectItemDialog.value = false } showSelectItemDialog.value = true } else { if (uris.size == 1) uriText = uris[0] transfer( ctx, viewModel, call.ua, if (Utils.isTelNumber(uriText)) "tel:$uriText" else uriText, !blindChecked.value ) } viewModel.requestHideKeyboard() showDialog.value = false showTransferDialog = false } }, modifier = Modifier.padding(end = 16.dp), ) { Text( text = stringResource( if (blindChecked.value) R.string.transfer else R.string.call ).uppercase(), color = MaterialTheme.colorScheme.primary ) } } } } } } val focusRequester = remember { FocusRequester() } val shouldRequestFocus by call.focusDtmf val interactionSource = remember { MutableInteractionSource() } BasicTextField( value = call.dtmfText.value, onValueChange = { newText -> if (newText.length > call.dtmfText.value.length) { val char = newText.last() if (char.isDigit() || char == '*' || char == '#') { Log.d(TAG, "Got DTMF digit '$char'") call.sendDigit(char) } } call.dtmfText.value = newText }, modifier = Modifier .width(80.dp) .focusRequester(focusRequester), enabled = call.dtmfEnabled.value, textStyle = TextStyle( fontSize = 16.sp, color = MaterialTheme.colorScheme.onSurface ), keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Phone), cursorBrush = SolidColor(MaterialTheme.colorScheme.primary), singleLine = true, interactionSource = interactionSource, decorationBox = { innerTextField -> OutlinedTextFieldDefaults.DecorationBox( value = call.dtmfText.value, visualTransformation = VisualTransformation.None, innerTextField = innerTextField, singleLine = true, enabled = call.dtmfEnabled.value, interactionSource = interactionSource, label = { Text( stringResource(R.string.dtmf), style = TextStyle(fontSize = 12.sp) ) }, contentPadding = PaddingValues( start = 4.dp, end = 4.dp, top = 8.dp, bottom = 8.dp ), colors = OutlinedTextFieldDefaults.colors( focusedContainerColor = Color.Transparent, unfocusedContainerColor = Color.Transparent, disabledContainerColor = Color.Transparent, focusedBorderColor = MaterialTheme.colorScheme.primary, unfocusedBorderColor = MaterialTheme.colorScheme.onSurfaceVariant, ) ) } ) LaunchedEffect(shouldRequestFocus) { if (shouldRequestFocus) { focusRequester.requestFocus() call.focusDtmf.value = false } } IconButton( modifier = Modifier.size(48.dp), onClick = { val stats = call.stats("audio") if (stats.isNotEmpty() && call.startTime != null) { val parts = ArrayList(stats.split(",")) if (parts[2] == "0/0") { parts[2] = "?/?" parts[3] = "?/?" parts[4] = "?/?" } val codecs = call.audioCodecs().split(',') val duration = call.duration() val txCodec = codecs[0].split("/") val rxCodec = codecs[1].split("/") alertTitle.value = ctx.getString(R.string.call_info) alertMessage.value = "${String.format(ctx.getString(R.string.duration), duration)}\n" + "${ctx.getString(R.string.codecs)}: ${txCodec[0]} ch ${txCodec[2]}/${rxCodec[0]} ch ${rxCodec[2]}\n" + "${String.format(ctx.getString(R.string.rate), parts[0])}\n" + "${String.format(ctx.getString(R.string.average_rate), parts[1])}\n" + "${ctx.getString(R.string.packets)}: ${parts[2]}\n" + "${ctx.getString(R.string.lost)}: ${parts[3]}\n" + String.format(ctx.getString(R.string.jitter), parts[4]) showAlert.value = true } else { alertTitle.value = ctx.getString(R.string.call_info) alertMessage.value = ctx.getString(R.string.call_info_not_available) showAlert.value = true } }, ) { Icon( imageVector = Icons.Outlined.Info, modifier = Modifier.size(36.dp), tint = MaterialTheme.colorScheme.secondary, contentDescription = null, ) } } if (call.showAnswerRejectButtons.value) { IconButton( modifier = Modifier.size(48.dp), onClick = { answer(ctx, call) }, ) { Icon( imageVector = Icons.Filled.Call, modifier = Modifier.size(42.dp), tint = colorResource(R.color.colorTrafficGreen), contentDescription = null, ) } Spacer(Modifier.weight(1f)) IconButton( modifier = Modifier.size(48.dp), onClick = { reject(call) }, ) { Icon( imageVector = Icons.Filled.CallEnd, modifier = Modifier.size(42.dp), tint = colorResource(R.color.colorTrafficRed), contentDescription = null, ) } } } } } @Composable private fun OnHoldNotice() { Box(modifier = Modifier.fillMaxWidth(), contentAlignment = Alignment.Center) { OutlinedButton( onClick = {}, border = BorderStroke(1.dp, MaterialTheme.colorScheme.error), modifier = Modifier.padding(16.dp), shape = RoundedCornerShape(20) ) { Text( text = stringResource(R.string.call_is_on_hold), fontSize = 18.sp ) } } } private fun spinToAor(viewModel: ViewModel, aor: String) { if (aor != viewModel.selectedAor.value) viewModel.updateSelectedAor(aor) viewModel.triggerAccountUpdate() } private fun callClick(ctx: Context, viewModel: ViewModel, dialerState: ViewModel.DialerState?) { if (viewModel.selectedAor.value != "") { if (Utils.checkPermissions(ctx, arrayOf(RECORD_AUDIO))) { if (dialerState != null) { val uriText = dialerState.callUri.value.trim() if (uriText.isNotEmpty()) { val uris = Contact.contactUris(uriText) if (uris.isEmpty()) makeCall(ctx, viewModel, uriText, dialerState.showCallConferenceButton.value) else if (uris.size == 1) makeCall(ctx, viewModel, uris[0], dialerState.showCallConferenceButton.value) else { selectItems.value = uris selectItemAction.value = { index -> makeCall(ctx, viewModel, uris[index], dialerState.showCallConferenceButton.value) } showSelectItemDialog.value = true } } else { val ua = UserAgent.ofAor(viewModel.selectedAor.value)!! val latestPeerUri = CallHistoryNew.aorLatestPeerUri(ua.account.aor) if (latestPeerUri != null) dialerState.callUri.value = Utils.friendlyUri(ctx, latestPeerUri, ua.account) } } } else Toast.makeText(ctx, R.string.no_calls, Toast.LENGTH_SHORT).show() } } private fun makeCall(ctx: Context, viewModel: ViewModel, uriText: String, conferenceCall: Boolean, onHoldCallp: Long = 0L) { val aor = viewModel.selectedAor.value val ua = UserAgent.ofAor(aor)!! val peerUri = if (Utils.isTelNumber(uriText)) "tel:$uriText" else uriText val uri = if (Utils.isTelUri(peerUri)) { if (ua.account.telProvider == "") { alertTitle.value = ctx.getString(R.string.notice) alertMessage.value = String.format(ctx.getString(R.string.no_telephony_provider), aor) showAlert.value = true return } Utils.telToSip(peerUri, ua.account) } else Utils.uriComplete(peerUri, aor) if (!Utils.checkUri(uri)) { alertTitle.value = ctx.getString(R.string.notice) alertMessage.value = String.format(ctx.getString(R.string.invalid_sip_or_tel_uri), uri) showAlert.value = true } else if (Utils.isPSTNCallActive(ctx) || Call.calls().any { it.ua.account.aor != ua.account.aor } ) Toast.makeText(ctx, R.string.call_already_active, Toast.LENGTH_SHORT).show() else { val tm = ctx.getSystemService(Context.TELECOM_SERVICE) as android.telecom.TelecomManager val extras = android.os.Bundle() extras.putParcelable(android.telecom.TelecomManager.EXTRA_PHONE_ACCOUNT_HANDLE, BaresipService.getPhoneAccountHandle(ctx)) val callExtras = android.os.Bundle() callExtras.putBoolean("conferenceCall", conferenceCall) callExtras.putLong("uap", ua.uap) if (onHoldCallp != 0L) callExtras.putLong("onHoldCallp", onHoldCallp) extras.putBundle(android.telecom.TelecomManager.EXTRA_OUTGOING_CALL_EXTRAS, callExtras) try { Log.d(TAG, "Placing Telecom call to $uri with uap=${ua.uap}") tm.placeCall(uri.toUri(), extras) } catch (e: SecurityException) { Log.e(TAG, "placeCall failed: ${e.message}") } } } private fun answer(ctx: Context, call: Call) { Log.d(TAG, "AoR ${call.ua.account.aor} answering call from ${call.callUri.value}") ConnectionService.connections[call.callp]?.setActive() val intent = Intent(ctx, BaresipService::class.java) intent.action = "Call Answer" intent.putExtra("uap", call.ua.uap) intent.putExtra("callp", call.callp) ContextCompat.startForegroundService(ctx, intent) } private fun reject(call: Call) { Log.d(TAG, "AoR ${call.ua.account.aor} rejecting call ${call.callp} from ${call.callUri.value}") val connection = ConnectionService.connections[call.callp] if (connection != null) connection.onReject() else { call.rejected = true Api.ua_hangup(call.ua.uap, call.callp, 486, "Busy Here") } } private fun transfer(ctx: Context, viewModel: ViewModel, ua: UserAgent, uriText: String, attended: Boolean) { val uri = if (Utils.isTelUri(uriText)) Utils.telToSip(uriText, ua.account) else Utils.uriComplete(uriText, ua.account.aor) if (!Utils.checkUri(uri)) { alertTitle.value = ctx.getString(R.string.notice) alertMessage.value = String.format(ctx.getString(R.string.invalid_sip_or_tel_uri), uri) showAlert.value = true } else { val call = ua.currentCall() if (call != null) { if (attended) { val connection = ConnectionService.connections[call.callp] val success = if (connection != null) { connection.onHold() true } else { call.hold() } if (success) { call.onhold = true call.referTo = uri makeCall(ctx, viewModel, uri, false, call.callp) showCall(ctx, viewModel, ua, call) } } else { val connection = ConnectionService.connections[call.callp] connection?.onHold() if (!call.transfer(uri)) { alertTitle.value = ctx.getString(R.string.notice) alertMessage.value = ctx.getString(R.string.transfer_failed) showAlert.value = true } } showCall(ctx, viewModel, ua) } } } private fun showCall(ctx: Context, viewModel: ViewModel, ua: UserAgent?, showCall: Call? = null) { if (ua == null) return val call = showCall ?: ua.currentCall() if (call == null) { pullToRefreshEnabled.value = true viewModel.dialerState.callUri.value = ua.account.resumeUri viewModel.dialerState.callUriLabel.value = ctx.getString(R.string.outgoing_call_to_dots) viewModel.dialerState.callUriEnabled.value = true viewModel.dialerState.showCallButton.value = true viewModel.dialerState.showCallConferenceButton.value = true viewModel.dialerState.callButtonsEnabled.value = true viewModel.dialerState.showSuggestions.value = false dialpadButtonEnabled.value = true if (BaresipService.isMicMuted) { BaresipService.isMicMuted = false viewModel.updateMicIcon(Icons.Filled.Mic) } } else { viewModel.dialerState.callUri.value = "" pullToRefreshEnabled.value = false call.callUriEnabled.value = false val isLandscape = ctx.resources.configuration.orientation == Configuration.ORIENTATION_LANDSCAPE if (isLandscape || call.held || call.status.value != "connected") { call.focusDtmf.value = false call.dtmfEnabled.value = !call.held Handler(Looper.getMainLooper()).postDelayed({ viewModel.requestHideKeyboard() }, 25) } else { call.dtmfEnabled.value = true call.focusDtmf.value = true viewModel.requestShowKeyboard() } Log.d(TAG, "Showing call ${call.callp} from ${call.ua.account.aor} with status ${call.status.value}") when (call.status.value) { "outgoing", "transferring", "answered" -> { call.callUriLabel.value = if (call.status.value == "answered") ctx.getString(R.string.incoming_call_from_dots) else ctx.getString(R.string.outgoing_call_to_dots) call.callUri.value = Utils.friendlyUri(ctx, call.peerUri, ua.account) call.showCallTimer.value = false call.securityIconTint.value = -1 call.showCallButton.value = false call.showCancelButton.value = call.status.value == "outgoing" call.showHangupButton.value = !call.showCancelButton.value call.showAnswerRejectButtons.value = false call.showOnHoldNotice.value = false dialpadButtonEnabled.value = false } "incoming" -> { call.showCallTimer.value = false call.securityIconTint.value = -1 val uri = call.diverterUri() if (uri != "") { call.callUriLabel.value = ctx.getString(R.string.diverted_by_dots) call.callUri.value = Utils.friendlyUri(ctx, uri, ua.account) } else { call.callUriLabel.value = ctx.getString(R.string.incoming_call_from_dots) call.callUri.value = Utils.friendlyUri(ctx, call.peerUri, ua.account) } call.showCallButton.value = false call.showCancelButton.value = false call.showHangupButton.value = false call.showAnswerRejectButtons.value = true call.showOnHoldNotice.value = false dialpadButtonEnabled.value = false } "connected" -> { if (call.referTo != "") { call.callUriLabel.value = ctx.getString(R.string.outgoing_call_to_dots) call.callUri.value = Utils.friendlyUri(ctx, call.referTo, ua.account) call.transferButtonEnabled.value = false } else { if (call.dir == "out") { call.callUriLabel.value = ctx.getString(R.string.outgoing_call_to_dots) call.callUri.value = Utils.friendlyUri(ctx, call.peerUri, ua.account) } else { call.callUriLabel.value = ctx.getString(R.string.incoming_call_from_dots) call.callUri.value = Utils.friendlyUri(ctx, call.peerUri, ua.account) } call.transferButtonEnabled.value = true } call.callTransfer.value = call.onHoldCall != null call.callDuration = call.duration() call.showCallTimer.value = true if (ua.account.mediaEnc == "") call.securityIconTint.value = -1 else call.securityIconTint.value = call.security call.showCallButton.value = false call.showCancelButton.value = false call.showHangupButton.value = true call.showAnswerRejectButtons.value = false call.callOnHold.value = call.onhold Handler(Looper.getMainLooper()).postDelayed({ call.showOnHoldNotice.value = call.held }, 100) } } } } fun handleServiceEvent(ctx: Context, viewModel: ViewModel, event: String, params: ArrayList) { fun handleNextEvent(logMessage: String? = null) { if (logMessage != null) Log.w(TAG, logMessage) if (BaresipService.serviceEvents.isNotEmpty()) { val first = BaresipService.serviceEvents.removeAt(0) handleServiceEvent(ctx, viewModel, first.event, first.params) } } if (event == "started") { val uriString = params[0] as String Log.d(TAG, "Handling service event 'started' with URI '$uriString'") if (uriString != "") callAction(ctx, viewModel, uriString.toUri(), "dial") else { if (viewModel.selectedAor.value == "" && uas.value.isNotEmpty()) viewModel.updateSelectedAor(uas.value.first().account.aor) } if (Preferences(ctx).displayTheme != AppCompatDelegate.getDefaultNightMode()) { AppCompatDelegate.setDefaultNightMode(Preferences(ctx).displayTheme) } handleNextEvent() return } val uap = params[0] as Long val ua = UserAgent.ofUap(uap) if (ua == null) { handleNextEvent("handleServiceEvent '$event' did not find ua $uap") return } val ev = event.split(",") Log.d(TAG, "Handling service event '${ev[0]}' for $uap") val acc = ua.account val aor = ua.account.aor when (ev[0]) { "call rejected" -> { if (aor == viewModel.selectedAor.value) viewModel.triggerAccountUpdate() } "call incoming", "call outgoing" -> { val callp = params[1] as Long if (!BaresipService.isMainVisible) viewModel.navigateToHome() spinToAor(viewModel, aor) showCall(ctx, viewModel, ua, Call.ofCallp(callp)) } "call answered" -> { if (!BaresipService.isMainVisible) viewModel.navigateToHome() spinToAor(viewModel, aor) val callp = params[1] as Long showCall(ctx, viewModel, ua, Call.ofCallp(callp)) } "call redirect" -> { val redirectUri = ev[1] val target = Utils.friendlyUri(ctx, redirectUri, acc) if (acc.autoRedirect) { redirect(ctx, viewModel, ua, redirectUri) Toast.makeText(ctx, String.format(ctx.getString(R.string.redirect_notice), target), Toast.LENGTH_SHORT ).show() } else { dialogTitle.value = ctx.getString(R.string.redirect_request) dialogMessage.value = String.format(ctx.getString(R.string.redirect_request_query), target) firstText.value = "" secondText.value = ctx.getString(R.string.no) onSecondClicked.value = { } lastText.value = ctx.getString(R.string.yes) onLastClicked.value = { redirect(ctx, viewModel, ua, redirectUri) } showDialog.value = true } showCall(ctx, viewModel, ua) } "call established" -> { if (aor == viewModel.selectedAor.value) { viewModel.dialerState.callButtonsEnabled.value = true // Re-enable dialer val callp = params[1] as Long val call = Call.ofCallp(callp) if (call != null) { call.dtmfText.value = "" if (call.conferenceCall) Api.cmd_exec("conference") } showCall(ctx, viewModel, ua, call) } } "call update" -> { showCall(ctx, viewModel, ua) } "call verify" -> { val callp = params[1] as Long val call = Call.ofCallp(callp) if (call == null) { handleNextEvent("Call $callp to be verified is not found") return } dialogTitle.value = ctx.getString(R.string.verify) dialogMessage.value = String.format(ctx.getString(R.string.verify_sas), ev[1]) firstText.value = "" secondText.value = ctx.getString(R.string.no) onSecondClicked.value = { call.security = R.color.colorTrafficYellow call.zid = ev[2] if (aor == viewModel.selectedAor.value) call.securityIconTint.value = R.color.colorTrafficYellow secondText.value = "" } lastText.value = ctx.getString(R.string.yes) onLastClicked.value = { call.security = if (Api.cmd_exec("zrtp_verify ${ev[2]}") != 0) { Log.e(TAG, "Command 'zrtp_verify ${ev[2]}' failed") R.color.colorTrafficYellow } else { R.color.colorTrafficGreen } call.zid = ev[2] if (aor == viewModel.selectedAor.value) call.securityIconTint.value = call.security } showDialog.value = true } "call verified", "call secure" -> { val callp = params[1] as Long val call = Call.ofCallp(callp) if (call == null) { handleNextEvent("Call $callp that is verified is not found") return } if (aor == viewModel.selectedAor.value) call.securityIconTint.value = call.security } "call transfer", "transfer show" -> { if (!BaresipService.isMainVisible) viewModel.navigateToHome() val callp = params[1] as Long val call = Call.ofCallp(callp) val target = Utils.friendlyUri(ctx, ev[1], acc) dialogTitle.value = if (call != null) ctx.getString(R.string.transfer_request) else ctx.getString(R.string.call_request) dialogMessage.value = if (call != null) String.format(ctx.getString(R.string.transfer_request_query), target) else String.format(ctx.getString(R.string.call_request_query), target) firstText.value = "" secondText.value = ctx.getString(R.string.no) onSecondClicked.value = { if (call in Call.calls()) call!!.notifySipfrag(603, "Decline") } lastText.value = ctx.getString(R.string.yes) onLastClicked.value = { if (call in Call.calls()) acceptTransfer(ctx, viewModel, ua, call!!, ev[1]) else makeCall(ctx, viewModel, ev[1], false) } showDialog.value = true } "transfer accept" -> { val callp = params[1] as Long val call = Call.ofCallp(callp) if (call in Call.calls()) Api.ua_hangup(uap, callp, 487, "Request Terminated") makeCall(ctx, viewModel, ev[1], false) showCall(ctx, viewModel, ua) } "transfer failed" -> { showCall(ctx, viewModel, ua) } "call closed" -> { viewModel.updateCalls(Call.calls().toList()) val activity = ctx as? Activity if (activity != null) { val kgm = activity.getSystemService(Context.KEYGUARD_SERVICE) as KeyguardManager if (kgm.isKeyguardLocked) { activity.moveTaskToBack(true) return } } if (aor == viewModel.selectedAor.value) { viewModel.dialerState.callButtonsEnabled.value = true ua.account.resumeUri = "" showCall(ctx, viewModel, ua) if (acc.missedCalls) viewModel.triggerAccountUpdate() } } "message", "message show", "message reply" -> { Handler(Looper.getMainLooper()).postDelayed({ viewModel.onNewMessageReceived(aor, params[1] as String) }, 200) } "mwi notify" -> { val lines = ev[1].split("\n") for (line in lines) { if (line.startsWith("Voice-Message:")) { val counts = (line.split(" ")[1]).split("/") acc.vmNew = counts[0].toInt() acc.vmOld = counts[1].toInt() break } } if (aor == viewModel.selectedAor.value) viewModel.triggerAccountUpdate() } "mic muted" -> { val muted = ev[1].toBoolean() if (muted) viewModel.updateMicIcon(Icons.Filled.MicOff) else viewModel.updateMicIcon(Icons.Filled.Mic) } "speaker update" -> { viewModel.updateSpeakerPhoneStatus(ev[1].toBoolean()) } else -> Log.e(TAG, "Unknown event '${ev[0]}'") } viewModel.updateCalls(Call.calls().toList()) handleNextEvent() } fun handleIntent(ctx: Context, viewModel: ViewModel, intent: Intent, action: String) { Log.d(TAG, "Handling intent '$action'") val ev = action.split(",") when (ev[0]) { "call", "dial" -> { if (Call.inCall()) { Toast.makeText(ctx, ctx.getString(R.string.call_already_active), Toast.LENGTH_SHORT).show() return } val uap = intent.getLongExtra("uap", 0L) val ua = UserAgent.ofUap(uap) if (ua == null) { Log.w(TAG, "handleIntent 'call' did not find ua $uap") return } viewModel.dialerState.callUri.value = intent.getStringExtra("peer")!! spinToAor(viewModel, ua.account.aor) if (ev[0] == "call") { viewModel.dialerState.showCallConferenceButton.value = false callClick(ctx, viewModel, viewModel.dialerState) } } "call show", "call answer" -> { val callp = intent.getLongExtra("callp", 0L) val call = Call.ofCallp(callp) if (call == null) { Log.w(TAG, "handleIntent '$action' did not find call $callp") return } val ua = call.ua spinToAor(viewModel, ua.account.aor) if (ev[0] == "call answer") answer(ctx, call) else BaresipService.postServiceEvent(ServiceEvent( "call incoming", arrayListOf(call.ua.uap, callp), System.nanoTime()) ) } "call missed" -> { val uap = intent.getLongExtra("uap", 0L) val ua = UserAgent.ofUap(uap) if (ua == null) { Log.w(TAG, "handleIntent did not find ua $uap") return } spinToAor(viewModel, ua.account.aor) viewModel.navigateToCalls(ua.account.aor) } "call transfer", "transfer show", "transfer accept" -> { val callp = intent.getLongExtra("callp", 0L) val call = Call.ofCallp(callp) if (call == null) { Log.w(TAG, "handleIntent '$action' did not find call $callp") // moveTaskToBack(true) return } val uri = if (ev[0] == "call transfer") ev[1] else intent.getStringExtra("uri")!! BaresipService.postServiceEvent(ServiceEvent( ev[0] + "," + uri, arrayListOf(call.ua.uap, callp), System.nanoTime()) ) } "message", "message show", "message reply" -> { val uap = intent.getLongExtra("uap", 0L) val ua = UserAgent.ofUap(uap) if (ua == null) { Log.w(TAG, "handleIntent did not find ua $uap") return } spinToAor(viewModel, ua.account.aor) BaresipService.postServiceEvent(ServiceEvent( ev[0], arrayListOf(uap, intent.getStringExtra("peer")!!), System.nanoTime()) ) } } } fun handleDialog(ctx: Context, title: String, message: String, action: () -> Unit = {}) { dialogTitle.value = title dialogMessage.value = message firstText.value = "" secondText.value = "" lastText.value = ctx.getString(R.string.ok) onLastClicked.value = { action() } showDialog.value = true } fun callAction(ctx: Context, viewModel: ViewModel, uri: Uri?, action: String) { if (Call.inCall() || uas.value.isEmpty()) return Log.d(TAG, "Action $action to $uri") if (uri != null) { var uriStr: String var uap: Long when (uri.scheme) { "sip" -> { uriStr = Utils.uriUnescape(uri.toString()) var ua = UserAgent.ofDomain(Utils.uriHostPart(uriStr)) if (ua == null && uas.value.isNotEmpty()) ua = uas.value[0] if (ua == null) { Log.w(TAG, "No accounts for '$uriStr'") return } uap = ua.uap } "tel" -> { uriStr = uri.toString().replace("%2B", "+").replace("%20", "") .filterNot { setOf('-', ' ', '(', ')').contains(it) } var account: Account? = null for (a in Account.accounts()) if (a.telProvider != "") { account = a break } if (account == null) { Log.w(TAG, "No telephony providers for '$uriStr'") return } uap = UserAgent.ofAor(account.aor)!!.uap } else -> { Log.w(TAG, "Unsupported URI scheme ${uri.scheme}") return } } val intent = Intent(ctx, MainActivity::class.java) intent.putExtra("uap", uap) intent.putExtra("peer", uriStr) handleIntent(ctx, viewModel, intent, action) } } private fun redirect(ctx: Context, viewModel: ViewModel, ua: UserAgent, redirectUri: String) { if (ua.account.aor != viewModel.selectedAor.value) spinToAor(viewModel, ua.account.aor) viewModel.dialerState.callUri.value = redirectUri callClick(ctx, viewModel, viewModel.dialerState) } private fun acceptTransfer(ctx: Context, viewModel: ViewModel, ua: UserAgent, call: Call, uri: String) { val newCallp = ua.callAlloc(call.callp, Api.VIDMODE_OFF) if (newCallp != 0L) { Log.d(TAG, "Adding outgoing call ${ua.uap}/$newCallp/$uri") val newCall = Call(newCallp, ua, uri, "out", "transferring") newCall.add() if (newCall.connect(uri)) { if (ua.account.aor != viewModel.selectedAor.value) spinToAor(viewModel, ua.account.aor) showCall(ctx, viewModel, ua) } else { Log.w(TAG, "call_connect $newCallp failed") call.notifySipfrag(500, "Call Error") } } else { Log.w(TAG, "callAlloc for ua ${ua.uap} call ${call.callp} transfer failed") call.notifySipfrag(500, "Call Error") } } private fun backup(ctx: Context, password: String) { val files = arrayListOf("accounts", "config", "contacts", "call_history", "messages", "uuid", "gzrtp.zid", "cert.pem", "ca_cert", "ca_certs.crt") File(BaresipService.filesPath).walk().forEach { if (it.name.endsWith(".png")) files.add(it.name) } val zipFile = ctx.getString(R.string.app_name) + ".zip" val zipFilePath = BaresipService.filesPath + "/$zipFile" if (!Utils.zip(files, zipFile)) { Log.w(TAG, "Failed to write zip file '$zipFile'") alertTitle.value = ctx.getString(R.string.error) alertMessage.value = String.format(ctx.getString(R.string.backup_failed), Utils.fileNameOfUri(ctx, downloadsOutputUri!!)) showAlert.value = true downloadsOutputUri = null return } val content = Utils.getFileContents(zipFilePath) if (content == null) { Log.w(TAG, "Failed to read zip file '$zipFile'") alertTitle.value = ctx.getString(R.string.error) alertMessage.value = String.format(ctx.getString(R.string.backup_failed), Utils.fileNameOfUri(ctx, downloadsOutputUri!!)) showAlert.value = true downloadsOutputUri = null return } if (!Utils.encryptToUri(ctx, downloadsOutputUri!!, content, password)) { alertTitle.value = ctx.getString(R.string.error) alertMessage.value = String.format(ctx.getString(R.string.backup_failed), Utils.fileNameOfUri(ctx, downloadsOutputUri!!)) showAlert.value = true downloadsOutputUri = null return } alertTitle.value = ctx.getString(R.string.info) alertMessage.value = String.format(ctx.getString(R.string.backed_up), Utils.fileNameOfUri(ctx, downloadsOutputUri!!)) showAlert.value = true Utils.deleteFile(File(zipFilePath)) downloadsOutputUri = null } private fun restore(ctx: Context, password: String, onRestartApp: () -> Unit) { val zipFile = ctx.getString(R.string.app_name) + ".zip" val zipFilePath = BaresipService.filesPath + "/$zipFile" val zipData = Utils.decryptFromUri(ctx, downloadsInputUri!!, password) if (zipData == null) { alertTitle.value = ctx.getString(R.string.error) alertMessage.value = String.format(ctx.getString(R.string.restore_failed), Utils.fileNameOfUri(ctx, downloadsOutputUri!!)) showAlert.value = true downloadsOutputUri = null return } if (!Utils.putFileContents(zipFilePath, zipData)) { Log.w(TAG, "Failed to write zip file '$zipFile'") alertTitle.value = ctx.getString(R.string.error) alertMessage.value = String.format(ctx.getString(R.string.restore_failed), Utils.fileNameOfUri(ctx, downloadsOutputUri!!)) showAlert.value = true downloadsOutputUri = null return } if (!Utils.unZip(zipFilePath)) { Log.w(TAG, "Failed to unzip file '$zipFile'") alertTitle.value = ctx.getString(R.string.error) alertMessage.value = String.format( ctx.getString(R.string.restore_unzip_failed), "baresip", BuildConfig.VERSION_NAME ) showAlert.value = true downloadsOutputUri = null return } Utils.deleteFile(File(zipFilePath)) File("${BaresipService.filesPath}/recordings").walk().forEach { if (it.name.startsWith("dump")) Utils.deleteFile(it) } Utils.createEmptyFile(BaresipService.filesPath + "/restored") dialogTitle.value = ctx.getString(R.string.info) dialogMessage.value = ctx.getString(R.string.restored) firstText.value = ctx.getString(R.string.cancel) onFirstClicked.value = { showDialog.value = false } secondText.value = "" lastText.value = ctx.getString(R.string.restart) onLastClicked.value = { onRestartApp() showDialog.value = false } showDialog.value = true downloadsOutputUri = null } ================================================ FILE: app/src/main/kotlin/com/tutpro/baresip/Message.kt ================================================ package com.tutpro.baresip import java.io.* import java.util.ArrayList class Message(val aor: String, val peerUri: String, val message: String, val timeStamp: Long, var direction: Int, var responseCode: Int, var responseReason: String, var new: Boolean): Serializable { fun add() { val updatedMessages = BaresipService.messages.toMutableList() updatedMessages.add(this) BaresipService.messages = updatedMessages.toList() var count = 0 var remove: Message? = null for (message in BaresipService.messages) if (message.aor == this.aor) { count++ if (count > MESSAGE_HISTORY_SIZE) { remove = message break } } if (remove != null) updatedMessages.remove(remove) BaresipService.messages = updatedMessages.toList() save() } fun delete() { val updatedMessages = BaresipService.messages.toMutableList() updatedMessages.remove(this) BaresipService.messages = updatedMessages.toList() save() } companion object { const val MESSAGE_HISTORY_SIZE = 100 fun messages(): List { return BaresipService.messages } fun clearMessagesOfAor(aor: String) { val updatedMessages = BaresipService.messages.toMutableList() val it = updatedMessages.iterator() while (it.hasNext()) if (it.next().aor == aor) it.remove() BaresipService.messages = updatedMessages.toList() save() } fun deleteAorMessage(aor: String, time: Long) { val updatedMessages = BaresipService.messages.toMutableList() for (message in updatedMessages.reversed()) if (message.aor == aor && message.timeStamp == time) { updatedMessages.remove(message) BaresipService.messages = updatedMessages.toList() save() return } } fun updateAorMessage(aor: String, time: Long) { val updatedMessages = BaresipService.messages.toMutableList() for (message in updatedMessages.reversed()) if (message.aor == aor && message.timeStamp == time) { message.new = false BaresipService.messages = updatedMessages.toList() save() return } } fun unreadMessages(aor: String): Boolean { for (message in BaresipService.messages.reversed()) if (message.aor == aor && message.new) return true return false } fun unreadMessagesFromPeer(aor: String, peerUri: String): Boolean { for (message in BaresipService.messages.reversed()) if (message.aor == aor && message.peerUri == peerUri && message.new) return true return false } fun updateMessagesFromPearRead(aor: String, peerUri: String): Boolean { val updatedMessages = BaresipService.messages.toMutableList() var updated = false for (message in updatedMessages) if (message.aor == aor && message.peerUri == peerUri && message.new) { message.new = false updated = true } if (updated) { BaresipService.messages = updatedMessages.toList() save() } return updated } fun save() { val file = File(BaresipService.filesPath, "messages") try { val fos = FileOutputStream(file) val oos = ObjectOutputStream(fos) oos.writeObject(BaresipService.messages) oos.close() fos.close() Log.d(TAG, "Saved ${BaresipService.messages.size} messages") } catch (e: IOException) { Log.e(TAG, "OutputStream exception: $e") e.printStackTrace() } } fun restore() { val file = File(BaresipService.filesPath, "messages") if (file.exists()) { try { val fis = FileInputStream(file) val ois = ObjectInputStream(fis) @Suppress("UNCHECKED_CAST") val restoredMessages = ois.readObject() as? List if (restoredMessages != null) BaresipService.messages = restoredMessages ois.close() fis.close() Log.d(TAG, "Restored ${BaresipService.messages.size} messages") } catch (e: Exception) { Log.e(TAG, "InputStream exception: - $e") } } } } } ================================================ FILE: app/src/main/kotlin/com/tutpro/baresip/Preferences.kt ================================================ package com.tutpro.baresip import android.content.Context import androidx.preference.PreferenceManager import androidx.core.content.edit class Preferences(context: Context) { companion object { private const val DISPLAY_THEME = "com.tutpro.baresip.DISPLAY_THEME" } private val preferences = PreferenceManager.getDefaultSharedPreferences(context) var displayTheme = preferences.getInt(DISPLAY_THEME, -1) set(value) = preferences.edit { putInt(DISPLAY_THEME, value) } var ringtoneUri = preferences.getString("ringtone_uri", "") set(value) = preferences.edit { putString("ringtone_uri", value) } } ================================================ FILE: app/src/main/kotlin/com/tutpro/baresip/ServiceEvent.kt ================================================ package com.tutpro.baresip class ServiceEvent (val event: String, val params: ArrayList, val timeStamp: Long) ================================================ FILE: app/src/main/kotlin/com/tutpro/baresip/SettingsScreen.kt ================================================ package com.tutpro.baresip import android.Manifest import android.app.Activity import android.app.Activity.RESULT_OK import android.app.role.RoleManager import android.content.ActivityNotFoundException import android.content.Context import android.content.Context.POWER_SERVICE import android.content.Context.ROLE_SERVICE import android.content.Intent import android.content.pm.PackageManager import android.media.RingtoneManager import android.net.Uri import android.os.Build.VERSION import android.os.PowerManager import android.provider.Settings import androidx.activity.compose.LocalActivity import androidx.activity.compose.rememberLauncherForActivityResult import androidx.activity.result.ActivityResult import androidx.activity.result.contract.ActivityResultContracts import androidx.annotation.RequiresApi import androidx.appcompat.app.AppCompatDelegate import androidx.core.content.ContextCompat import androidx.compose.foundation.background import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.WindowInsets import androidx.compose.foundation.layout.asPaddingValues import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.imePadding import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.statusBars import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.text.KeyboardOptions import androidx.compose.foundation.verticalScroll import androidx.compose.material.icons.Icons import androidx.compose.material.icons.automirrored.filled.ArrowBack import androidx.compose.material.icons.filled.ArrowDropDown import androidx.compose.material.icons.filled.Check import androidx.compose.material3.DropdownMenu import androidx.compose.material3.DropdownMenuItem import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.HorizontalDivider import androidx.compose.material3.Icon import androidx.compose.material3.IconButton import androidx.compose.material3.MaterialTheme import androidx.compose.material3.OutlinedTextField import androidx.compose.material3.Scaffold import androidx.compose.material3.Switch import androidx.compose.material3.Text import androidx.compose.material3.TopAppBar import androidx.compose.material3.TopAppBarDefaults import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue import androidx.compose.runtime.livedata.observeAsState import androidx.compose.runtime.mutableIntStateOf import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.input.KeyboardType import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import androidx.core.app.ActivityCompat.shouldShowRequestPermissionRationale import androidx.core.net.toUri import androidx.lifecycle.viewmodel.compose.viewModel import androidx.navigation.NavController import androidx.navigation.NavGraphBuilder import androidx.navigation.compose.composable import com.tutpro.baresip.CustomElements.AlertDialog import com.tutpro.baresip.CustomElements.verticalScrollbar import com.tutpro.baresip.Utils.copyInputStreamToFile import java.io.File import java.io.FileInputStream import java.util.Locale private var restart = false private val showRestartDialog = mutableStateOf(false) private var save = false fun NavGraphBuilder.settingsScreenRoute( navController: NavController, onRestartApp: () -> Unit ) { composable("settings") { val ctx = LocalContext.current val viewModel = viewModel() SettingsScreen( ctx = ctx, navController = navController, settingsViewModel = viewModel, onBack = { navController.navigateUp() }, checkOnClick = { if (checkOnClick(ctx, viewModel)) { if (restart) showRestartDialog.value = true else navController.navigateUp() } }, onRestartApp = onRestartApp ) } } @OptIn(ExperimentalMaterial3Api::class) @Composable private fun SettingsScreen( ctx: Context, navController: NavController, settingsViewModel: SettingsViewModel, onBack: () -> Unit, checkOnClick: () -> Unit, onRestartApp: () -> Unit ) { val activity = LocalActivity.current var areSettingsLoaded by remember { mutableStateOf(false) } val audioResult = navController.currentBackStackEntry ?.savedStateHandle ?.getLiveData("audio_settings_result") ?.observeAsState() LaunchedEffect(audioResult?.value) { if (audioResult?.value == true) { Log.d(TAG, "Got result from AudioSettings: true") restart = true navController.currentBackStackEntry ?.savedStateHandle ?.remove("audio_settings_result") } } LaunchedEffect(null) { settingsViewModel.loadSettings(ctx) areSettingsLoaded = true } Scaffold( modifier = Modifier .fillMaxSize() .imePadding(), containerColor = MaterialTheme.colorScheme.background, topBar = { Column( modifier = Modifier .fillMaxWidth() .background(MaterialTheme.colorScheme.background) .padding( top = WindowInsets.statusBars.asPaddingValues().calculateTopPadding() ) ) { TopAppBar( title = { Text( text = stringResource(R.string.configuration), fontWeight = FontWeight.Bold ) }, colors = TopAppBarDefaults.topAppBarColors( containerColor = MaterialTheme.colorScheme.primary, navigationIconContentColor = MaterialTheme.colorScheme.onPrimary, titleContentColor = MaterialTheme.colorScheme.onPrimary, actionIconContentColor = MaterialTheme.colorScheme.onPrimary ), navigationIcon = { IconButton(onClick = onBack) { Icon( imageVector = Icons.AutoMirrored.Filled.ArrowBack, contentDescription = null, ) } }, windowInsets = WindowInsets(0, 0, 0, 0), actions = { IconButton(onClick = checkOnClick) { Icon( imageVector = Icons.Filled.Check, contentDescription = "Check" ) } }, ) } } ) { contentPadding -> if (showRestartDialog.value) { AlertDialog( showDialog = showRestartDialog, title = stringResource(R.string.restart_request), message = stringResource(R.string.config_restart), firstButtonText = stringResource(R.string.cancel), onFirstClicked = { navController.navigateUp() }, lastButtonText = stringResource(R.string.restart), onLastClicked = { onRestartApp() }, ) } if (areSettingsLoaded && activity != null) SettingsContent(settingsViewModel, contentPadding, navController, activity, onRestartApp) } } private val dialogTitle = mutableStateOf("") private val dialogMessage = mutableStateOf("") private val firstButtonText = mutableStateOf("") private val onFirstClicked = mutableStateOf({}) private val lastButtonText = mutableStateOf("") private val onLastClicked = mutableStateOf({}) private val showDialog = mutableStateOf(false) private val alertTitle = mutableStateOf("") private val alertMessage = mutableStateOf("") private val showAlert = mutableStateOf(false) @Composable private fun SettingsContent( viewModel: SettingsViewModel, contentPadding: PaddingValues, navController: NavController, activity: Activity, onRestartApp: () -> Unit ) { val alertTitleText = stringResource(R.string.alert) val errorTitleText = stringResource(R.string.error) val noticeTitleText = stringResource(R.string.notice) val confirmationText = stringResource(R.string.confirmation) val okButtonText = stringResource(R.string.ok) val cancelButtonText = stringResource(R.string.cancel) if (showAlert.value) { AlertDialog( showDialog = showAlert, title = alertTitle.value, message = alertMessage.value, firstButtonText = "", lastButtonText = okButtonText, ) } if (showDialog.value) AlertDialog( showDialog = showDialog, title = dialogTitle.value, message = dialogMessage.value, firstButtonText = firstButtonText.value, onFirstClicked = onFirstClicked.value, lastButtonText = lastButtonText.value, onLastClicked = onLastClicked.value, ) @Composable fun StartAutomatically() { val startAutomaticallyTitle = stringResource(R.string.start_automatically) val startAutomaticallyHelp = stringResource(R.string.start_automatically_help) val appearOnTopPermissionMessage = stringResource(R.string.appear_on_top_permission) Row( Modifier .fillMaxWidth() .padding(end = 10.dp), verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.Start ) { val ctx = LocalContext.current Text(text = startAutomaticallyTitle, modifier = Modifier .weight(1f) .clickable { alertTitle.value = startAutomaticallyTitle alertMessage.value = startAutomaticallyHelp showAlert.value = true }, fontSize = 18.sp ) val startAutomatically by viewModel.autoStart.collectAsState() Switch( checked = startAutomatically, onCheckedChange = { if (it) { if (!isAppearOnTopPermissionGranted(ctx)) { dialogTitle.value = noticeTitleText dialogMessage.value = appearOnTopPermissionMessage firstButtonText.value = cancelButtonText onFirstClicked.value = {} lastButtonText.value = okButtonText onLastClicked.value = { val intent = Intent(Settings.ACTION_MANAGE_OVERLAY_PERMISSION) ctx.startActivity(intent) } showDialog.value = true viewModel.autoStart.value = false } else viewModel.autoStart.value = true } else viewModel.autoStart.value = false } ) } } @Composable fun AddressFamily() { val addressFamilyTitle = stringResource(R.string.address_family) val addressFamilyHelp = stringResource(R.string.address_family_help) Row( Modifier .fillMaxWidth() .padding(end = 10.dp), verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.Start ) { val addressFamily by viewModel.addressFamily.collectAsState() Text(text = addressFamilyTitle, modifier = Modifier .weight(1f) .clickable { alertTitle.value = addressFamilyTitle alertMessage.value = addressFamilyHelp showAlert.value = true }, fontSize = 18.sp ) val isDropDownExpanded = remember { mutableStateOf(false) } val familyNames = listOf("--", "IPv4", "IPv6") val familyValues = listOf("", "ipv4", "ipv6") val itemPosition = remember { mutableIntStateOf(familyValues.indexOf(addressFamily)) } Box { Row( horizontalArrangement = Arrangement.End, verticalAlignment = Alignment.CenterVertically, modifier = Modifier.clickable { isDropDownExpanded.value = true } ) { Text(text = familyNames[itemPosition.intValue]) Icon( imageVector = Icons.Filled.ArrowDropDown, contentDescription = null, modifier = Modifier.size(36.dp) ) //CustomElements.DrawDrawable(R.drawable.arrow_drop_down) } DropdownMenu( expanded = isDropDownExpanded.value, onDismissRequest = { isDropDownExpanded.value = false }) { familyNames.forEachIndexed { index, family -> DropdownMenuItem( text = { Text(text = family) }, onClick = { isDropDownExpanded.value = false itemPosition.intValue = index viewModel.addressFamily.value = familyValues[index] }) if (index < 2) HorizontalDivider(thickness = 1.dp) } } } } } @Composable fun ListenAddress() { val listenAddressTitle = stringResource(R.string.listen_address) val listenAddressHelp = stringResource(R.string.listen_address_help) Row( Modifier .fillMaxWidth() .padding(end = 10.dp), verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.Start, ) { val listenAddress by viewModel.listenAddress.collectAsState() OutlinedTextField( value = listenAddress, placeholder = { Text(stringResource(R.string._0_0_0_0_5060)) }, onValueChange = { viewModel.listenAddress.value = it }, modifier = Modifier .fillMaxWidth() .clickable { alertTitle.value = listenAddressTitle alertMessage.value = listenAddressHelp showAlert.value = true }, singleLine = true, textStyle = androidx.compose.ui.text.TextStyle(fontSize = 18.sp), label = { Text(listenAddressTitle) }, keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Text) ) } } @Composable fun TransportProtocols() { val transportProtocolsTitle = stringResource(R.string.transport_protocols) val transportProtocolsHelp = stringResource(R.string.transport_protocols_help) Row( Modifier .fillMaxWidth() .padding(end = 10.dp), verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.Start ) { val transportProtocols by viewModel.transportProtocols.collectAsState() OutlinedTextField( value = transportProtocols, onValueChange = { viewModel.transportProtocols.value = it }, modifier = Modifier .fillMaxWidth() .clickable { alertTitle.value = transportProtocolsTitle alertMessage.value = transportProtocolsHelp showAlert.value = true }, textStyle = androidx.compose.ui.text.TextStyle(fontSize = 18.sp), label = { Text(transportProtocolsTitle) }, keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Text) ) } } @Composable fun DnsServers() { val dnsServersTitle = stringResource(R.string.dns_servers) val dnsServersHelp = stringResource(R.string.dns_servers_help) Row( Modifier .fillMaxWidth() .padding(end = 10.dp), verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.Start ) { val dnsServers by viewModel.dnsServers.collectAsState() OutlinedTextField( value = dnsServers, onValueChange = { viewModel.dnsServers.value = it }, modifier = Modifier .fillMaxWidth() .clickable { alertTitle.value = dnsServersTitle alertMessage.value = dnsServersHelp showAlert.value = true }, textStyle = androidx.compose.ui.text.TextStyle(fontSize = 18.sp), label = { Text(dnsServersTitle) }, keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Text) ) } } @Composable fun TlsCertificateFile(activity: Activity) { val ctx = LocalContext.current val readCertError = stringResource(R.string.read_cert_error) val tlsCertificateFileTitle = stringResource(R.string.tls_certificate_file) val tlsCertificateFileHelp = stringResource(R.string.tls_certificate_file_help) val requestPermissionLauncher = rememberLauncherForActivityResult( contract = ActivityResultContracts.RequestPermission() ) {} val certificateRequest = rememberLauncherForActivityResult( contract = ActivityResultContracts.StartActivityForResult() ) { val certPath = BaresipService.filesPath + "/cert.pem" val certFile = File(certPath) if (it.resultCode == RESULT_OK) { it.data?.data?.also { uri -> try { val inputStream = ctx.contentResolver.openInputStream(uri) as FileInputStream certFile.copyInputStreamToFile(inputStream) inputStream.close() Config.replaceVariable("sip_certificate", certPath) viewModel.tlsCertificateFile.value = true save = true restart = true } catch (e: Error) { alertTitle.value = errorTitleText alertMessage.value = readCertError + ": " + e.message showAlert.value = true viewModel.tlsCertificateFile.value = false } } } else viewModel.tlsCertificateFile.value = false if (!viewModel.tlsCertificateFile.value) Utils.deleteFile(certFile) } val showAlertDialog = remember { mutableStateOf(false) } Row( Modifier .fillMaxWidth() .padding(end = 10.dp), verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.Start ) { val ctx = LocalContext.current Text(text = tlsCertificateFileTitle, modifier = Modifier .weight(1f) .clickable { alertTitle.value = tlsCertificateFileTitle alertMessage.value = tlsCertificateFileHelp showAlert.value = true }, fontSize = 18.sp ) val tlsCertificateFile by viewModel.tlsCertificateFile.collectAsState() Switch( checked = tlsCertificateFile, onCheckedChange = { viewModel.tlsCertificateFile.value = it if (it) if (VERSION.SDK_INT < 29) { viewModel.tlsCertificateFile.value = false val permission = Manifest.permission.READ_EXTERNAL_STORAGE when { ContextCompat.checkSelfPermission(ctx, permission) == PackageManager.PERMISSION_GRANTED -> { Log.d(TAG, "Read External Storage permission granted") val downloadsPath = Utils.downloadsPath("cert.pem") val content = Utils.getFileContents(downloadsPath) if (content == null) { alertTitle.value = errorTitleText alertMessage.value = readCertError showAlert.value = true return@Switch } val certPath = BaresipService.filesPath + "/cert.pem" Utils.putFileContents(certPath, content) Config.replaceVariable("sip_certificate", certPath) viewModel.tlsCertificateFile.value = true save = true restart = true } shouldShowRequestPermissionRationale(activity, permission) -> showAlertDialog.value = true else -> requestPermissionLauncher.launch(permission) } } else Utils.selectInputFile(certificateRequest) else { Config.removeVariable("sip_certificate") Utils.deleteFile(File(BaresipService.filesPath + "/cert.pem")) save = true restart = true } } ) } if (showAlertDialog.value) AlertDialog( showDialog = showAlertDialog, title = stringResource(R.string.notice), message = stringResource(R.string.no_read_permission), firstButtonText = "", onFirstClicked = {}, lastButtonText = stringResource(R.string.ok), onLastClicked = { requestPermissionLauncher.launch(Manifest.permission.READ_EXTERNAL_STORAGE) }, ) } @Composable fun VerifyServer() { val verifyServerTitle = stringResource(R.string.verify_server) val verifyServerHelp = stringResource(R.string.verify_server_help) Row( Modifier .fillMaxWidth() .padding(end = 10.dp), verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.Start ) { Text(text = verifyServerTitle, modifier = Modifier .weight(1f) .clickable { alertTitle.value = verifyServerTitle alertMessage.value = verifyServerHelp showAlert.value = true }, fontSize = 18.sp ) val verifyServer by viewModel.verifyServer.collectAsState() Switch( checked = verifyServer, onCheckedChange = { viewModel.verifyServer.value = it } ) } } @Composable fun CaFile(activity: Activity) { val ctx = LocalContext.current val tlsCaFileTitle = stringResource(R.string.tls_ca_file) val tlsCaFileHelp = stringResource(R.string.tls_ca_file_help) val readCaCertsError = stringResource(R.string.read_ca_certs_error) val noReadPermissionMessage = stringResource(R.string.no_read_permission) val requestPermissionLauncher = rememberLauncherForActivityResult( contract = ActivityResultContracts.RequestPermission() ) {} val caCertsRequest = rememberLauncherForActivityResult( contract = ActivityResultContracts.StartActivityForResult() ) { val caCertsFile = File(BaresipService.filesPath + "/ca_certs.crt") if (it.resultCode == RESULT_OK) it.data?.data?.also { uri -> try { val inputStream = ctx.contentResolver.openInputStream(uri) as FileInputStream caCertsFile.copyInputStreamToFile(inputStream) inputStream.close() restart = true } catch (e: Error) { alertTitle.value = errorTitleText alertMessage.value = readCaCertsError + ": " + e.message showAlert.value = true viewModel.caFile.value = false } } else viewModel.caFile.value = false if (!viewModel.caFile.value) caCertsFile.delete() } Row( Modifier .fillMaxWidth() .padding(end = 10.dp), verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.Start ) { val ctx = LocalContext.current Text(text = tlsCaFileTitle, modifier = Modifier .weight(1f) .clickable { alertTitle.value = tlsCaFileTitle alertMessage.value = tlsCaFileHelp showAlert.value = true }, fontSize = 18.sp ) val caFile by viewModel.caFile.collectAsState() Switch( checked = caFile, onCheckedChange = { viewModel.caFile.value = it if (it) { if (VERSION.SDK_INT < 29) { viewModel.caFile.value = false val permission = Manifest.permission.READ_EXTERNAL_STORAGE when { ContextCompat.checkSelfPermission(ctx, permission) == PackageManager.PERMISSION_GRANTED -> { Log.d(TAG, "Read External Storage permission granted") val downloadsPath = Utils.downloadsPath("ca_certs.crt") val content = Utils.getFileContents(downloadsPath) if (content == null) { alertTitle.value = errorTitleText alertMessage.value = readCaCertsError showAlert.value = true return@Switch } File(BaresipService.filesPath + "/ca_certs.crt").writeBytes(content) viewModel.caFile.value = true restart = true } shouldShowRequestPermissionRationale(activity, permission) -> { dialogTitle.value = noticeTitleText dialogMessage.value = noReadPermissionMessage firstButtonText.value = "" lastButtonText.value = okButtonText onLastClicked.value = { requestPermissionLauncher.launch(permission) } showDialog.value = true } else -> requestPermissionLauncher.launch(permission) } } else Utils.selectInputFile(caCertsRequest) } else { Utils.deleteFile(File(BaresipService.filesPath + "/ca_certs.crt")) restart = true } } ) } } @Composable fun UserAgent() { val userAgentTitle = stringResource(R.string.user_agent) val userAgentHelp = stringResource(R.string.user_agent_help) Row( Modifier .fillMaxWidth() .padding(end = 10.dp), verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.Start ) { val userAgent by viewModel.userAgent.collectAsState() OutlinedTextField( value = userAgent, placeholder = { Text(userAgentTitle) }, onValueChange = { viewModel.userAgent.value = it }, modifier = Modifier .fillMaxWidth() .clickable { alertTitle.value = userAgentTitle alertMessage.value = userAgentHelp showAlert.value = true }, textStyle = androidx.compose.ui.text.TextStyle(fontSize = 18.sp), label = { Text(userAgentTitle) }, keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Text) ) } } @Composable fun UniqueContactUri() { val uniqueContactUriTitle = stringResource(R.string.unique_contact_uri) val uniqueContactUriHelp = stringResource(R.string.unique_contact_uri_help) Row( Modifier .fillMaxWidth() .padding(end = 10.dp), verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.Start ) { Text(text = uniqueContactUriTitle, modifier = Modifier .weight(1f) .clickable { alertTitle.value = uniqueContactUriTitle alertMessage.value = uniqueContactUriHelp showAlert.value = true }, fontSize = 18.sp ) val uniqueContactUri by viewModel.uniqueContactUri.collectAsState() Switch( checked = uniqueContactUri, onCheckedChange = { viewModel.uniqueContactUri.value = it } ) } } @Composable fun AudioSettings(navController: NavController) { Row( Modifier .fillMaxWidth() .padding(top = 12.dp), verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.Start ) { Text( text = stringResource(R.string.audio_settings), modifier = Modifier .weight(1f) .clickable { navController.navigate("audio") }, fontSize = 18.sp, fontWeight = FontWeight. Bold ) } } @Composable fun Contacts(activity: Activity) { val contactsTitle = stringResource(R.string.contacts) val contactsHelp = stringResource(R.string.contacts_help) val consentRequestTitle = stringResource(R.string.consent_request) val contactsConsentMessage = stringResource(R.string.contacts_consent) val denyText = stringResource(R.string.deny) val acceptText = stringResource(R.string.accept) val both = stringResource(R.string.both) val noAndroidContactsMessage = stringResource(R.string.no_android_contacts) val showAlertDialog = remember { mutableStateOf(false) } Row( Modifier .fillMaxWidth() .padding(top = 12.dp) .padding(end = 10.dp), verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.Start ) { val ctx = LocalContext.current Text(text = contactsTitle, modifier = Modifier .weight(1f) .clickable { alertTitle.value = contactsTitle alertMessage.value = contactsHelp showAlert.value = true }, fontSize = 18.sp ) val isDropDownExpanded = remember { mutableStateOf(false) } val contactNames = listOf( "baresip", "Android", both ) val contactsMode by viewModel.contactsMode.collectAsState() val contactValues = listOf("baresip", "android", "both") val itemPosition = remember { mutableIntStateOf(contactValues.indexOf(contactsMode)) } val requestPermissionsLauncher = rememberLauncherForActivityResult( contract = ActivityResultContracts.RequestMultiplePermissions() ) {} val contactsPermissions = arrayOf(Manifest.permission.READ_CONTACTS, Manifest.permission.WRITE_CONTACTS) Box { Row( horizontalArrangement = Arrangement.Center, verticalAlignment = Alignment.CenterVertically, modifier = Modifier.clickable { isDropDownExpanded.value = true } ) { Text(text = contactNames[itemPosition.intValue]) Icon( imageVector = Icons.Filled.ArrowDropDown, contentDescription = null, modifier = Modifier.size(36.dp) ) } DropdownMenu( expanded = isDropDownExpanded.value, onDismissRequest = { isDropDownExpanded.value = false }) { contactNames.forEachIndexed { index, name -> DropdownMenuItem( text = { Text(text = name) }, onClick = { isDropDownExpanded.value = false val mode = contactValues[index] if (mode != "baresip" && !Utils.checkPermissions(ctx, contactsPermissions)) { dialogTitle.value = consentRequestTitle dialogMessage.value = contactsConsentMessage firstButtonText.value = denyText onFirstClicked.value = { itemPosition.intValue = contactValues.indexOf(contactsMode) } lastButtonText.value = acceptText onLastClicked.value = { showDialog.value = false viewModel.contactsMode.value = mode if (ContextCompat.checkSelfPermission( ctx, Manifest.permission.READ_CONTACTS ) == PackageManager.PERMISSION_GRANTED && ContextCompat.checkSelfPermission( ctx, Manifest.permission.WRITE_CONTACTS ) == PackageManager.PERMISSION_GRANTED ) { Log.d(TAG, "Contacts permissions already granted") } else { if (shouldShowRequestPermissionRationale( activity, Manifest.permission.READ_CONTACTS ) || shouldShowRequestPermissionRationale( activity, Manifest.permission.WRITE_CONTACTS ) ) showAlertDialog.value = true else requestPermissionsLauncher.launch( arrayOf( Manifest.permission.READ_CONTACTS, Manifest.permission.WRITE_CONTACTS ) ) } } showDialog.value = true } else { itemPosition.intValue = index viewModel.contactsMode.value = contactValues[index] } }) if (index < 2) HorizontalDivider(thickness = 1.dp) } } } if (showAlertDialog.value) AlertDialog( showDialog = showAlertDialog, title = noticeTitleText, message = noAndroidContactsMessage, firstButtonText = "", onFirstClicked = {}, lastButtonText = okButtonText, onLastClicked = { requestPermissionsLauncher.launch( arrayOf( Manifest.permission.READ_CONTACTS, Manifest.permission.WRITE_CONTACTS ) )}, ) } } @Composable fun Ringtone() { val ringToneTitle = stringResource(R.string.ringtone) val selectRingToneMessage = stringResource(R.string.select_ringtone) val launcher = rememberLauncherForActivityResult( ActivityResultContracts.StartActivityForResult() ) { result: ActivityResult -> if (result.resultCode == RESULT_OK) { val uri: Uri? = if (VERSION.SDK_INT >= 33) result.data?.getParcelableExtra(RingtoneManager.EXTRA_RINGTONE_PICKED_URI, Uri::class.java) else @Suppress("DEPRECATION") result.data?.getParcelableExtra(RingtoneManager.EXTRA_RINGTONE_PICKED_URI) if (uri != null) viewModel.ringtoneUri.value = uri.toString() } } Row( Modifier .fillMaxWidth() .padding(top = 12.dp), verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.Start ) { Text( text = ringToneTitle, modifier = Modifier .weight(1f) .clickable { val intent = Intent(RingtoneManager.ACTION_RINGTONE_PICKER) intent.putExtra( RingtoneManager.EXTRA_RINGTONE_TYPE, RingtoneManager.TYPE_RINGTONE ) intent.putExtra( RingtoneManager.EXTRA_RINGTONE_TITLE, selectRingToneMessage ) intent.putExtra( RingtoneManager.EXTRA_RINGTONE_EXISTING_URI, viewModel.ringtoneUri.value.toUri() ) intent.putExtra(RingtoneManager.EXTRA_RINGTONE_SHOW_SILENT, false) intent.putExtra(RingtoneManager.EXTRA_RINGTONE_SHOW_DEFAULT, true) launcher.launch(intent) }, fontSize = 18.sp, fontWeight = FontWeight.Bold ) } } @Composable fun BatteryOptimizations() { val batteryOptimizationsTitle = stringResource(R.string.battery_optimizations) val batteryOptimizationsHelp = stringResource(R.string.battery_optimizations_help) Row( Modifier .fillMaxWidth() .padding(end = 10.dp), verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.Start ) { val ctx = LocalContext.current val batterySettingsLauncher = rememberLauncherForActivityResult( contract = ActivityResultContracts.StartActivityForResult() ) { _ -> val powerManager = ctx.getSystemService(POWER_SERVICE) as PowerManager viewModel.batteryOptimizations.value = !powerManager.isIgnoringBatteryOptimizations(ctx.packageName) } Text(text = batteryOptimizationsTitle, modifier = Modifier .weight(1f) .clickable { alertTitle.value = batteryOptimizationsTitle alertMessage.value = batteryOptimizationsHelp showAlert.value = true }, fontSize = 18.sp ) val battery by viewModel.batteryOptimizations.collectAsState() Switch( checked = battery, onCheckedChange = { viewModel.batteryOptimizations.value = it batterySettingsLauncher.launch( Intent(Settings.ACTION_IGNORE_BATTERY_OPTIMIZATION_SETTINGS)) } ) } } @Composable fun DarkTheme() { val darkThemeTitle = stringResource(R.string.dark_theme) val darkThemeHelp = stringResource(R.string.dark_theme_help) Row( Modifier .fillMaxWidth() .padding(end = 10.dp), verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.Start ) { Text(text = darkThemeTitle, modifier = Modifier .weight(1f) .clickable { alertTitle.value = darkThemeTitle alertMessage.value = darkThemeHelp showAlert.value = true }, fontSize = 18.sp ) val darkTheme by viewModel.darkTheme.collectAsState() Switch( checked = darkTheme, onCheckedChange = { viewModel.darkTheme.value = it } ) } } @Composable fun DynamicColors() { val dynamicColorsTitle = stringResource(R.string.dynamic_colors) val dynamicColorsHelp = stringResource(R.string.dynamic_colors_help) Row( Modifier .fillMaxWidth() .padding(end = 10.dp), verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.Start ) { Text(text = dynamicColorsTitle, modifier = Modifier .weight(1f) .clickable { alertTitle.value = dynamicColorsTitle alertMessage.value = dynamicColorsHelp showAlert.value = true }, fontSize = 18.sp ) val dynamicColors by viewModel.dynamicColors.collectAsState() Switch( checked = dynamicColors, onCheckedChange = { viewModel.dynamicColors.value = it } ) } } @Composable fun ColorBlind() { val colorBlindTitle = stringResource(R.string.colorblind) val colorBlindHelp = stringResource(R.string.colorblind_help) Row( Modifier .fillMaxWidth() .padding(end = 10.dp), verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.Start ) { Text(text = colorBlindTitle, modifier = Modifier .weight(1f) .clickable { alertTitle.value = colorBlindTitle alertMessage.value = colorBlindHelp showAlert.value = true }, fontSize = 18.sp ) val colorblind by viewModel.colorblind.collectAsState() Switch( checked = colorblind, onCheckedChange = { viewModel.colorblind.value = it } ) } } @Composable fun ProximitySensing() { val proximitySensingTitle = stringResource(R.string.proximity_sensing) val proximitySensingHelp = stringResource(R.string.proximity_sensing_help) Row( Modifier .fillMaxWidth() .padding(end = 10.dp), verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.Start ) { Text(text = proximitySensingTitle, modifier = Modifier .weight(1f) .clickable { alertTitle.value = proximitySensingTitle alertMessage.value = proximitySensingHelp showAlert.value = true }, fontSize = 18.sp ) val proximitySensing by viewModel.proximitySensing.collectAsState() Switch( checked = proximitySensing, onCheckedChange = { viewModel.proximitySensing.value = it } ) } } @RequiresApi(29) @Composable fun DefaultDialer() { val defaultPhoneAppTitle = stringResource(R.string.default_phone_app) val defaultPhoneAppHelp = stringResource(R.string.default_phone_app_help) val dialerRoleNotAvailableMessage = stringResource(R.string.dialer_role_not_available) Row( Modifier .fillMaxWidth() .padding(end = 10.dp), verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.Start ) { val ctx = LocalContext.current Text(text = defaultPhoneAppTitle, modifier = Modifier .weight(1f) .clickable { alertTitle.value = defaultPhoneAppTitle alertMessage.value = defaultPhoneAppHelp showAlert.value = true }, fontSize = 18.sp ) val defaultDialer by viewModel.defaultDialer.collectAsState() val roleManager = ctx.getSystemService(ROLE_SERVICE) as RoleManager val dialerRoleRequest = rememberLauncherForActivityResult( contract = ActivityResultContracts.StartActivityForResult() ) { result -> Log.d(TAG, "dialerRoleRequest result: $result") viewModel.defaultDialer.value = roleManager.isRoleHeld(RoleManager.ROLE_DIALER) } Switch( checked = defaultDialer, onCheckedChange = { viewModel.defaultDialer.value = it if (it) { if (!roleManager.isRoleAvailable(RoleManager.ROLE_DIALER)) { alertTitle.value = alertTitleText alertMessage.value = dialerRoleNotAvailableMessage showAlert.value = true } else if (!roleManager.isRoleHeld(RoleManager.ROLE_DIALER)) dialerRoleRequest.launch(roleManager.createRequestRoleIntent(RoleManager.ROLE_DIALER)) } else { try { dialerRoleRequest.launch(Intent("android.settings.MANAGE_DEFAULT_APPS_SETTINGS")) } catch (e: ActivityNotFoundException) { Log.e(TAG, "ActivityNotFound exception: ${e.message}") } } } ) } } @Composable fun Debug() { val debugTitle = stringResource(R.string.debug) val debugHelp = stringResource(R.string.debug_help) Row( Modifier .fillMaxWidth() .padding(end = 10.dp), verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.Start ) { Text(text = debugTitle, modifier = Modifier .weight(1f) .clickable { alertTitle.value = debugTitle alertMessage.value = debugHelp showAlert.value = true }, fontSize = 18.sp ) val debug by viewModel.debug.collectAsState() Switch( checked = debug, onCheckedChange = { viewModel.debug.value = it } ) } } @Composable fun SipTrace() { val sipTraceTitle = stringResource(R.string.sip_trace) val sipTraceHelp = stringResource(R.string.sip_trace_help) Row( Modifier .fillMaxWidth() .padding(end = 10.dp), verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.Start ) { Text(text = sipTraceTitle, modifier = Modifier .weight(1f) .clickable { alertTitle.value = sipTraceTitle alertMessage.value = sipTraceHelp showAlert.value = true }, fontSize = 18.sp ) val sipTrace by viewModel.sipTrace.collectAsState() Switch( checked = sipTrace, onCheckedChange = { viewModel.sipTrace.value = it } ) } } @Composable fun Reset(onRestartApp: () -> Unit) { val resetConfigTitle = stringResource(R.string.reset_config) val resetConfigHelp = stringResource(R.string.reset_config_help) val resetConfigAlert = stringResource(R.string.reset_config_alert) val resetButtonText = stringResource(R.string.reset) Row( Modifier .fillMaxWidth() .padding(end = 10.dp), verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.Start ) { Text(text = resetConfigTitle, modifier = Modifier .weight(1f) .clickable { alertTitle.value = resetConfigTitle alertMessage.value = resetConfigHelp showAlert.value = true }, fontSize = 18.sp ) var reset by remember { mutableStateOf(false) } Switch( checked = reset, onCheckedChange = { dialogTitle.value = confirmationText dialogMessage.value = resetConfigAlert firstButtonText.value = cancelButtonText onFirstClicked.value = { reset = false } lastButtonText.value = resetButtonText onLastClicked.value = { Config.reset() onRestartApp() } showDialog.value = true } ) } } if (Config.variable("auto_start") == "yes" && !isAppearOnTopPermissionGranted(LocalContext.current)) { Config.replaceVariable("auto_start", "no") save = true } val scrollState = rememberScrollState() Column( modifier = Modifier .fillMaxWidth() .padding(contentPadding) .padding(start = 16.dp, end = 4.dp, top = 8.dp, bottom = 8.dp) .verticalScrollbar(scrollState) .verticalScroll(scrollState), verticalArrangement = Arrangement.spacedBy(8.dp), ) { StartAutomatically() AddressFamily() ListenAddress() TransportProtocols() DnsServers() TlsCertificateFile(activity) VerifyServer() CaFile(activity) UserAgent() UniqueContactUri() AudioSettings(navController) Contacts(activity) Ringtone() BatteryOptimizations() DarkTheme() if (VERSION.SDK_INT >= 31) DynamicColors() ColorBlind() ProximitySensing() if (VERSION.SDK_INT >= 29) DefaultDialer() Debug() SipTrace() Reset(onRestartApp) } } private fun checkOnClick(ctx: Context, viewModel: SettingsViewModel): Boolean { val noticeTitle = ctx.getString(R.string.notice) if ((Config.variable("auto_start") == "yes") != viewModel.autoStart.value) { Config.replaceVariable( "auto_start", if (viewModel.autoStart.value) "yes" else "no" ) save = true } val listenAddr = viewModel.listenAddress.value.trim() if (listenAddr != Config.variable("sip_listen")) { if ((listenAddr != "") && !Utils.checkIpPort(listenAddr)) { alertTitle.value = noticeTitle alertMessage.value = "${ctx.getString(R.string.invalid_listen_address)}: $listenAddr" showAlert.value = true return false } Config.replaceVariable("sip_listen", listenAddr) save = true restart = true } if (Config.variable("net_af").lowercase() != viewModel.addressFamily.value) { Config.replaceVariable("net_af", viewModel.addressFamily.value) save = true restart = true } val transportProtocols = viewModel.transportProtocols.value .lowercase(Locale.ROOT).replace(" ", "") if (transportProtocols != Config.variable("sip_transports")) { if (!checkTransportProtocols(transportProtocols)) { alertTitle.value = noticeTitle alertMessage.value = "${ctx.getString(R.string.invalid_transport_protocols)}: " + transportProtocols showAlert.value = true return false } Config.removeVariable("sip_transports") if (transportProtocols.isNotEmpty()) Config.replaceVariable("sip_transports", transportProtocols) save = true restart = true } val dnsServers = addMissingPorts(viewModel.dnsServers.value .lowercase(Locale.ROOT).replace(" ", "")) if (dnsServers != Config.dnsServers()) { if (!checkDnsServers(dnsServers)) { alertTitle.value = noticeTitle alertMessage.value = "${ctx.getString(R.string.invalid_dns_servers)}: $dnsServers" showAlert.value = true return false } Config.removeVariable("dns_server") if (dnsServers.isNotEmpty()) { for (server in dnsServers.split(",")) Config.addVariable("dns_server", server) Config.replaceVariable("dyn_dns", "no") if (Api.net_use_nameserver(dnsServers) != 0) { alertTitle.value = noticeTitle alertMessage.value = "${ctx.getString(R.string.failed_to_set_dns_servers)}: $dnsServers" showAlert.value = true return false } } else { Config.replaceVariable("dyn_dns", "yes") Config.updateDnsServers(BaresipService.dnsServers) } // Api.net_dns_debug() save = true } if ((Config.variable("sip_verify_server") == "yes") != viewModel.verifyServer.value) { Config.replaceVariable("sip_verify_server", if (viewModel.verifyServer.value) "yes" else "no") Api.config_verify_server_set(viewModel.verifyServer.value) save = true } val userAgent = viewModel.userAgent.value.trim() if (userAgent != Config.variable("user_agent")) { if (userAgent != "" && !Utils.checkServerVal(userAgent)) { alertTitle.value = noticeTitle alertMessage.value = "${ctx.getString(R.string.invalid_user_agent)}: " + userAgent showAlert.value = true return false } if (userAgent != "") Config.replaceVariable("user_agent", userAgent) else Config.removeVariable("user_agent") save = true restart = true } if ((Config.variable("sip_cuser_random") == "yes") != viewModel.uniqueContactUri.value) { Config.replaceVariable("sip_cuser_random", if (viewModel.uniqueContactUri.value) "yes" else "no") save = true restart = true } val ringtoneUri = viewModel.ringtoneUri.value Preferences(ctx).ringtoneUri = ringtoneUri BaresipService.rt = RingtoneManager.getRingtone(ctx, ringtoneUri.toUri()) val contactsMode = viewModel.contactsMode.value if (Config.variable("contacts_mode").lowercase() != contactsMode) { Config.replaceVariable("contacts_mode", contactsMode) BaresipService.contactsMode = contactsMode val baresipService = Intent(ctx, BaresipService::class.java) when (contactsMode) { "baresip" -> { BaresipService.androidContacts.value = listOf() Contact.restoreBaresipContacts() baresipService.action = "Stop Content Observer" } "android" -> { BaresipService.baresipContacts.value = mutableListOf() Contact.loadAndroidContacts(ctx) baresipService.action = "Start Content Observer" } "both" -> { Contact.restoreBaresipContacts() Contact.loadAndroidContacts(ctx) baresipService.action = "Start Content Observer" } } Contact.contactsUpdate() ContextCompat.startForegroundService(ctx, baresipService) save = true } val darkTheme = viewModel.darkTheme.value val newDisplayTheme = if (darkTheme) AppCompatDelegate.MODE_NIGHT_YES else AppCompatDelegate.MODE_NIGHT_FOLLOW_SYSTEM if (Preferences(ctx).displayTheme == AppCompatDelegate.MODE_NIGHT_YES != darkTheme) { Preferences(ctx).displayTheme = newDisplayTheme BaresipService.darkTheme.value = darkTheme AppCompatDelegate.setDefaultNightMode(newDisplayTheme) Config.replaceVariable("dark_theme", if (darkTheme) "yes" else "no") save = true } val dynamicColors = viewModel.dynamicColors.value if (BaresipService.dynamicColors.value != dynamicColors) { BaresipService.dynamicColors.value = dynamicColors Config.replaceVariable("dynamic_colors", if (dynamicColors) "yes" else "no") save = true } val colorblind = viewModel.colorblind.value if ((Config.variable("colorblind") == "yes") != colorblind) { Config.replaceVariable( "colorblind", if (colorblind) "yes" else "no" ) BaresipService.colorblind = colorblind UserAgent.updateColorblindStatus() val baresipService = Intent(ctx, BaresipService::class.java) baresipService.action = "Update Notification" ContextCompat.startForegroundService(ctx, baresipService) save = true } val proximitySensing = viewModel.proximitySensing.value if ((Config.variable("proximity_sensing") == "yes") != proximitySensing) { Config.replaceVariable( "proximity_sensing", if (proximitySensing) "yes" else "no" ) BaresipService.proximitySensing = proximitySensing save = true } val debug = viewModel.debug.value if ((Config.variable("log_level") == "0") != debug) { val logLevelString = if (debug) "0" else "2" Config.replaceVariable("log_level", logLevelString) Api.log_level_set(logLevelString.toInt()) Log.logLevelSet(logLevelString.toInt()) save = true } val sipTrace = viewModel.sipTrace.value if (BaresipService.sipTrace != sipTrace) { BaresipService.sipTrace = sipTrace Api.uag_enable_sip_trace(sipTrace) } if (save) Config.save() return true } private fun isAppearOnTopPermissionGranted(ctx: Context): Boolean { return Settings.canDrawOverlays(ctx) } private fun addMissingPorts(addressList: String): String { if (addressList == "") return "" var result = "" for (addr in addressList.split(",")) result = if (Utils.checkIpPort(addr)) { "$result,$addr" } else { if (Utils.checkIpV4(addr)) "$result,$addr:53" else "$result,[$addr]:53" } return result.substring(1) } private fun checkTransportProtocols(transportProtocols: String): Boolean { if (transportProtocols.isEmpty()) return true for (protocol in transportProtocols.split(",")) if (protocol !in listOf("udp", "tcp", "tls", "ws", "wss")) return false return true } private fun checkDnsServers(dnsServers: String): Boolean { if (dnsServers.isEmpty()) return true for (server in dnsServers.split(",")) if (!Utils.checkIpPort(server.trim())) return false return true } ================================================ FILE: app/src/main/kotlin/com/tutpro/baresip/SettingsViewModel.kt ================================================ package com.tutpro.baresip import android.app.role.RoleManager import android.content.Context import android.content.Context.POWER_SERVICE import android.content.Context.ROLE_SERVICE import android.media.RingtoneManager import android.os.Build import android.os.PowerManager import androidx.appcompat.app.AppCompatDelegate import androidx.compose.runtime.mutableIntStateOf import androidx.lifecycle.ViewModel import kotlinx.coroutines.flow.MutableStateFlow import java.io.File class SettingsViewModel: ViewModel() { val autoStart = MutableStateFlow(false) val listenAddress = MutableStateFlow("") val addressFamily = MutableStateFlow("") val transportProtocols = MutableStateFlow("") val dnsServers = MutableStateFlow("") val tlsCertificateFile = MutableStateFlow(false) val verifyServer = MutableStateFlow(false) val caFile = MutableStateFlow(false) val userAgent = MutableStateFlow("") val uniqueContactUri = MutableStateFlow(true) val contactsMode = MutableStateFlow("") val ringtoneUri = MutableStateFlow("") val batteryOptimizations = MutableStateFlow(false) val darkTheme = MutableStateFlow(false) val dynamicColors = MutableStateFlow(false) val colorblind = MutableStateFlow(false) val proximitySensing = MutableStateFlow(false) val defaultDialer = MutableStateFlow(false) val debug = MutableStateFlow(false) val sipTrace = MutableStateFlow(false) private var isLoaded = false fun loadSettings(ctx: Context) { if (isLoaded) return else isLoaded = true autoStart.value = Config.variable("auto_start") == "yes" listenAddress.value = Config.variable("sip_listen") val familyValues = listOf("", "ipv4", "ipv6") val itemPosition = mutableIntStateOf(familyValues.indexOf(Config.variable("net_af").lowercase())) addressFamily.value = familyValues[itemPosition.intValue] transportProtocols.value = Config.variable("sip_transports") dnsServers.value = Config.dnsServers() tlsCertificateFile.value = File(BaresipService.filesPath + "/cert.pem").exists() verifyServer.value = Config.variable("sip_verify_server") == "yes" caFile.value = File(BaresipService.filesPath + "/ca_certs.crt").exists() userAgent.value = Config.variable("user_agent") uniqueContactUri.value = Config.variable("sip_cuser_random") == "yes" contactsMode.value = Config.variable("contacts_mode").lowercase() ringtoneUri.value = if (Preferences(ctx).ringtoneUri == "") RingtoneManager.getActualDefaultRingtoneUri(ctx, RingtoneManager.TYPE_RINGTONE).toString() else Preferences(ctx).ringtoneUri!! val powerManager = ctx.getSystemService(POWER_SERVICE) as PowerManager batteryOptimizations.value = !powerManager.isIgnoringBatteryOptimizations(ctx.packageName) darkTheme.value = Preferences(ctx).displayTheme == AppCompatDelegate.MODE_NIGHT_YES dynamicColors.value = BaresipService.dynamicColors.value colorblind.value = Config.variable("colorblind") == "yes" proximitySensing.value = Config.variable("proximity_sensing") == "yes" if (Build.VERSION.SDK_INT >= 29) { val roleManager = ctx.getSystemService(ROLE_SERVICE) as RoleManager defaultDialer.value = roleManager.isRoleHeld(RoleManager.ROLE_DIALER) } debug.value = Config.variable("log_level") == "0" sipTrace.value = BaresipService.sipTrace } } ================================================ FILE: app/src/main/kotlin/com/tutpro/baresip/TaskReceiver.kt ================================================ package com.tutpro.baresip import android.content.BroadcastReceiver import android.content.Context import android.content.Intent import androidx.core.content.ContextCompat class TaskReceiver : BroadcastReceiver() { override fun onReceive(context: Context, intent: Intent) { Log.d(TAG, "TaskReceiver: received intent ${intent.action}") when (intent.action) { "com.tutpro.baresip.REGISTER", "com.tutpro.baresip.UNREGISTER" -> { var aor = intent.getStringExtra("aor") if (aor == null) { Log.i(TAG, "TaskReceiver: 'aor' extra is missing") return } if (!aor.startsWith("sip:")) aor = "sip:$aor" val ua = UserAgent.ofAor(aor) if (ua == null) { Log.i(TAG, "TaskReceiver: user agent of AoR $aor is not found") return } val acc = ua.account if (intent.action == "com.tutpro.baresip.REGISTER") { Log.d(TAG, "TaskReceiver: registering $aor") Api.account_set_regint(acc.accp, REGISTRATION_INTERVAL) Api.ua_register(ua.uap) acc.regint = Api.account_regint(acc.accp) Account.saveAccounts() } else { Log.d(TAG, "TaskReceiver: un-registering $aor") Api.account_set_regint(acc.accp, 0) Api.ua_unregister(ua.uap) acc.regint = Api.account_regint(acc.accp) Account.saveAccounts() } } "com.tutpro.baresip.QUIT" -> { Log.d(TAG, "TaskReceiver: quiting") val baresipService = Intent(context, BaresipService::class.java) if (BaresipService.isServiceRunning) { baresipService.action = "Stop" ContextCompat.startForegroundService(context, baresipService) } } } } } ================================================ FILE: app/src/main/kotlin/com/tutpro/baresip/Theme.kt ================================================ package com.tutpro.baresip import android.app.Activity import android.os.Build.VERSION import androidx.activity.ComponentActivity import androidx.activity.SystemBarStyle import androidx.activity.enableEdgeToEdge import androidx.compose.foundation.isSystemInDarkTheme import androidx.compose.material3.MaterialTheme import androidx.compose.material3.darkColorScheme import androidx.compose.material3.dynamicDarkColorScheme import androidx.compose.material3.dynamicLightColorScheme import androidx.compose.material3.lightColorScheme import androidx.compose.runtime.Composable import androidx.compose.runtime.SideEffect import androidx.compose.runtime.getValue import androidx.compose.runtime.remember import androidx.compose.ui.graphics.toArgb import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalView import androidx.core.graphics.ColorUtils import androidx.core.view.WindowCompat @Composable fun AppTheme( content: @Composable () -> Unit ) { val context = LocalContext.current val useDarkTheme by remember { BaresipService.darkTheme } val useDynamicColors by remember { BaresipService.dynamicColors } val useActualDynamicColors = useDynamicColors && VERSION.SDK_INT >= 31 val isDark = useDarkTheme || isSystemInDarkTheme() val colorScheme = when { useActualDynamicColors -> { if (isDark) dynamicDarkColorScheme(context) else dynamicLightColorScheme(context) } isDark -> darkColorScheme( primary = PrimaryDark, onPrimary = OnPrimaryDark, primaryContainer = PrimaryContainerDark, onPrimaryContainer = OnPrimaryContainerDark, secondary = SecondaryDark, onSecondary = OnSecondaryDark, secondaryContainer = SecondaryContainerDark, onSecondaryContainer = OnSecondaryContainerDark, tertiary = TertiaryDark, onTertiary = OnTertiaryDark, tertiaryContainer = TertiaryContainerDark, onTertiaryContainer = OnTertiaryContainerDark, error = ErrorDark, onError = OnErrorDark, errorContainer = ErrorContainerDark, onErrorContainer = OnErrorContainerDark, background = BackgroundDark, onBackground = OnBackgroundDark, surface = SurfaceDark, onSurface = OnSurfaceDark, surfaceVariant = SurfaceVariantDark, onSurfaceVariant = OnSurfaceVariantDark, surfaceContainer = SurfaceContainerDark, surfaceContainerLow = SurfaceContainerLowDark, surfaceContainerHigh = SurfaceContainerHighDark, surfaceContainerLowest = SurfaceContainerLowestDark, surfaceContainerHighest = SurfaceContainerHighestDark, outline = OutlineDark, outlineVariant = OutlineVariantDark ) else -> lightColorScheme( primary = Primary, onPrimary = OnPrimary, primaryContainer = PrimaryContainer, onPrimaryContainer = OnPrimaryContainer, secondary = Secondary, onSecondary = OnSecondary, secondaryContainer = SecondaryContainer, onSecondaryContainer = OnSecondaryContainer, tertiary = Tertiary, onTertiary = OnTertiary, tertiaryContainer = TertiaryContainer, onTertiaryContainer = OnTertiaryContainer, error = Error, onError = OnError, errorContainer = ErrorContainer, onErrorContainer = OnErrorContainer, background = Background, onBackground = OnBackground, surface = Surface, onSurface = OnSurface, surfaceVariant = SurfaceVariant, onSurfaceVariant = OnSurfaceVariant, surfaceContainer = SurfaceContainer, surfaceContainerLow = SurfaceContainerLow, surfaceContainerHigh = SurfaceContainerHigh, surfaceContainerLowest = SurfaceContainerLowest, surfaceContainerHighest = SurfaceContainerHighest, outline = Outline, outlineVariant = OutlineVariant ) } val view = LocalView.current if (!view.isInEditMode) { SideEffect { val activity = view.context as? ComponentActivity if (activity != null) { activity.enableEdgeToEdge( statusBarStyle = SystemBarStyle.auto( android.graphics.Color.TRANSPARENT, android.graphics.Color.TRANSPARENT, ) { isDark }, navigationBarStyle = SystemBarStyle.auto( android.graphics.Color.TRANSPARENT, android.graphics.Color.TRANSPARENT, ) { isDark } ) } else { val window = (view.context as Activity).window val insetsController = WindowCompat.getInsetsController(window, view) val isBackgroundEffectivelyLight = ColorUtils.calculateLuminance(colorScheme.background.toArgb()) > 0.5 insetsController.isAppearanceLightStatusBars = isBackgroundEffectivelyLight insetsController.isAppearanceLightNavigationBars = isBackgroundEffectivelyLight } } } MaterialTheme( colorScheme = colorScheme, content = content ) } ================================================ FILE: app/src/main/kotlin/com/tutpro/baresip/UserAgent.kt ================================================ package com.tutpro.baresip import com.tutpro.baresip.BaresipService.Companion.circleYellow import com.tutpro.baresip.BaresipService.Companion.colorblind import com.tutpro.baresip.BaresipService.Companion.uas import com.tutpro.baresip.BaresipService.Companion.uasStatus class UserAgent(val uap: Long) { val account = Account(Api.ua_account(uap)) var status = R.drawable.circle_white fun callAlloc(xCall: Long, videoMode: Int): Long { return Api.ua_call_alloc(uap, xCall, videoMode) } fun add() { val updatedUas = uas.value.toMutableList() updatedUas.add(this) uas.value = updatedUas.toList() uasStatus.value = statusMap() } fun remove() { val updatedUas = uas.value.toMutableList() updatedUas.remove(this) uas.value = updatedUas.toList() uasStatus.value = statusMap() } fun updateStatus(status: Int) { uas.value.find { it.uap == this.uap }?.status = status uasStatus.value = statusMap() } fun calls(dir: String = ""): ArrayList { val result = ArrayList() for (c in BaresipService.calls) if ((c.ua == this) && ((dir == "") || c.dir == dir)) result.add(c) return result } // Returns call of UA that was added last or NULL fun currentCall(): Call? { for (c in BaresipService.calls.reversed()) if (c.ua == this) return c return null } fun reRegister() { this.status = circleYellow.getValue(colorblind) if (this.account.regint == 0) Api.ua_unregister(this.uap) else Api.ua_register(this.uap) } fun makeDefault() { val index = uas.value.indexOf(this) val updatedUas = uas.value.toMutableList() updatedUas.removeAt(index) updatedUas.add(0, this) uas.value = updatedUas.toList() uasStatus.value = statusMap() } companion object { fun ofAor(aor: String): UserAgent? { for (ua in uas.value) if (ua.account.aor == aor) return ua return null } fun ofDomain(domain: String): UserAgent? { for (ua in uas.value) if (Utils.aorDomain(ua.account.aor) == domain) return ua return null } fun ofUap(uap: Long): UserAgent? { for (ua in uas.value) if (ua.uap == uap) return ua return null } fun uaAlloc(uri: String): UserAgent? { val uap = Api.ua_alloc(uri) if (uap != 0L) return UserAgent(uap) Log.e(TAG, "Failed to allocate UserAgent for $uri") return null } fun findAorIndex(aor: String): Int? { for (i in uas.value.indices) { if (uas.value[i].account.aor == aor) return i } return null } fun register() { for (ua in uas.value) { if (ua.account.regint > 0) { if (Api.ua_register(ua.uap) != 0) Log.d(TAG, "Failed to register ${ua.account.aor}") } } } fun updateColorblindStatus() { val updatedUas = uas.value.toMutableList() for (ua in updatedUas) ua.status = if (colorblind) when (ua.status) { R.drawable.circle_green -> R.drawable.circle_green_blind R.drawable.circle_yellow -> R.drawable.circle_yellow_blind R.drawable.circle_red -> R.drawable.circle_red_blind else -> R.drawable.circle_white } else when (ua.status) { R.drawable.circle_green_blind -> R.drawable.circle_green R.drawable.circle_yellow_blind -> R.drawable.circle_yellow R.drawable.circle_red_blind -> R.drawable.circle_red else -> R.drawable.circle_white } uas.value = updatedUas.toList() uasStatus.value = statusMap() } fun statusMap(): Map { val result = emptyMap().toMutableMap() for (ua in uas.value) result[ua.account.aor] = ua.status return result } } } ================================================ FILE: app/src/main/kotlin/com/tutpro/baresip/Utils.kt ================================================ package com.tutpro.baresip import android.annotation.SuppressLint import android.app.Activity import android.app.KeyguardManager import android.content.ContentResolver import android.content.Context import android.content.Intent import android.content.pm.PackageManager import android.content.res.Configuration import android.graphics.Bitmap import android.graphics.PorterDuff import android.graphics.PorterDuffXfermode import android.media.AudioAttributes import android.media.AudioDeviceInfo import android.media.AudioManager import android.media.MediaPlayer import android.media.audiofx.AcousticEchoCanceler import android.media.audiofx.AutomaticGainControl import android.net.Uri import android.net.wifi.WifiManager import android.os.Build import android.os.Bundle import android.os.Environment import android.provider.DocumentsContract import android.provider.MediaStore import android.provider.OpenableColumns import android.text.format.DateUtils import androidx.activity.result.ActivityResultLauncher import androidx.annotation.RequiresApi import androidx.appcompat.app.AppCompatDelegate import androidx.compose.ui.text.AnnotatedString import androidx.compose.ui.text.SpanStyle import androidx.compose.ui.text.buildAnnotatedString import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.withStyle import androidx.core.content.ContextCompat import androidx.core.graphics.toColorInt import androidx.core.net.toUri import androidx.core.text.isDigitsOnly import androidx.lifecycle.Lifecycle import androidx.lifecycle.ProcessLifecycleOwner import androidx.navigation.NavController import com.tutpro.baresip.Call.Companion.inCall import java.io.BufferedInputStream import java.io.BufferedOutputStream import java.io.File import java.io.FileInputStream import java.io.FileNotFoundException import java.io.FileOutputStream import java.io.IOException import java.io.InputStream import java.io.ObjectInputStream import java.io.ObjectOutputStream import java.io.RandomAccessFile import java.io.Serializable import java.lang.reflect.Method import java.net.InetAddress import java.net.NetworkInterface import java.net.SocketException import java.security.KeyStore import java.security.SecureRandom import java.security.cert.CertificateException import java.security.cert.CertificateFactory import java.security.cert.X509Certificate import java.text.DateFormat import java.util.Calendar import java.util.Enumeration import java.util.GregorianCalendar import java.util.Locale import java.util.Random import java.util.concurrent.Executor import java.util.zip.ZipEntry import java.util.zip.ZipFile import java.util.zip.ZipOutputStream import javax.crypto.Cipher import javax.crypto.SecretKeyFactory import javax.crypto.spec.IvParameterSpec import javax.crypto.spec.PBEKeySpec import javax.crypto.spec.SecretKeySpec import javax.net.ssl.HttpsURLConnection import javax.net.ssl.SSLContext import javax.net.ssl.TrustManagerFactory import javax.net.ssl.X509TrustManager object Utils { fun getNameValue(string: String, name: String): ArrayList { val lines = string.split("\n") val result = ArrayList() for (line in lines) { if (line.startsWith(name)) result.add((line.substring(name.length).trim()).split(" \t")[0]) } return result } fun removeLinesStartingWithString(lines: String, string: String): String { var result = "" for (line in lines.split("\n")) if (!line.startsWith(string) && (line.isNotEmpty())) result += line + "\n" return result } fun uriHostPart(uri: String): String { return if (uri.contains("@")) { uri.substringAfter("@") .substringBefore(":") .substringBefore(";") .substringBefore("?") .substringBefore(">") } else { val parts = uri.split(":") when (parts.size) { 2 -> parts[1].substringBefore(";") .substringBefore("?") .substringBefore(">") 3 -> parts[1] else -> "" } } } fun uriUserPart(uri: String): String { return if (uri.contains("@")) uri.substringAfter(":").substringBefore("@") else "" } fun uriMatch(firstUri: String, secondUri: String): Boolean { if (firstUri.startsWith("tel:")) return firstUri == secondUri || firstUri.substringAfter(":") == uriUserPart(secondUri) if (firstUri.startsWith("sip:")) return uriUserPart(firstUri) == uriUserPart(secondUri) && uriHostPart(firstUri) == uriHostPart(secondUri) return false } private fun uriParams(uri: String): List { val params = uri.split(";") return if (params.size == 1) listOf() else params.subList(1, params.size) } fun friendlyUri(ctx: Context, uri: String, account: Account, e164Check: Boolean = true): String { var u = Contact.contactName(uri) if (u != uri) return u if (e164Check) { val e164Uri = e164Uri(uri, account.countryCode) u = Contact.contactName(e164Uri) if (u != e164Uri) return u } u = u.replace("%23", "#") if (u.contains("@")) { val user = uriUserPart(u) val host = uriHostPart(u) val params = uriParams(u).filter{it != "transport=udp"} return if (host == aorDomain(account.aor) || params.contains("user=phone")) user else if (host == "anonymous.invalid") ctx.getString(R.string.anonymous) else if (host == "unknown.invalid") ctx.getString(R.string.unknown) else if (params.isEmpty()) "$user@$host" else "$user@$host;" + params.joinToString(";") } if (uri.startsWith("<") && (uri.endsWith(">"))) u = uri.substring(1).substringBeforeLast(">") u = u.substringBefore("?") u = u.replace(":5060", "") u = u.replace(";transport=udp", "", true) return u } private fun e164Uri(uri: String, countryCode: String): String { if (countryCode == "") return uri val scheme = uri.take(4) val userPart = uriUserPart(uri) return if (userPart.isDigitsOnly()) { when { userPart.startsWith("00") -> uri.replace("$scheme$userPart", scheme + userPart.substring(2)) userPart.startsWith("0") -> uri.replace("${scheme}0", "$scheme$countryCode") else -> uri.replace(scheme, "$scheme$countryCode") } } else uri } fun uriComplete(uri: String, aor: String): String { val res = if (!uri.startsWith("sip:")) "sip:$uri" else uri return if (checkUriUser(uri)) "$res@${aorDomain(aor)}" else res } private fun String.replace(vararg pairs: Pair): String = pairs.fold(this) { acc, (old, new) -> acc.replace(old, new, ignoreCase = true) } fun uriUnescape(uri: String): String { return uri.replace("%2B" to "+", "%3A" to ":", "%3B" to ";", "%40" to "@", "%3D" to "=") } fun aorDomain(aor: String): String { return uriHostPart(aor) } fun plainAor(aor: String): String { return uriUserPart(aor) + "@" + uriHostPart(aor) } fun checkAor(aor: String): Boolean { if (!checkSipUri(aor)) return false val params = uriParams(aor) return params.isEmpty() || ((params.size == 1) && params[0] in arrayOf("transport=udp", "transport=tcp", "transport=tls")) } private fun checkTransport(transport: String, transports: Set): Boolean { return transport.split("=")[0] == "transport" && transport.split("=")[1].lowercase() in transports } fun checkStunUri(uri: String): Boolean { if (uri.substringBefore(":").lowercase() !in setOf("stun", "stuns", "turn", "turns")) return false return checkHostPort(uri.substringAfter(":").substringBefore("?")) && (uri.indexOf("?") == -1 || checkTransport(uri.substringAfter("?"), setOf("udp", "tcp"))) } fun checkIpV4(ip: String): Boolean { return Regex("^(([0-1]?[0-9]{1,2}\\.)|(2[0-4][0-9]\\.)|(25[0-5]\\.)){3}(([0-1]?[0-9]{1,2})|(2[0-4][0-9])|(25[0-5]))$").matches(ip) } private fun checkIpV6(ip: String): Boolean { return Regex("^(([0-9a-fA-F]{0,4}:){1,7}[0-9a-fA-F]{0,4})$").matches(ip) } private fun checkIpv6InBrackets(bracketedIp: String): Boolean { return bracketedIp.startsWith("[") && bracketedIp.endsWith("]") && checkIpV6(bracketedIp.substring(1, bracketedIp.length - 2)) } fun checkUriUser(user: String): Boolean { val escaped = """%(\d|A|B|C|D|E|F|a|b|c|d|e|f){2}""".toRegex() escaped.replace(user, "").forEach { if (!(it.isLetterOrDigit() || "-_.!~*\'()&=+$,;?/".contains(it))) return false } return user.isNotEmpty() && !checkIpV4(user) && !checkIpV6(user) } fun checkDomain(domain: String): Boolean { val parts = domain.split(".") for (p in parts) { if (p.endsWith("-") || p.startsWith("-") || !Regex("^[-a-zA-Z0-9]+$").matches(p)) return false } return true } private fun checkPort(port: String): Boolean { val number = port.toIntOrNull() ?: return false return (number > 0) && (number < 65536) } fun checkIpPort(ipPort: String): Boolean { return if (ipPort.startsWith("[")) checkIpv6InBrackets(ipPort.substringBeforeLast(":")) && checkPort(ipPort.substringAfterLast(":")) else checkIpV4(ipPort.substringBeforeLast(":")) && checkPort(ipPort.substringAfterLast(":")) } private fun checkDomainPort(domainPort: String): Boolean { return checkDomain(domainPort.substringBeforeLast(":")) && checkPort(domainPort.substringAfterLast(":")) } private fun checkHostPort(hostPort: String): Boolean { return checkIpV4(hostPort) || checkIpv6InBrackets(hostPort) || checkDomain(hostPort) || checkIpPort(hostPort) || checkDomainPort(hostPort) } private fun checkParams(params: String): Boolean { for (param in params.split(";")) if (!checkParam(param)) return false return true } private fun checkParam(param: String): Boolean { val nameValue = param.split("=") if (nameValue.size == 1) return checkParamChars(nameValue[0]) if (nameValue.size == 2) { if (nameValue[0] == "transport") return setOf("udp", "tcp", "tls", "wss").contains(nameValue[1].lowercase()) return checkParamChars(nameValue[1]) } return false } private fun checkParamChars(s: String): Boolean { // Does not currently allow escaped characters val allowed = "[]/:&+$-_.!~*'()" for (c in s) if (!allowed.contains(c) && !c.isLetterOrDigit()) return false return true } fun paramValue(params: String, name: String): String { if (params == "") return "" for (param in params.split(";")) if (param.substringBefore("=") == name) return param.substringAfter("=") return "" } fun paramExists(params: String, name: String): Boolean { for (param in params.split(";")) if (param.substringBefore("=") == name) return true return false } fun checkHostPortParams(hpp: String) : Boolean { val restParams = hpp.split(";", limit = 2) return if (restParams.size == 1) checkHostPort(restParams[0]) else checkHostPort(restParams[0]) && checkParams(restParams[1]) } private fun checkSipUri(uri: String): Boolean { return if (uri.startsWith("sip:")) { val userRest = uri.substring(4).split("@") when (userRest.size) { 1 -> checkHostPortParams(userRest[0]) 2 -> checkUriUser(userRest[0]) && checkHostPortParams(userRest[1]) else -> false } } else { false } } fun isTelNumber(no: String): Boolean { return no.isNotEmpty() && Regex("^([+][1-9])?[0-9- (),*#]{0,24}$").matches(no) } fun isTelUri(uri: String): Boolean { return uri.startsWith("tel:") && isTelNumber(uri.substring(4)) } fun checkUri(uri: String): Boolean { return checkSipUri(uri) || isTelUri(uri) } fun telToSip(telUri: String, account: Account): String { val hostPart = if (account.telProvider != "") account.telProvider else aorDomain(account.aor) return "sip:" + telUri.substring(4) .filterNot{setOf('-', ' ', '(', ')').contains(it)} .replace("#", "%23") + "@" + hostPart + ";user=phone" } fun checkName(name: String): Boolean { return name.isNotEmpty() && name == String(name.toByteArray(), Charsets.UTF_8) && name.lines().size == 1 && !name.contains('"') } fun checkCountryCode(cc: String): Boolean { return cc.startsWith("+") && cc.length > 1 && cc.length < 5 && cc.substring(1).isDigitsOnly() && cc[1] != '0' } fun checkServerVal(server: String): Boolean { val parts = server.replace(Regex("[(][^()\\\\]+[)]"), "") .trim().split("\\s+".toRegex()) for (part in parts) if (!checkProduct(part)) return false return true } private fun checkProduct(product: String): Boolean { val parts = product.split("/", limit = 2) return if (parts.count() == 2) checkToken(parts[0]) && checkToken(parts[1]) else checkToken(parts[0]) } private fun checkToken(token: String): Boolean { return Regex("^[-a-zA-Z0-9.!%*_+`'~]+$").matches(token) } @Suppress("unused") fun checkIfName(name: String): Boolean { if ((name.length < 2) || !name.first().isLetter()) return false for (c in name) if (!c.isLetterOrDigit()) return false return true } fun implode(list: List, sep: String): String { var res = "" for (s in list) { res = if (res == "") s else res + sep + s } return res } fun unaccent(input: String): String { val normalized = java.text.Normalizer.normalize(input, java.text.Normalizer.Form.NFD) return "\\p{InCombiningDiacriticalMarks}+".toRegex().replace(normalized, "") } fun buildAnnotatedStringWithHighlight(name: String, query: String): AnnotatedString { val normalizedName = unaccent(name) val normalizedQuery = unaccent(query) val startIndex = normalizedName.indexOf(normalizedQuery, ignoreCase = true) return if (startIndex == -1) { buildAnnotatedString { append(name) } } else { buildAnnotatedString { append(name.take(startIndex)) withStyle(style = SpanStyle(fontWeight = FontWeight.Bold)) { append(name.drop(startIndex).take(normalizedQuery.length)) } append(name.drop(startIndex + normalizedQuery.length)) } } } fun isVisible(): Boolean { return ProcessLifecycleOwner.get().lifecycle.currentState.isAtLeast(Lifecycle.State.STARTED) } fun isHotSpotOn(wm: WifiManager): Boolean { try { val method: Method = wm.javaClass.getDeclaredMethod("isWifiApEnabled") method.isAccessible = true return method.invoke(wm) as Boolean } catch (_: Throwable) { } return false } fun hotSpotAddresses(): Map { val result = mutableMapOf() try { val interfaces: Enumeration = NetworkInterface.getNetworkInterfaces() while (interfaces.hasMoreElements()) { val iface: NetworkInterface = interfaces.nextElement() val ifName = iface.name Log.d(TAG, "Found interface with name $ifName") if (ifName.startsWith("ap") || ifName.contains("wlan")) { val addresses: Enumeration = iface.inetAddresses while (addresses.hasMoreElements()) { val inetAddress: InetAddress = addresses.nextElement() if (inetAddress.isSiteLocalAddress) result[inetAddress.hostAddress!!] = ifName } if (result.isNotEmpty()) return result } } } catch (ex: SocketException) { Log.e(TAG, "hotSpotAddresses SocketException: $ex") } catch (ex: NullPointerException) { Log.e(TAG, "hotSpotAddresses NullPointerException: $ex") } return result } fun checkPermissions(ctx: Context, permissions: Array) : Boolean { for (p in permissions) { if (ContextCompat.checkSelfPermission(ctx, p) != PackageManager.PERMISSION_GRANTED) { Log.d(TAG, "Permission $p is denied") return false } else { Log.d(TAG, "Permission $p is granted") } } return true } fun copyAssetToFile(context: Context, asset: String, path: String) { try { val `is` = context.assets.open(asset) val os = FileOutputStream(path) val buffer = ByteArray(512) var byteRead: Int = `is`.read(buffer) while (byteRead != -1) { os.write(buffer, 0, byteRead) byteRead = `is`.read(buffer) } os.close() `is`.close() } catch (e: IOException) { Log.e(TAG, "Failed to copy asset '$asset' to file: $e") } } fun deleteFile(file: File) { if (file.exists()) { try { file.delete() } catch (e: IOException) { Log.e(TAG, "Could not delete file ${file.absolutePath}: $e") } } } fun deleteFile(ctx: Context, uri: Uri): Boolean { val contentResolver: ContentResolver = ctx.contentResolver try { if (DocumentsContract.isDocumentUri(ctx, uri)) { if (DocumentsContract.deleteDocument(contentResolver, uri)) { Log.d(TAG, "File deleted successfully: $uri") return true } else { Log.d(TAG, "File not found or could not be deleted: $uri") return false } } else { Log.d(TAG, "Uri is not a document uri: $uri") return false } } catch (e: UnsupportedOperationException) { Log.w(TAG, "Error deleting file $uri: $e") return false } catch (e: Exception) { Log.e(TAG, "Error deleting file $uri: $e") return false } } fun getFileContents(filePath: String): ByteArray? { return try { File(filePath).readBytes() } catch (e: FileNotFoundException) { Log.e(TAG, "File '$filePath' not found: ${e.printStackTrace()}") null } catch (e: Exception) { Log.e(TAG, "Failed to read file '$filePath': ${e.printStackTrace()}") null } } fun putFileContents(filePath: String, contents: ByteArray): Boolean { try { File(filePath).writeBytes(contents) } catch (e: IOException) { Log.e(TAG, "Failed to write file '$filePath': $e") return false } return true } fun File.copyInputStreamToFile(inputStream: InputStream): Boolean { try { this.outputStream().use { fileOut -> inputStream.copyTo(fileOut) } return true } catch (e: IOException) { Log.e(TAG, "Failed to write file '${this.absolutePath}': $e") } return false } @RequiresApi(29) fun selectInputFile(request: ActivityResultLauncher) { val intent = Intent(Intent.ACTION_OPEN_DOCUMENT).apply { addCategory(Intent.CATEGORY_OPENABLE) type = "*/*" putExtra(DocumentsContract.EXTRA_INITIAL_URI, MediaStore.Downloads.EXTERNAL_CONTENT_URI) } request.launch(intent) } @RequiresApi(29) @Suppress("unused") fun selectOutputFile(title: String) { Intent(Intent.ACTION_CREATE_DOCUMENT).apply { addCategory(Intent.CATEGORY_OPENABLE) type = "application/octet-stream" putExtra(Intent.EXTRA_TITLE, title) putExtra(DocumentsContract.EXTRA_INITIAL_URI, MediaStore.Downloads.EXTERNAL_CONTENT_URI) } } fun downloadsPath(fileName: String): String { return Environment.getExternalStoragePublicDirectory( Environment.DIRECTORY_DOWNLOADS).path + "/$fileName" } fun fileNameOfUri(ctx: Context, uri: Uri): String { val cursor = ctx.contentResolver.query(uri, null, null, null, null) var name = "" if (cursor != null) { val index = cursor.getColumnIndex(OpenableColumns.DISPLAY_NAME) cursor.moveToFirst() name = cursor.getString(index) cursor.close() } return if (name == "") "$uri".substringAfterLast("/") else name } class Crypto(val salt: ByteArray, val iter: Int, val iv: ByteArray, val data: ByteArray): Serializable { companion object { @Suppress("unused") private const val serialVersionUID: Long = -29238082928391L } } private fun encrypt(content: ByteArray, password: CharArray): ByteArray? { fun intToByteArray(int: Int): ByteArray { val bytes = ByteArray(2) bytes[0] = (int shr 0).toByte() bytes[1] = (int shr 8).toByte() return bytes } try { val sr = SecureRandom() val salt = ByteArray(128) sr.nextBytes(salt) val iterationCount = Random().nextInt(1024) + 512 val pbKeySpec = PBEKeySpec(password, salt, iterationCount, 128) val secretKeyFactory = SecretKeyFactory.getInstance("PBKDF2WithHmacSHA1") val keyBytes = secretKeyFactory.generateSecret(pbKeySpec).encoded val keySpec = SecretKeySpec(keyBytes, "AES") val ivRandom = SecureRandom() val iv = ByteArray(16) ivRandom.nextBytes(iv) val ivSpec = IvParameterSpec(iv) val cipher = Cipher.getInstance("AES/CBC/PKCS7Padding") cipher.init(Cipher.ENCRYPT_MODE, keySpec, ivSpec) val cipherData = cipher.doFinal(content) val res = ByteArray(128 + 2 + 16 + cipherData.size) salt.copyInto(res, 0) intToByteArray(iterationCount).copyInto(res, salt.size) iv.copyInto(res, salt.size + 2) cipherData.copyInto(res, salt.size + 2 + iv.size) return res } catch (e: Exception) { Log.e(TAG, "Encrypt failed: ${e.printStackTrace()}") } return null } private fun decrypt(content: ByteArray, password: CharArray): ByteArray? { fun byteArrayToInt(bytes: ByteArray) : Int { return (bytes[1].toInt() and 0xff shl 8) or (bytes[0].toInt() and 0xff) } try { val salt = content.copyOfRange(0, 128) val iterationCount = byteArrayToInt(content.copyOfRange(128, 130)) val iv = content.copyOfRange(130, 146) val data = content.copyOfRange(146, content.size) val pbKeySpec = PBEKeySpec(password, salt, iterationCount, 128) val secretKeyFactory = SecretKeyFactory.getInstance("PBKDF2WithHmacSHA1") val keyBytes = secretKeyFactory.generateSecret(pbKeySpec).encoded val keySpec = SecretKeySpec(keyBytes, "AES") val cipher = Cipher.getInstance("AES/CBC/PKCS7Padding") val ivSpec = IvParameterSpec(iv) cipher.init(Cipher.DECRYPT_MODE, keySpec, ivSpec) return cipher.doFinal(data) } catch (e: Exception) { Log.e(TAG, "Decrypt failed: ${e.printStackTrace()}") } return null } private fun decryptOld(obj: Crypto, password: CharArray): ByteArray? { var plainData: ByteArray? = null try { val pbKeySpec = PBEKeySpec(password, obj.salt, obj.iter, 128) val secretKeyFactory = SecretKeyFactory.getInstance("PBKDF2WithHmacSHA1") val keyBytes = secretKeyFactory.generateSecret(pbKeySpec).encoded val keySpec = SecretKeySpec(keyBytes, "AES") val cipher = Cipher.getInstance("AES/CBC/PKCS7Padding") val ivSpec = IvParameterSpec(obj.iv) cipher.init(Cipher.DECRYPT_MODE, keySpec, ivSpec) plainData = cipher.doFinal(obj.data) } catch (e: Exception) { Log.e(TAG, "Decrypt failed: ${e.printStackTrace()}") } return plainData } fun encryptToUri(ctx: Context, uri: Uri, content: ByteArray, password: String): Boolean { val obj = encrypt(content, password.toCharArray()) val stream = ctx.contentResolver.openOutputStream(uri) as FileOutputStream try { ObjectOutputStream(stream).use { it.writeObject(obj) } } catch (e: Exception) { Log.w(TAG, "encryptToUri failed: $e") return false } return true } fun decryptFromUri(ctx: Context, uri: Uri, password: String): ByteArray? { var plainData: ByteArray? = null var stream: FileInputStream try { stream = ctx.contentResolver.openInputStream(uri) as FileInputStream } catch(e: Exception) { Log.w(TAG, "decryptFromUri could not open stream: $e") return null } try { ObjectInputStream(stream).use { val content = it.readObject() as ByteArray plainData = decrypt(content, password.toCharArray()) } stream.close() } catch (e: Exception) { Log.w(TAG, "decryptFromUri as ByteArray failed: $e") stream.close() try { stream = ctx.contentResolver.openInputStream(uri) as FileInputStream ObjectInputStream(stream).use { val obj = it.readObject() as Crypto plainData = decryptOld(obj, password.toCharArray()) } stream.close() } catch (e: Exception) { Log.w(TAG, "decryptFromUri as Crypto failed: $e") } } return plainData } fun zip(fileNames: ArrayList, zipFileName: String): Boolean { val zipFilePath = BaresipService.filesPath + "/" + zipFileName try { ZipOutputStream(BufferedOutputStream(FileOutputStream(zipFilePath))).use { out -> val data = ByteArray(1024) for (fileName in fileNames) { val filePath = BaresipService.filesPath + "/" + fileName if (File(filePath).exists()) { FileInputStream(filePath).use { fi -> BufferedInputStream(fi).use { origin -> val entry = ZipEntry(fileName) out.putNextEntry(entry) while (true) { val readBytes = origin.read(data) if (readBytes == -1) break out.write(data, 0, readBytes) } } } } } } } catch (e: IOException) { Log.e(TAG, "Failed to zip file '$zipFilePath': $e") return false } return true } fun unZip(zipFilePath: String): Boolean { val allFiles = listOf("accounts", "call_history", "config", "contacts", "messages", "uuid", "gzrtp.zid", "cert.pem", "ca_cert", "ca_certs.crt") val zipFiles = mutableListOf() try { ZipFile(File(zipFilePath)).use { zip -> zip.entries().asSequence().forEach { entry -> val entryName = if (entry.name.startsWith("/")) entry.name.substringAfterLast("/") else entry.name zipFiles.add(entryName) zip.getInputStream(entry).use { input -> File(BaresipService.filesPath + "/" + entryName).outputStream().use { output -> input.copyTo(output) } } } } } catch (e: IOException) { Log.e(TAG, "Failed to unzip file '$zipFilePath': $e") return false } (allFiles - zipFiles.toSet()).iterator().forEach { deleteFile(File(BaresipService.filesPath, it)) } return true } @Suppress("unused") fun dumpIntent(intent: Intent) { val bundle: Bundle = intent.extras ?: return val keys = bundle.keySet() val it = keys.iterator() Log.d(TAG, "Dumping intent start") while (it.hasNext()) { val key = it.next() Log.d(TAG, "[" + key + "=" + bundle.getBundle(key) + "]") } Log.d(TAG, "Dumping intent finish") } fun randomColor(): Int { val rnd = Random() return android.graphics.Color.argb(255, rnd.nextInt(256), rnd.nextInt(256), rnd.nextInt(256)) } fun requestDismissKeyguard(activity: Activity) { val kgm = activity.getSystemService(Context.KEYGUARD_SERVICE) as KeyguardManager kgm.requestDismissKeyguard(activity, null) } fun isThemeDark(ctx: Context) : Boolean { return Preferences(ctx).displayTheme == AppCompatDelegate.MODE_NIGHT_YES || ctx.resources?.configuration?.uiMode?.and(Configuration.UI_MODE_NIGHT_MASK) == Configuration.UI_MODE_NIGHT_YES } fun isPSTNCallActive(ctx: Context): Boolean { // MODE_IN_CALL indicates a PSTN call is active val am = ctx.getSystemService(Context.AUDIO_SERVICE) as AudioManager return am.mode == AudioManager.MODE_IN_CALL } fun relativeTime(ctx: Context, time: GregorianCalendar): String { return if (DateUtils.isToday(time.timeInMillis)) { val fmt = DateFormat.getTimeInstance(DateFormat.SHORT) ctx.getString(R.string.today) + "\n" + fmt.format(time.time) } else { val month = time.getDisplayName(Calendar.MONTH, Calendar.SHORT, Locale.getDefault())!! .replaceFirstChar { if (it.isLowerCase()) it.titlecase() else it.toString() } val day = time.get(Calendar.DAY_OF_MONTH) val currentYear = Calendar.getInstance().get(Calendar.YEAR) if (time.get(Calendar.YEAR) == currentYear) { val fmt = DateFormat.getTimeInstance(DateFormat.SHORT) "$month $day" + "\n" + fmt.format(time.time) } else { "$month $day" + "\n" + time.get(Calendar.YEAR) } } } private fun setSpeakerPhone(executor: Executor, am: AudioManager, enable: Boolean) { if (Build.VERSION.SDK_INT >= 31) { if (!enable) { Log.d(TAG, "Disabling speakerphone") clearCommunicationDevice(am) if (inCall() && am.mode == AudioManager.MODE_NORMAL) { Log.d(TAG, "Restoring MODE_IN_COMMUNICATION") am.mode = AudioManager.MODE_IN_COMMUNICATION } return } val current = am.communicationDevice!!.type Log.d(TAG, "Current com dev/mode is $current/${am.mode}") var speakerDevice: AudioDeviceInfo? = null for (device in am.availableCommunicationDevices) if (device.type == AudioDeviceInfo.TYPE_BUILTIN_SPEAKER) { speakerDevice = device break } if (speakerDevice == null) { Log.w(TAG,"Could not find requested communication device") return } if (current != speakerDevice.type) { // Currently at API levels 31+, speakerphone needs normal mode if (am.mode == AudioManager.MODE_NORMAL) { Log.d(TAG, "Setting com device to ${speakerDevice.type} in MODE_NORMAL") if (!am.setCommunicationDevice(speakerDevice)) Log.e(TAG, "Could not set com device") } else { val normalListener = object : AudioManager.OnModeChangedListener { override fun onModeChanged(mode: Int) { if (mode == AudioManager.MODE_NORMAL) { am.removeOnModeChangedListener(this) Log.d( TAG, "Setting com device to ${speakerDevice.type}" + " in mode ${am.mode}" ) if (!am.setCommunicationDevice(speakerDevice)) Log.e(TAG, "Could not set com device") } } } am.addOnModeChangedListener(executor, normalListener) Log.d(TAG, "Setting mode to NORMAL") am.mode = AudioManager.MODE_NORMAL } Log.d(TAG, "New com device/mode is ${am.communicationDevice!!.type}/${am.mode}") } } else { @Suppress("DEPRECATION") am.isSpeakerphoneOn = enable Log.d(TAG, "Speakerphone is $enable") } } fun toggleSpeakerPhone(executor: Executor, am: AudioManager) { if (Build.VERSION.SDK_INT >= 31) { if (am.communicationDevice!!.type == AudioDeviceInfo.TYPE_BUILTIN_SPEAKER) setSpeakerPhone(executor, am, false) else setSpeakerPhone(executor, am, true) } else { @Suppress("DEPRECATION") setSpeakerPhone(executor, am, !am.isSpeakerphoneOn) } } fun clearCommunicationDevice(am: AudioManager) { if (Build.VERSION.SDK_INT >= 31) { am.clearCommunicationDevice() } else { @Suppress("DEPRECATION") if (am.isSpeakerphoneOn) am.isSpeakerphoneOn = false } } @Suppress("unused") fun playFile(ctx: Context, path: String) { Log.d(TAG, "Playing file $path") MediaPlayer().apply { setAudioAttributes( AudioAttributes.Builder() .setContentType(AudioAttributes.CONTENT_TYPE_MUSIC) .setUsage(AudioAttributes.USAGE_MEDIA) .build() ) setOnPreparedListener { Log.d(TAG, "Starting MediaPlayer") it.start() Log.d(TAG, "MediaPlayer started") } setOnCompletionListener { Log.d(TAG, "Stopping MediaPlayer") it.stop() it.release() } try { Log.d(TAG, "Preparing $path") setDataSource(ctx, path.toUri()) prepareAsync() } catch (e: IllegalArgumentException) { Log.e(TAG, "MediaPlayer IllegalArgumentException: ${e.printStackTrace()}") } catch (e: IOException) { Log.e(TAG, "MediaPlayer IOException: ${e.printStackTrace()}") } catch (e: Exception) { Log.e(TAG, "MediaPlayer Exception: ${e.printStackTrace()}") } } } fun aecAgcCheck() { val sessionId = Api.AAudio_open_stream() if (sessionId == -1) { Log.e(TAG, "Failed to open AAudio stream") return } if (AcousticEchoCanceler.isAvailable()) { val aec = AcousticEchoCanceler.create(sessionId) if (aec != null) { BaresipService.aecAvailable = true aec.release() Log.i(TAG, "Creation of hardware AEC for $sessionId succeeded") } else { Log.w(TAG, "Creation of hardware AEC for $sessionId failed") } } else Log.i(TAG, "Hardware AEC is NOT available") if (AutomaticGainControl.isAvailable()) { val agc = AutomaticGainControl.create(sessionId) if (agc != null) { BaresipService.agcAvailable = true agc.release() Log.d(TAG, "Creation of hardware AGC for $sessionId succeeded") } else { Log.w(TAG, "Creation of hardware AGC for $sessionId failed") } } else Log.i(TAG, "Hardware AGC is NOT available") Api.AAudio_close_stream() } fun readUrlWithCustomCAs(urlConnection: HttpsURLConnection, caFile: File): String? { if (!caFile.exists()) { Log.d("Utils", "Custom CA file not found at ${caFile.path}") return null } try { // Create a TrustManager that trusts the CAs in the user-provided file val customTrustManager = fun(): X509TrustManager { val certificateFactory = CertificateFactory.getInstance("X.509") val certificateInputStream = caFile.inputStream() val certificates = certificateFactory.generateCertificates(certificateInputStream) certificateInputStream.close() val keyStore = KeyStore.getInstance(KeyStore.getDefaultType()) keyStore.load(null, null) certificates.forEachIndexed { index, certificate -> keyStore.setCertificateEntry("user_ca_$index", certificate) } val tmf = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm()) tmf.init(keyStore) return tmf.trustManagers.find { it is X509TrustManager } as X509TrustManager }() // Create a TrustManager that trusts the default system CAs val systemTrustManager = fun(): X509TrustManager { val factory = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm()) factory.init(null as KeyStore?) // A null keystore loads the system's default CAs return factory.trustManagers.find { it is X509TrustManager } as X509TrustManager }() // Create a composite TrustManager that delegates to both system and custom CAs @SuppressLint("CustomX509TrustManager") val compositeTrustManager = object : X509TrustManager { override fun checkClientTrusted(chain: Array?, authType: String?) { // Delegate to the system manager by default. systemTrustManager.checkClientTrusted(chain, authType) } override fun checkServerTrusted(chain: Array?, authType: String?) { try { // Try to validate the chain with the system's default TrustManager. systemTrustManager.checkServerTrusted(chain, authType) } catch (_: CertificateException) { // If that fails, and only if that fails, try to validate with our custom TrustManager. // This will throw the final CertificateException if it also fails. customTrustManager.checkServerTrusted(chain, authType) } } override fun getAcceptedIssuers(): Array { // Return a combined list of issuers from both trust managers. return systemTrustManager.acceptedIssuers + customTrustManager.acceptedIssuers } } // Create an SSLContext that uses our new composite TrsustManager val sslContext = SSLContext.getInstance("TLS") sslContext.init(null, arrayOf(compositeTrustManager), null) // Tell HttpsURLConnection to use our custom SSLContext for this connection urlConnection.sslSocketFactory = sslContext.socketFactory // Proceed with the connection and return the result return urlConnection.inputStream.bufferedReader().use { it.readText() } } catch (e: Exception) { // Catch any exception from certificate loading or from the network connection Log.e("Utils", "readUrlWithCustomCa failed: ${e.message}") return null } } fun Bitmap.toCircle(): Bitmap { // Use full package names to avoid conflict with Compose classes val output = androidx.core.graphics.createBitmap(this.width, this.height, Bitmap.Config.ARGB_8888) val canvas = android.graphics.Canvas(output) val paint = android.graphics.Paint() val rect = android.graphics.Rect(0, 0, this.width, this.height) paint.isAntiAlias = true canvas.drawARGB(0, 0, 0, 0) canvas.drawCircle(this.width / 2f, this.height / 2f, this.width / 2f, paint) paint.xfermode = PorterDuffXfermode(PorterDuff.Mode.SRC_IN) canvas.drawBitmap(this, rect, rect, paint) return output } fun createTextAvatar(letter: String, colorHex: String): Bitmap { // Use decent resolution 128x128 and let Android scale it down. val size = 128 // Use KTX createBitmap to match toCircle style val bitmap = androidx.core.graphics.createBitmap(size, size, Bitmap.Config.ARGB_8888) // Use Fully Qualified Names to avoid Compose conflicts val canvas = android.graphics.Canvas(bitmap) // Draw the colored circle background val bgPaint = android.graphics.Paint() bgPaint.isAntiAlias = true try { bgPaint.color = colorHex.toColorInt() } catch (_: Exception) { bgPaint.color = android.graphics.Color.GRAY // Fallback color } canvas.drawCircle(size / 2f, size / 2f, size / 2f, bgPaint) // Draw the text (Initial) val textPaint = android.graphics.Paint() textPaint.isAntiAlias = true textPaint.color = android.graphics.Color.WHITE textPaint.textSize = size / 2f // Text size is half the circle size textPaint.textAlign = android.graphics.Paint.Align.CENTER // Use a bold font if possible textPaint.typeface = android.graphics.Typeface.create(android.graphics.Typeface.DEFAULT, android.graphics.Typeface.BOLD) // Calculate vertical center to center the text properly val bounds = android.graphics.Rect() textPaint.getTextBounds(letter, 0, letter.length, bounds) val yOffset = (bounds.bottom - bounds.top) / 2f // Draw text at Center X, Center Y + half text height (to visually center) canvas.drawText(letter.uppercase(), size / 2f, (size / 2f) + (yOffset / 2) + (bounds.height()/4), textPaint) return bitmap } fun mergeWavFiles(file1: File, file2: File, mergedFile: File): Boolean { try { val fis1 = FileInputStream(file1) val fis2 = FileInputStream(file2) val fos = FileOutputStream(mergedFile) // Skip headers (assumed 44 bytes for standard WAV) // NOTE: A robust implementation parses the header to find the 'data' chunk. // For this quick fix, assuming 44 bytes is standard for Baresip output. val headerSize = 44 val header1 = ByteArray(headerSize) val header2 = ByteArray(headerSize) if (fis1.read(header1) != headerSize || fis2.read(header2) != headerSize) { Log.e(TAG, "MergeWav: Files too small") return false } // Construct new header for stereo // Copy header from file1 but update channels to 2 and block align val newHeader = header1.clone() // 1. Update File Size (Indices 4-7) - placeholder, fixed at end // 2. Update Channels (Index 22) to 2 (Stereo) newHeader[22] = 2 newHeader[23] = 0 // 3. Update Block Align (Index 32) - usually 2*Channels (16bit) -> 4 newHeader[32] = 4 newHeader[33] = 0 // 4. Update Byte Rate (Index 28) - usually SampleRate * BlockAlign // Assuming 8000Hz sample rate: 8000 * 4 = 32000 // You should calculate this dynamically based on the input header if possible. // For now, copying the rest is usually "okay" if players are lenient, // but setting channels to 2 is the critical part. fos.write(newHeader) // MERGE LOOP with Buffering val bufferSize = 4096 // 4KB buffer val buffer1 = ByteArray(bufferSize) val buffer2 = ByteArray(bufferSize) val stereoBuffer = ByteArray(bufferSize * 2) // Output is twice as large var bytesRead1: Int var bytesRead2: Int var totalBytesData = 0 while (true) { bytesRead1 = fis1.read(buffer1) bytesRead2 = fis2.read(buffer2) if (bytesRead1 == -1 && bytesRead2 == -1) break // Use the smaller read count to avoid out of bounds if files differ slightly val limit = maxOf(bytesRead1, bytesRead2) var outIndex = 0 // Interleave samples (Simple Left/Right merge) // Assuming 16-bit audio (2 bytes per sample) for (i in 0 until limit step 2) { // Left Channel (File 1) if (i + 1 < bytesRead1) { stereoBuffer[outIndex++] = buffer1[i] stereoBuffer[outIndex++] = buffer1[i+1] } else { // Padding if file1 ended stereoBuffer[outIndex++] = 0 stereoBuffer[outIndex++] = 0 } // Right Channel (File 2) if (i + 1 < bytesRead2) { stereoBuffer[outIndex++] = buffer2[i] stereoBuffer[outIndex++] = buffer2[i+1] } else { // Padding if file2 ended stereoBuffer[outIndex++] = 0 stereoBuffer[outIndex++] = 0 } } fos.write(stereoBuffer, 0, outIndex) totalBytesData += outIndex } fis1.close() fis2.close() // Fix Header Sizes // ChunkSize (4-7) = TotalFileSize - 8 val totalFileSize = totalBytesData + 44 - 8 val rFile = RandomAccessFile(mergedFile, "rw") rFile.seek(4) rFile.write(intToLittleEndian(totalFileSize), 0, 4) // Subchunk2Size (40-43) = DataSize rFile.seek(40) rFile.write(intToLittleEndian(totalBytesData), 0, 4) rFile.close() fos.close() return true } catch (e: Exception) { Log.e(TAG, "MergeWav error: $e") return false } } // Helper for header writing private fun intToLittleEndian(value: Int): ByteArray { return byteArrayOf( (value and 0xff).toByte(), (value shr 8 and 0xff).toByte(), (value shr 16 and 0xff).toByte(), (value shr 24 and 0xff).toByte() ) } fun createEmptyFile(path: String): File { val file = File(path) if (file.exists()) file.delete() file.createNewFile() return file } @Suppress("unused") fun listFilesInDirectory(directoryPath: String): List { val directory = File(directoryPath) if (!directory.exists()) { Log.w(TAG, "Directory does not exist: $directoryPath") return emptyList() } if (!directory.isDirectory) { Log.w(TAG, "Path is not a directory: $directoryPath") return emptyList() } val files = directory.listFiles() if (files == null) { Log.e( TAG, "Failed to list files in directory (listFiles returned null): $directoryPath" ) return emptyList() } return files.filter { it.isFile } } @SuppressLint("RestrictedApi") @Suppress("unused", "DEPRECATION") fun printBackStack(navController: NavController) { Log.e(TAG, "---- Current Navigation Back Stack ----") navController.currentBackStack.value.forEachIndexed { index, navBackStackEntry -> val route = navBackStackEntry.destination.route val arguments = navBackStackEntry.arguments?.let { bundle -> bundle.keySet().joinToString(", ") { key -> "$key=${bundle.get(key)}" } } ?: "null" Log.e(TAG, "$index: Route='${route}', Args=[$arguments], ID=${navBackStackEntry.id}") } Log.e(TAG, "--------------------------------------") } } ================================================ FILE: app/src/main/kotlin/com/tutpro/baresip/ViewModel.kt ================================================ package com.tutpro.baresip import androidx.compose.runtime.MutableState import androidx.compose.runtime.mutableStateOf import androidx.compose.ui.graphics.vector.ImageVector import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.asSharedFlow import kotlinx.coroutines.flow.asStateFlow import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.Mic import kotlinx.coroutines.launch // Sealed class for type-safe navigation events sealed class NavigationCommand { object NavigateToHome : NavigationCommand() data class NavigateToCalls(val aor: String) : NavigationCommand() data class NavigateToChat(val aor: String, val peerUri: String) : NavigationCommand() } class ViewModel: ViewModel() { // A map to store message drafts. Key is "aor:peerUri" private val messageDrafts = mutableMapOf() fun getAorPeerMessage(aor: String, peerUri: String): String { return messageDrafts["$aor:$peerUri"] ?: "" } fun updateAorPeerMessage(aor: String, peerUri: String, message: String) { val key = "$aor:$peerUri" if (message.isEmpty()) { messageDrafts.remove(key) } else { messageDrafts[key] = message } } data class DialerState( val callUri: MutableState = mutableStateOf(""), val callUriEnabled: MutableState = mutableStateOf(true), val callUriLabel: MutableState = mutableStateOf(""), val showSuggestions: MutableState = mutableStateOf(false), val showCallButton: MutableState = mutableStateOf(true), val showCallConferenceButton: MutableState = mutableStateOf(true), val callButtonsEnabled: MutableState = mutableStateOf(true), val conferenceCall: MutableState = mutableStateOf(false), ) val dialerState = DialerState() private val _calls = MutableStateFlow>(emptyList()) val calls = _calls.asStateFlow() private val _selectedAor = MutableStateFlow("") val selectedAor = _selectedAor.asStateFlow() private val _accountUpdate = MutableStateFlow(0) val accountUpdate = _accountUpdate.asStateFlow() private val _micIcon = MutableStateFlow(Icons.Filled.Mic) val micIcon = _micIcon.asStateFlow() private val _isSpeakerOn = MutableStateFlow(false) val isSpeakerOn = _isSpeakerOn.asStateFlow() private val _isDialpadVisible = MutableStateFlow(false) val isDialpadVisible = _isDialpadVisible.asStateFlow() private val _showKeyboard = MutableStateFlow(0) val showKeyboard = _showKeyboard.asStateFlow() private val _hideKeyboard = MutableStateFlow(0) val hideKeyboard = _hideKeyboard.asStateFlow() private val _navigationCommand = MutableSharedFlow() val navigationCommand = _navigationCommand.asSharedFlow() private var _selectedCallRow: CallRow? = null fun selectCallRow(callRow: CallRow) { _selectedCallRow = callRow } fun consumeSelectedCallRow(): CallRow? { val callRow = _selectedCallRow _selectedCallRow = null return callRow } fun onNewMessageReceived(aor: String, peerUri: String) { viewModelScope.launch { _navigationCommand.emit(NavigationCommand.NavigateToChat(aor, peerUri)) } } fun updateCalls(calls: List) { _calls.value = calls } fun updateSelectedAor(aor: String) { _selectedAor.value = aor } fun triggerAccountUpdate() { _accountUpdate.value += 1 } fun updateMicIcon(icon: ImageVector) { _micIcon.value = icon } fun updateSpeakerPhoneStatus(on: Boolean) { _isSpeakerOn.value = on } fun toggleDialpadVisibility() { _isDialpadVisible.value = !_isDialpadVisible.value } fun requestShowKeyboard() { _showKeyboard.value += 1 } fun requestHideKeyboard() { _hideKeyboard.value += 1 } fun navigateToHome() { viewModelScope.launch { _navigationCommand.emit(NavigationCommand.NavigateToHome) } } fun navigateToCalls(aor: String) { viewModelScope.launch { _navigationCommand.emit(NavigationCommand.NavigateToCalls(aor)) } } } ================================================ FILE: app/src/main/res/drawable/circle_green.xml ================================================ ================================================ FILE: app/src/main/res/drawable/circle_green_blind.xml ================================================ ================================================ FILE: app/src/main/res/drawable/circle_red.xml ================================================ ================================================ FILE: app/src/main/res/drawable/circle_red_blind.xml ================================================ ================================================ FILE: app/src/main/res/drawable/circle_white.xml ================================================ ================================================ FILE: app/src/main/res/drawable/circle_yellow.xml ================================================ ================================================ FILE: app/src/main/res/drawable/circle_yellow_blind.xml ================================================ ================================================ FILE: app/src/main/res/drawable/ic_launcher_foreground.xml ================================================ ================================================ FILE: app/src/main/res/drawable/ic_notification_b.xml ================================================ ================================================ FILE: app/src/main/res/drawable/ic_notification_call.xml ================================================ ================================================ FILE: app/src/main/res/drawable/ic_notification_call_end.xml ================================================ ================================================ FILE: app/src/main/res/drawable/ic_notification_call_missed.xml ================================================ ================================================ FILE: app/src/main/res/drawable/ic_notification_delete.xml ================================================ ================================================ FILE: app/src/main/res/drawable/ic_notification_message.xml ================================================ ================================================ FILE: app/src/main/res/drawable/ic_notification_reply.xml ================================================ ================================================ FILE: app/src/main/res/drawable/ic_notification_save.xml ================================================ ================================================ FILE: app/src/main/res/layout/status_notification.xml ================================================ ================================================ FILE: app/src/main/res/mipmap-anydpi/ic_launcher.xml ================================================ ================================================ FILE: app/src/main/res/mipmap-anydpi/ic_launcher_round.xml ================================================ ================================================ FILE: app/src/main/res/values/colors.xml ================================================ #0ca1fd #00B9A1 #BA1A1A #ffffff #4CAF50 #FFEB3B #d83025 #FFFFFF ================================================ FILE: app/src/main/res/values/strings.xml ================================================ baresip baresip+ About baresip About baresip+ SIP User Agent based on baresip library

Juha Heinanen <jh@tutpro.com>

Version %1$s


Usage Hints

  • Check that default values in baresip\'s Settings meet your needs (touch item titles for help).
  • Then in Accounts, create one or more accounts (again touch item titles for help).
  • Registration status of an account is shown with a colored dot: green (registration succeeded), yellow (registration is in progress), red (registration failed), white (registration has not been activated).
  • Touch on the status dot leads directly to account configuration.
  • Long touch on baresip bar icons shows information about the icons.
  • Swipe down gesture causes re-registration of the currently shown account.
  • Long touch on currently shown account enables or disables account\'s registration.
  • Swipe left/right gesture toggles between the accounts.
  • Previous call party can be reselect by touching the call icon when Callee is empty.
  • Peers of calls and messages can be added to contacts by long touches.
  • Long touches can also be used to remove calls, chats, messages, and contacts.
  • Touch/long touch on contact icon can be used to install/remove image avatar.
  • Long touch on an audio codec can be used to enable/disable the codec.
  • See Wiki for more information.

Privacy Policy

Privacy policy is available here.


Source Code

Source code is available at GitHub, where also issues can be reported.


Language Translations

Language translations are managed via baresip Weblate project.


Licenses

  • BSD-3-Clause except the following:
  • Apache 2.0 AMR codecs and TLS security
  • AGPLv4 ZRTP media encryption
  • GNU LGPL 2.1 Codec2 codec
  • Free G.722 codec
  • GNU GPLv3 G.729 codec
]]>
SIP User Agent with video calls based on baresip library

Juha Heinanen <jh@tutpro.com>

Version %1$s


Usage Hints

  • Check that default values in baresip+\'s Settings meet your needs (touch item titles for help).
  • Then in Accounts, create one or more accounts (again touch item titles for help).
  • Registration status of an account is shown with a colored dot: green (registration succeeded), yellow (registration is in progress), red (registration failed), white (registration has not been activated).
  • Touch on the status dot leads directly to account configuration.
  • Long touch on baresip bar icons shows information about the icons.
  • Swipe down gesture causes re-registration of the currently shown account.
  • Long touch on currently shown account enables or disables account\'s registration.
  • Swipe left/right gesture toggles between the accounts.
  • Previous call party can be reselect by touching the call icon when Callee is empty.
  • Peers of calls and messages can be added to contacts by long touches.
  • Long touches can also be used to remove calls, chats, messages, and contacts.
  • Touch/long touch of contact icon can be used to install/remove image avatar.
  • Long touch on an audio or video codec can be used to enable/disable the codec.
  • See Wiki for more information.

Known Issues

  • Selfview is not properly shown when video stream is sendonly.

Privacy Policy

Privacy policy is available here.


Source code

Source code is available at GitHub, where also issues can be reported.


Language translations

Language translations are managed via baresip Weblate project.


Licenses

  • BSD-3-Clause except the following:
  • Apache 2.0 AMR codecs and TLS security
  • AGPLv4 ZRTP media encryption
  • GNU LGPL 2.1 Codec2 codec
  • Free G.722 codec
  • GNU GPLv3 G.729 codec
  • GNU GPLv2 H.264 and H.265 codecs
  • AOMedia AV1 codec
]]>
Account Nickname (if any) used to identify this account within baresip app. Nickname Invalid Account Nickname \'%1$s\' Nickname \'%1$s\' already exists Display Name Name (if any) used in From URI of outbound requests. Invalid Display Name \'%1$s\' Authentication Username Authentication username if authentication of SIP requests is required. Default value is account\'s username. Invalid Authentication Username \'%1$s\' Authentication Password Authentication Password up to 64 characters. If Authentication Username is given, but Password is not given, it will be asked when baresip is started. Invalid Authentication Password \'%1$s\' Outbound Proxies SIP URI of one or two proxies that must be used when sending requests. If two is given, REGISTER requests are sent to both and other requests are sent to one that responds. If no outbound proxy is given, requests are sent based on DNS NAPTR/SRV/A record lookup of callee URI hostpart. If hostpart of SIP URI is an IPv6 address, the address must be written inside brackets []. \nExamples: \n • sip:example.com:5061;transport=tls \n • sip:[2001:67c:223:777::10];transport=tcp \n • sip:192.168.43.50:443;transport=wss SIP URI of Proxy Server SIP URI of another Proxy Server Invalid Proxy Server URI \'%1$s\' Register If checked, registration is enabled and REGISTER requests are sent at the interval specified by Registration Interval. Registration Interval Tells how often (in seconds) baresip sends REGISTER requests. Valid values are from 60 to 3600. Invalid Registration Interval \'%1$s\' Check Origin If checked, inbound requests are allowed only from IP addresses where register requests were sent. Media NAT Traversal Selects media NAT traversal protocol (if any). Possible choices are STUN (Session Traversal Utilities for NAT, RFC 5389) and ICE (Interactive Connectivity Establishment, RFC 5245). STUN/TURN Server A STUN/TURN Server URI of form scheme:host[:port][\?transport=udp|tcp], where scheme is \'stun\', \'stuns\', \'turn\', or \'turns\'. Factory default STUN Server for STUN and ICE protocols is \'stun:stun.l.google.com:19302\' pointing to public Google STUN server. There is no factory default TURN server. Invalid STUN/TURN Server URI \'%1$s\' stun:stun.l.google.com:19302 STUN/TURN Username Username if required by STUN/TURN server Invalid Username \'%1$s\' STUN/TURN Password Password if required by STUN/TURN server Invalid Password \'%1$s\' Media Encryption Selects media transport encryption protocol (if any). \n • ZRTP (recommended) means that ZRTP end-to-end media encryption negotiation is tried after the call has been established. \n • DTLS-SRTPF means that UDP/TLS/RTP/SAVPF is offered in outgoing call and that RTP/SAVP, RTP/SAVPF, UDP/TLS/RTP/SAVP, or UDP/TLS/RTP/SAVPF is used if offered in incoming call. \n • SRTP-MANDF means that RTP/SAVPF is offered in outgoing call and required in incoming call. \n • SRTP-MAND means that RTP/SAVP is offered in outgoing call and required in incoming call. \n • SRTP means that RTP/AVP is offered in outgoing call and that RTP/SAVP or RTP/SAVPF is used if offered in incoming call. RTCP Multiplexing If checked, RTP and RTCP packets are multiplexed on a single port (RFC 5761). Reliable Provisional Responses If checked, indicate support for reliable provisional responses (RFC 3262). DTMF Mode Selects how DTMF tones 0–9, #, *, and A-D are sent. In-band RTP Events SIP INFO Requests In-band RTP or SIP INFO Answer Mode Selects how incoming calls are answered. Redirect Mode Selects if call redirect request is followed automatically or if confirmation is requested. Manual Automatic Block Unknown Block calls and messages from peers that are not found in contacts. Voicemail URI SIP URI for checking of voicemail messages. If left empty, voicemail messages (Message Waiting Indications) are not subscribed to. Country Code E.164 country code of this account. If From URI userpart of incoming call or message contains a telephone number that does not start with \'+\' sign and if contact lookup fails, the number is prefixed with this country code and contact lookup is tried again. If the telephone number starts with a single digit \'0\', digit \'0\' is removed before the number is prefixed. Invalid Country Code \'%1$s\' Telephony Provider SIP URI host part used in calls to telephone numbers. Factory default is account\'s domain. If not given, this account cannot be used to call telephone numbers. Numeric Keypad" If checked, numeric keypad is shown when \"Call to …\" field is focused." Invalid SIP URI host part \'%1$s\' Default Account If checked, this account is selected when baresip is started. Custom Parameters Semicolor separeted list of custom account parameters Accounts SIP URI of New Account SIP URI of new account of form <user>@<domain>[:<port>][;transport=udp|tcp|tls]. If <port> is given and transport protocol is not given, transport protocol defaults to UDP. If <port> is not given and transport protocol is given, <port> defaults to 5060 or 5061 (TLS). If neither is given and no outbound proxy is specified, account\'s registrar (if any) is determined solely based on domain\'s DNS information. Invalid user@domain[:port][;transport=udp|tcp|tls] \'%1$s\' Account \'%1$s\' already exists. "Failed to allocate new account. Encrypt Password Decrypt Password Do you want to delete account \'%1$s\'\? is calling Missed call from Missed calls %1$d missed calls Call transfer request to Auto-rejected call from \`%1$s\` Auto-rejected blocked call from \`%1$s\` Auto-rejected blocked message from \`%1$s\` Blocked Blocked Calls Blocked Messages Do you want to delete blocked requests of \'%1$s\'\? Do you want to add peer \'%1$s\' to contacts\? Call History Call Details Call calls call Peer Direction Time Duration Do you want to add \'%1$s\' to contacts or delete %2$s from call history\? Do you want to delete \'%1$s\' %2$s from call history\? Disable Enable Do you want to delete call history of account \'%1$s\'\? Call answered Call answered elsewhere Call missed Call rejected Playing recording … Save Recording Do you want to save this recording? Recording saved Do you want to delete this call from history? Chat with %1$s New message Do you want to delete message or add peer \'%1$s\' to contacts\? Do you want to delete message\? Add Contact Sending of message failed Failed Chat History Today You New Chat Peer Do you want to delete chat with peer \'%1$s\' or add peer to contacts\? Do you want to delete chat with \'%1$s\'\? Do you want to delete chat history of account \'%1$s\'\? Audio Codecs Video Codecs Settings Start Automatically If checked, baresip starts automatically after device boots or after a new version of baresip is installed. Start is delayed until device is unlocked. Automatic start needs Appear on Top permission. Battery Optimizations Disable battery optimizations (recommended) if you want to reduce likelihood that Android restricts baresip\'s access to network or enters baresip to standby state. Default Phone App Dialer role is not available If checked, baresip is the default phone app. Do not check if your device may need to handle also other than SIP calls or messages. Listen Address IP address and port of form \'address:port\' at which baresip listens for incoming SIP requests. If IP address is an IPv6 address, it must be written inside brackets []. IPv4 address 0.0.0.0 or IPv6 address [::] makes baresip listen at all available addresses. If left empty (factory default), baresip listens at an arbitrary port at all available addresses. Invalid Listen Address 0.0.0.0:5060 Address Family Chooses which IP addresses baresip is using. If IPv4 or IPv6 is chosen, baresip uses only IPv4 or IPv6 addresses. If neither is chosen, baresip uses both IPv4 and IPv6 addresses. Transport Protocols Comma separated list of supported SIP request/response transport protocols. If left empty, default value is \'udp,tcp,tls,ws,wss\' that includes all supported transport protocols. List only the ones you need. Use of UDP is not recommended for security and other reasons. Invalid Transport Protocols list DNS Servers Comma separated list of addresses of DNS servers. If not given, DNS server addresses are obtained dynamically from the system. Each DNS address is of form \'ip:port\' or \'ip\'. If port is omitted, it defaults to 53. If ip is an IPv6 address and also port is given, ip must be written inside brackets []. As an example, list \'8.8.8.8:53,[2001:4860:4860::8888]:53\' points to IPv4 and IPv6 addresses of public Google DNS servers. Invalid DNS Servers Failed to set DNS servers TLS Certificate File If checked, a file containing TLS certificate and private key of this baresip instance has been or will be loaded. In Android version 9, a file called \'cert.pem\' is loaded from Download folder. For security reasons, delete the file after loading. Verify Server Certificates If checked, baresip verifies TLS certificates of SIP User Agent and SIP Proxy Servers when TLS transport is used. TLS CA File If checked, a file has been or will be loaded that contains TLS certificates of such Certificate Authorities that are not included in Android OS. In Android version 9, a file called \'ca_certs.crt\' is loaded from Download folder. No External Storage read permission Unique Contact URI If checked, Contact URI is guaranteed to be unique. Needs to be checked if there is more than one account with the same SIP URI userpart, but also improves protection of the accounts from attacks. Audio Settings Speaker Phone If checked, speaker phone is turned automatically on when call starts. Audio Modules Audio codecs provided by the checked modules are available for use by the accounts. Failed to load module. Microphone Gain Multiply microphone volume by this decimal number. Minimum value is 1.0 (factory default) that disables microphone gain. Larger values may negatively affect audio quality. Invalid Microphone Gain value Opus Bit Rate Average maximum bit rate used by Opus audio stream. Valid values are 6000-510000. Factory default is 28000. Expected Opus packet-loss Expected Opus audio stream packet loss percentage, from 0–100. Factory default value is 1. Value 0 also turns off Opus Forward Error Correction (FEC). Invalid Opus bitrate Invalid Opus Packet Loss Percentage Audio Delay Time (in milliseconds) to wait audio from callee when call is established. Set to a higher value if you miss audio from callee at the beginning of the call. Invalid Audio Delay \'%1$s\'. Valid values are from 100 to 3000. Default Call Volume If set, default call audio volume at scale 1–10. Tone Country Country of call ringing, waiting, and callee busy tones Dark Theme Force dark display theme Dynamic Colors Use dynamic colors if enabled in Android Settings Colorblind Use colorblind friendly registration status icons Proximity Sensing"> If checked, proximity sensing is active during calls. Video Frame Size Size of transmitted video frames (width x height) Video Frames Per Second Video frame rate that will be offered during the SDP handshake. Valid values are from 10 to 30. Invalid Frames Per Second \'%1$d\' Ringtone Select Ringtone User Agent Custom SIP request/response User-Agent header field value Invalid User-Agent header field value Chooses if baresip contacts, Android contacts, or both are used. If both are used and a contact with the same name exists in both contacts, the baresip contact will be chosen. Both Debug If checked, provides debug and info level log messages to Logcat. SIP Trace If checked and if Debug is checked, Logcat messages include also SIP request and response trace. Unchecked automatically at baresip start. Reset to Factory Defaults If checked, settings are reset to factory default values. Are you sure you want to reset settings to factory default values\? Reset Failed to read file \'cert.pem\'. Failed to read file \'ca_certs.crt\'. You need to restart baresip in order to activate the new settings. Restart now\? Consent Request If Android contacts is chosen, they can be used in calling and messaging as references to SIP and tel URIs. baresip app does not store Android contacts nor share them with anyone. In order to make Android contacts available in baresip, Google requires that you accept their use as described here and in app\'s Privacy Policy. New Contact Name SIP or tel URI user@domain or telephone number Favorite If checked, contact is shown among other favorites on top of contacts list. Invalid contact name \'%1$s\' Contact \'%1$s\' already exists. If checked, this contact is added to Android contacts. Profile image Contacts Do you want to call or send message to \'%1$s\'\? Send Message Do you want to delete contact \'%1$s\'\? Search Alert Info Notice Cancel Stop Reply Save OK Yes No Accept Deny SIP URI Add Delete Edit Status Error Confirmation Anonymous Unknown \u2022 %1$s Invalid SIP or tel URI \'%1$s\' baresip Android Backup Restore Logcat About Restart Quit Call to … Call from … Diverted by … Account \'%1$s\' has no Telephony Provider Video call Video Request Accept sending and receiving video with \'%1$s\'\? Accept sending of video to \'%1$s\'\? Accept receiving video from \'%1$s\'\? Call is ringing Call is on hold Call is connected Recording can be turned on or off only when call is not connected Call Transfer Blind Attended Transfer destination Choose destination URI Transfer Transfer failed Peer does not support REPLACES feature DTMF Call Info No info available Duration: %1$d (secs) Codecs Current Rate: %1$s (Kbits/s) Average Rate: %1$s (Kbits/s) Packets Lost Jitter: %1$s (ms) Voicemail Messages You have one new message new messages one old message old messages and You have no messages Listen You already have an active call. Baresip failed to start. This may be due to an invalid Settings value. Check Listen Address, TLS Certificate File, and TLS CA File. Then restart baresip. Registering of \`%1$s\` failed. Verify Request Do you want to verify SAS <%1$s>\? Transfer Request Do you accept to transfer this call to \'%1$s\'\? Call Request Do you accept request to call \'%1$s\'\? Automatic redirection to \'%1$s\'\ Redirect Request Do you accept call redirection to \'%1$s\'\? Call failed Call is closed This call is NOT secure! This call is SECURE, but peer is NOT verified! This call is SECURE and peer is VERIFIED! Do you want to unverify the peer\? Unverify Application data (excluding recordings) backed up to file \'%1$s\'. In Android version 9, the file is in Download folder. Failed to back up application data to file \'%1$s\'. Check Apps → baresip → Permissions → Storage. Restart Request Application data restored. baresip needs to be restarted. Restart now\? Failed to restore application data. Check that you gave correct password and that the backup file is from this application. In Android versions 9, also check Apps → baresip → Permissions → Storage and that file \'%1$s\' exists in Download folder. Failed to restore application data. Android version 14 and above does not allow restoring data that was backed up before %1$s version %2$s. You are not able to use this application without \"Notifications\" permission. baresip needs \"Microphone\" permission for voice calls. Grant \"Camera\" permission to make or answer video calls. You are not able create backup without \"Storage\" permission. You are not able restore backup without \"Storage\" permission. You are not able to access Android contacts without \"Contacts\" permission. No supported video cameras! No network connection! No hardware Acoustic Echo Cancellation! Audio focus denied! Permissions rationale baresip needs \"Microphone\" permission for voice calls, \"Nearby devices\" permission for Bluetooth microphone/speaker detection, \"Notifications\" permission for posting notifications, and in Android 9 \"Storage\" permission for Backup/Restore operations. baresip+ needs \"Microphone\" permission for voice calls, \"Camera\" permission for video calls, \"Nearby devices\" permission for Bluetooth microphone/speaker detection, \"Notifications\" permission for posting notifications, and in Android 9 \"Storage\" permissions for Backup/Restore operations. Call Recording If activated, new incoming and outgoing calls will be recorded. Recordings can be played on Call Details page. Microphone If activated during call, microphone is muted. Speakerphone If activated, audio is played via device speakerphone.
================================================ FILE: app/src/main/res/values-ar/strings.xml ================================================ ================================================ FILE: app/src/main/res/values-bg/strings.xml ================================================ Относно baresip SES потребителско приложение, базирано на библиотеката Baresip

Juha Heinanen <jh@tutpro.com>

версия %1$s

Съвети за употреба

  • Проверете дали стойностите по подразбиране на конфигурацията отговарят на вашите нужди (заглавия на елементите с докосване).
  • След това създайте един или повече акаунти (отново докоснете заглавия на артикули за помощ).
  • Участниците в обажданията и съобщенията могат да се добавят към контактите с по-продължителни докосвания.
  • По същия начин може да се премахват повиквания, чатове, съобщения и контакти.
  • Имате възможност да преизберете повторно последния абонат, като кликнете на иконата за повикване, когато полето е празно.
  • Ако не можете да чуете другата страна по време на разговор, опитайте се да увеличите силата на звука на Media устройството си или задайте силата на звука на разговора по подразбиране в конфигурация.

Програмен код

Изходният код е достъпен на адрес GitHub, където също могат да се докладват проблеми. ]]>
Акаунт Показване на име Публично име (ако има такова), използвано в URI на изходящи заявки. Потребителско име за удостоверяване Потребителско име за удостоверяване, ако се изисква от изходящ прокси. Парола за удостоверяване Парола за удостоверяване, ако се изисква от изходящ прокси. Изходящи прокси SIP URI на един или два прокси сървъра, които трябва да се използват при изпращане на заявки. Ако са дадени две, заявките за РЕГИСТРИРАНЕ се изпращат и до двете, и до други заявки тази, която отговаря. Ако не е даден изходящ прокси, заявките се изпращат въз основа на DNS NAPTR / SRV / Търсене на запис на хоризонталната част на URI на повикващия. Ако хостпарт на SIP URI е IPv6 адрес, адресът трябва да бъде написан в скоби []. \nExamples: \n • sip:foo.com:5060;transport=tls \n • sip:[2001:67c:223:777::10]:5060;transport=tcp SIP URI на прокси сървър SIP URI на друг прокси сървър Регистрирай Ако е отметнато, регистрацията е активирана и заявките за РЕГИСТРИРАНЕ се изпращат на 12 минути интервали. Аудио кодеци NAT Traversal Average Избира протокол за преминаване на носител NAT (ако има такъв). Възможният избор е STUN (Помощни програми за сесионно преминаване за NAT, RFC 5389) и ICE (Интерактивна свързаност Учредяване, RFC 5245). СТУН Сървър STUN сървър на хост на формуляра [: port]. Фабричната стойност по подразбиране е \'stun.l.google.com:19302\', сочещи към обществен сървър на Google STUN. В момента потребителското име и паролата не се поддържат. Шифроване на медиите Избира протокол за криптиране на медийния транспорт (ако има такъв). \n • ZRTP (препоръчително) означава, че след изпробване на преговорите за криптиране на медиите от край до край ZRTP разговорът е установен. \n • DTLS-SRTPF означава, че UDP / TLS / RTP / SAVPF се предлага при изходящо повикване и RTP / SAVP, RTP / SAVPF, UDP / TLS / RTP / SAVP или UDP / TLS / RTP / SAVPF се използва, ако се предлага при входящо повикване. \n • SRTP-MANDF означава, че RTP / SAVPF се предлага при изходящо повикване и се изисква при входящо повикване. \n • SRTP-MAND означава, че RTP / SAVP се предлага при изходящо повикване и се изисква при входящо повикване. \n • SRTP означава, че RTP / AVP се предлага при изходящо повикване и че RTP / SAVP или RTP / SAVPF се използва ако се предлага при входящо повикване. Режим на отговори Избира как се отговаря на входящите повиквания. Ръчно Автоматично URI на гласова поща SIP URI за проверка на гласови съобщения. Ако се остави празно, съобщенията на гласовата поща (Индикации за чакане на съобщение) не са активирани. Профил по подразбиране Ако е отметнато, този акаунт се избира при стартиране на baresip. Профили Невалиден потребител@домейн \'%1$s\' Профила \'%1$s\' вече съществува. "Неуспешно разпределяне на нов акаунт. Шифроване на парола Дешифриране на парола Искате ли да изтриете акаунта \'%1$s\'? Заявка за прехвърляне на обаждане до История на обажданията Обади се повиквания обади се Искате ли да добавите \'%1$s\' към контакти или изтриване %2$s от историята на обажданията? Искате ли да изтриете \'%1$s\' %2$s от историята на обажданията? Забрани история на обаждания Разреши история на обаждания Искате ли да изтриете историята на обажданията на акаунта \'%1$s\'? Чатя с %1$s Ново съобщение Искате ли да изтриете съобщението или да добавите потребителя \'%1$s\' в контакти? Искате ли да изтриете съобщението? Добави контакт Изпращането на съобщението не бе успешно Неуспешно История на чата днес Вие Нов потребител за чат Искате ли да изтриете чата с този потребител \'%1$s\' или да го добавяне в контактите? Искате ли да изтриете чата с \'%1$s\'? Искате ли да изтриете историята на чата на акаунта \'%1$s\'? Конфигуриране Стартирайте автоматично Ако е отметнато, baresip се стартира автоматично след включване на устройството. Слушане на адрес IP адрес и порт на формата \'адрес: порт\', на който baresip слуша за входящи SIP заявки. Ако IP адресът е IPv6 адрес, той трябва да бъде написан вътре в скоби []. IPv4 адрес 0.0.0.0 или IPv6 адрес [::] позволява на baresip да слуша всички налични адреси. Ако се остави празно (фабрично по подразбиране), baresip ще слуша на порт 5060 от всички налични адреси. Невалиден адрес за слушане DNS сървъри Списък разделен със запетая на адреси на DNS сървъри. Ако не е зададено, адресите на DNS сървъра се получават динамично от системата. Всеки DNS адрес е във форма \'ip:port\' или \'ip\'. Ако портът е пропуснат, той по подразбиране е 53. Ако ip е IPv6 адрес и също е зададен порт, ip трябва да бъде написани вътре в скоби []. Като пример, посочете \`8.8.8.8:53,[2001:4860:4860::8888]:53 \' сочи към IPv4 и IPv6 адреси на обществени DNS сървъри на Google. Невалидни DNS сървъри Неуспешно задаване на DNS сървъри Файл за сертификати TLS Ако е поставена отметка, файл \'cert.pem\' съдържащ TLS сертификатът и личният ключ на този екземпляр baresip е бил или ще бъде зареден от директорията за Изтегляне. От съображения за сигурност изтрийте файла след зареждането му. TLS CA File Ако е поставено отметка, файл \'ca_certs.crt\' съдържащи TLS сертификати на Сертифициращите органи е бил или ще бъде зареден от директорията за Изтегляне. Opus Bit Rate Средна максимална битова скорост, използвана от аудио поток на Opus. Валидните стойности са 6000-510000. Фабрично по подразбиране е 28000. Очаквана загуба на пакети Opus Очакван процент загуба на пакет от аудио поток на Opus, от 0–100. По подразбиране 0 изключване на Opus Forward Error Correction (FEC). Невалиден битрейт на Opus Невалиден процент загуба на пакет Opus Усилване на повикването по подразбиране Ако се задава, трябва да се избира сила на звука при повикване по подразбиране в мащаб 1–10. Debug Осигурява наличие на съобщения за грешки и информация на ниво информация в Logcat. Възстановяване на фабричните настройки Ако е отметнато, конфигурацията се нулира до фабрични стойности по подразбиране Неуспешно четене на файл \'cert.pem\' от директорията за изтегляне. Файлът не беше прочетен \'ca_certs.crt\' от директорията за изтегляне. Трябва да рестартирате baresip, за да активирате новата конфигурация. Рестартирай сега? Нов контакт име Невалидно име за контакт \'%1$s\' Този контакт \'%1$s\' вече съществува. Контакти Искате ли да се обадите или да изпратите съобщение до \'%1$s\'? Изпрати съобщение Искате ли да изтриете този контакт \'%1$s\'? Внимание Информация известие Отказ Добре да не приемам отказвам Добави Изтрий Редактиране Статус Грешка Създай Резервно копие Възстанови от Резервно копие Относно Рестартирай Отписване Изходящо повикване до… Входящо обаждане от… DTMF Информация за обаждане Продължителност %1$d (s) Кодеци Скорост: %1$s Съобщения за гласова поща Ти имаш едно ново съобщение Нови съобщения Едно старо съобщение Стари съобщения и Нямате съобщения Слушам Вече имате активно обаждане. Baresip не успя да стартира. Това може да се дължи на невалиден адрес за слушане или TLS файл. Те са нулирани. Рестартирайте baresip. Регистрация на \`%1$s\` се провали. Потвърди Искате ли да потвърдите <%1$s>\? Приемате ли да прехвърлите обаждане до \'%1$s\'? Обаждането не бе успешно Обаждането е затворено Това обаждане НЕ е сигурно! Това обаждане е СИГУРНО, но другият абонат НЕ е потвърдил! Този разговор е СИГУРЕН и другият абонат е ПРОВЕРЕН! Искате ли да го потвърдите? Отмяна на потвърждението Данните от приложението са архивирани във файла за изтегляне на папки \'%1$s\'. Неуспешно архивиране на данни от приложението за изтегляне на файла с папки \'%1$s\'. Проверете Приложения → baresip → Разрешения → Съхранение Данните за приложението са възстановени. baresip трябва да се рестартира. Рестартирай сега? Възстановяването на данните на приложението от папката за изтегляне не бе успешно. Проверете Приложения → baresip → Разрешения → Съхранение и този архивен файл \'%1$s\' съществува в папката и ако е така, вие сте дали правилна парола за дешифриране. Аудио модули Аудио кодеци предоставени от проверените модули могат да бъдат използвани от акаунтите. Не могат да бъдат заредени модулите. Разговори не могат да бъдат осъществени без разрешен достъп до микрофона. Порта и транспортния протокол на акаунта могат да бъдат посочени по избор, когато се създаде нов акаунт: потребителско име@домейн[:port] [;transport=udp|tcp|tls]. Ако е даден порт и транспортният протокол не е даден, транспортният протокол по подразбиране е udp. Ако порт не е даден и транспортният протокол е даден, портът по подразбиране е 5060 или 5061 (tls). Ако нито едното не е посочено и не е посочен изходящ прокси, регистраторът на акаунта (ако има такъв) се определя единствено въз основа на DNS информация от домейна. Нов Акаунт
================================================ FILE: app/src/main/res/values-ca/strings.xml ================================================ ================================================ FILE: app/src/main/res/values-cs/strings.xml ================================================ O aplikaci baresip O aplikaci baresip+ Přezdívka (pokud existuje) používaná k identifikaci tohoto účtu v aplikaci baresip. Přezdívka Heslo Odchozí proxy servery Registrace Neplatná URI proxy serveru \'%1$s\' SIP URI proxy serveru SIP URI jednoho nebo dvou proxy serverů, které je třeba použít při odesílání požadavků. Pokud jsou zadány dva, jsou požadavky REGISTER zasílány oběma a ostatní požadavky jsou zasílány tomu, který odpoví. Pokud není zadán žádný odchozí proxy server, jsou požadavky odesílány na základě vyhledávání záznamů DNS NAPTR/SRV/A z hostitelského jména URI volaného. Pokud je hostitelské jméno URI SIP adresa IPv6, musí být adresa zapsána v závorkách []. \nPříklady: \n - sip:example.com:5061;transport=tls \n • sip:[2001:67c:223:777::10];transport=tcp \n • sip:192.168.43.50:443;transport=wss SIP URI dalšího proxy serveru Udává, jak často (v sekundách) baresip odesílá požadavky REGISTER. Platné hodnoty jsou od 60 do 3600. Neplatný interval registrace \'%1$s\' Pokud je zatrženo, registrace je povolena a požadavky REGISTER jsou odesílány v intervalu určeném parametrem Interval registrace. Interval registrace Vybírá způsob přijímání příchozích hovorů. Průchod médií skrze NAT Neplatná URI serveru STUN/TURN \'%1$s\' Vybírá protokol pro průchod médií skrze NAT (pokud existuje). Možné volby jsou STUN (Session Traversal Utilities for NAT, RFC 5389) a ICE (Interactive Connectivity Establishment, RFC 5245). Server STUN/TURN Uživatelské jméno, pokud ho server STUN/TURN vyžaduje URI serveru STUN/TURN ve tvaru scheme:host[:port][\?transport=udp|tcp], kde scheme je \"stun\", \"stuns\", \"turn\" nebo \"turns\". Výchozí server STUN pro protokoly STUN a ICE je \'stun:stun.l.google.com:19302\', který ukazuje na veřejný server STUN společnosti Google. Výchozí server TURN neexistuje. Heslo STUN/TURN Heslo, pokud ho server STUN/TURN vyžaduje Neplatné heslo \'%1$s\' Šifrování médií Pokud je zatrženo, pakety RTP a RTCP jsou multiplexovány na jednom portu (RFC 5761). Multiplexování RTCP Uživatelské jméno pro ověřování, pokud je vyžadováno ověřování požadavků SIP. Výchozí hodnotou je uživatelské jméno účtu. Poskytovatel telefonních služeb Čas Zobrazované jméno Název (pokud existuje) použitý v URI odchozích požadavků. Neplatné uživatelské jméno pro ověřování \'%1$s\' Ověřovací heslo až 64 znaků. Pokud je zadáno Uživatelské jméno pro ověřování, ale není zadáno Heslo, bude po spuštění baresipu požadováno. Účastník Směr Chcete přidat \'%1$s\' do kontaktů nebo odstranit %2$s z historie hovorů\? Chcete odstranit historii volání účtu \"%1$s\"\? Trvání Chcete smazat chat s účastníkem \'%1$s\' nebo přidat účastníka do kontaktů\? Chcete odstranit zprávu nebo přidat účastníka \'%1$s\' do kontaktů\? Zakázat Nová zpráva Chcete zprávu odstranit\? Chcete odstranit \'%1$s\' %2$s z historie hovorů\? Chatovat s %1$s Odeslání zprávy se nezdařilo Vy Nový účastník chatu Chcete odstranit chat s \'%1$s\'\? Přidat kontakt Historie chatu Dnes Nepodařilo se Neplatné servery DNS Chcete odstranit historii chatu účtu \'%1$s\'\? Zvukové kodeky Pokud je zaškrtnuto, byl nebo bude načten soubor obsahující certifikát TLS a soukromý klíč této instance baresipu. Ve verzi systému Android 9 se soubor s názvem \'cert.pem\' načte ze složky Download. Z bezpečnostních důvodů tento soubor po načtení odstraňte. Neplatná naslouchací adresa Nastavení Optimalizace baterie Naslouchací adresa Spustit automaticky Pokud je zaškrtnuto, baresip se spustí automaticky po (opětovném) spuštění zařízení nebo po instalaci nové verze baresip. Ke spuštění dojde po odemčení zařízení. Pokud chcete snížit pravděpodobnost, že systém Android omezí přístup zařízení baresip k síti nebo jej přepne do pohotovostního režimu, zakažte optimalizace baterie (doporučeno). IP adresa a port ve tvaru \"address:port\", na kterém baresip naslouchá příchozím požadavkům SIP. Pokud je IP adresa IPv6, musí být zapsána v závorce []. IPv4 adresa 0.0.0.0 nebo IPv6 adresa [::] způsobí, že baresip bude naslouchat na všech dostupných adresách. Pokud zůstane prázdné (výchozí nastavení), bude baresip naslouchat na libovolném portu na všech dostupných adresách. Servery DNS Seznam adres serverů DNS oddělených čárkou. Pokud nejsou zadány, adresy serverů DNS jsou získávány dynamicky ze systému. Každá adresa DNS má tvar \'ip:port\' nebo \'ip\'. Pokud je port vynechán, je výchozí hodnota 53. Pokud je ip adresa IPv6 a je zadán i port, musí být ip zapsáno v závorkách []. Příklad: seznam \'8.8.8.8:53,[2001:4860:4860::8888]:53\' ukazuje na adresy IPv4 a IPv6 veřejných serverů Google DNS. Soubor TLS CA Nepodařilo se nastavit servery DNS Ověřit certifikáty serveru Pokud je tato možnost zaškrtnuta, ověřuje baresip certifikáty TLS uživatelského agenta SIP a proxy serverů SIP, pokud se používá přenos TLS. Události v pásmu RTP Žádosti SIP INFO Vybere protokol šifrování přenosu média (pokud existuje). \n • ZRTP (doporučeno) znamená, že se po navázání hovoru vyzkouší vyjednávání o koncové šifrování médií ZRTP. \n • DTLS-SRTPF znamená, že v odchozím hovoru je nabízen UDP/TLS/RTP/SAVPF a že v případě příchozího hovoru je použit RTP/SAVP, RTP/SAVPF, UDP/TLS/RTP/SAVP nebo UDP/TLS/RTP/SAVPF. \n • SRTP-MANDF znamená, že RTP/SAVPF je nabízen v odchozím hovoru a vyžadován v příchozím hovoru. \n • SRTP-MAND znamená, že RTP/SAVP je nabízen v odchozím hovoru a vyžadován v příchozím hovoru. \n • SRTP znamená, že RTP/AVP je nabízen v odchozím hovoru a že RTP/SAVP nebo RTP/SAVPF je použit, pokud je nabízen v příchozím hovoru. Vybírá způsob odesílání tónů DTMF 0-9, #, * a A-D. URI hlasové schránky Režim DTMF Režim odpovědi Ručně Automaticky Kód země SIP URI pro kontrolu zpráv hlasové pošty. Pokud je ponecháno prázdné, zprávy hlasové pošty (indikace čekající zprávy) nejsou přihlášeny k odběru. Kód země E.164 tohoto účtu. Pokud uživatelská část \"Od\" v URI příchozího hovoru nebo zprávy obsahuje telefonní číslo, které nezačíná znakem \"+\", a pokud se vyhledání kontaktu nezdaří, je číslo předřazeno tomuto kódu země a vyhledání kontaktu je zopakováno. Pokud telefonní číslo začíná jedinou číslicí \"0\", číslice \"0\" se před prefixací čísla odstraní. Výchozí účet Neplatný kód země \'%1$s\' Hostitelská část SIP URI používaná při volání na telefonní čísla. Výchozí tovární nastavení je doména účtu. Pokud není zadána, nelze tento účet použít pro volání na telefonní čísla. Pokud je zatrženo, je tento účet vybrán při spuštění baresipu. Neplatná část hostitelského URI SIP \'%1$s\' SIP URI nového účtu Účty Účet \'%1$s\' již existuje. Dešifrovat heslo SIP URI nového účtu v následující podobě: <uživatel>@<doména>[:<port>][;transport=udp|tcp|tls]. Pokud je zadán <port> a transportní protokol není zadán, je výchozí transportní protokol UDP. Pokud není zadán <port> a je zadán transportní protokol, je výchozí hodnota <port> 5060 nebo 5061 (TLS). Pokud není zadán ani jeden z těchto parametrů a není zadán žádný odchozí proxy server, je registrátor účtu (pokud existuje) určen pouze na základě DNS informací o doméně. Nepodařilo se vytvořit nový účet. Chcete odstranit účet \'%1$s\'\? Zmeškaný hovor od Zmeškané hovory Hovor Žádost o přepojení hovoru na Povolit Historie volání volání Podrobnosti o volání volání Pokud je zaškrtnuto, byl nebo bude načten soubor obsahující certifikáty TLS takových certifikačních autorit, které nejsou součástí operačního systému Android. Ve verzi systému Android 9 se ze složky Download načte soubor s názvem \'ca_certs.crt\'. Nastavení zvuku Průměrná maximální přenosová rychlost používaná zvukovým tokem Opus. Platné hodnoty jsou 6000-510000. Výchozí hodnota je 28000. Očekávaná ztráta paketů Opus Očekávaná procentuální ztráta paketů zvukového toku Opus od 0 do 100. Výchozí hodnota je 1. Hodnota 0 také vypíná funkci Opus Forward Error Correction (FEC). Zvukové moduly Výchozí hlasitost volání Vynutit tmavé téma Nepodařilo se načíst modul. Zvukové kodeky poskytované zaškrtnutými moduly jsou k dispozici pro použití v účtech. Neplatné procento ztráty paketů Opus Neplatný datový tok Opus Pokud je nastaveno, je výchozí hlasitost zvuku hovoru na stupnici 1-10. Trasování SIP Tmavé téma Obojí Rozlišení videa Rozlišení videa (šířka x výška) Vybírá, zda se použijí kontakty baresip, kontakty Android nebo obojí. Pokud jsou použita volba obojí a v obou kontaktech existuje kontakt se stejným jménem, bude vybrán kontakt baresip. Pokud je zaškrtnuto, poskytuje zprávy protokolu Logcat na úrovni ladění a informací. Pokud je zaškrtnuto a pokud je zaškrtnuta volba Debug, zprávy Logcat obsahují také trasování požadavků a odpovědí SIP. Zruší se automaticky při spuštění baresipu. Obnovit výchozí nastavení Pokud je zaškrtnuto, nastavení se obnoví na výchozí hodnoty. Jste si jisti, že chcete obnovit výchozí hodnoty\? Obnovit Nepodařilo se načíst soubor \'cert.pem\'. Nepodařilo se načíst soubor \'ca_certs.crt\'. Kontakty Nový kontakt Pro aktivaci nových nastavení je třeba restartovat baresip. Restartovat nyní\? Žádost o souhlas SIP nebo tel. URI Pokud jsou vybrány kontakty Android, lze je použít při volání a zasílání zpráv jako odkazy na SIP a tel. URI. Aplikace baresip neukládá kontakty Android ani je s nikým nesdílí. Aby byly kontakty pro Android dostupné v baresip, Google vyžaduje, abyste souhlasili s jejich používáním, jak je popsáno zde v Zásadách ochrany osobních údajů. Jméno uživatel@doména nebo telefonní číslo Kontakt \'%1$s\' již existuje. Pokud je zaškrtnuto, je tento kontakt přidán do kontaktů systému Android. Profilový obrázek Neplatné jméno kontaktu \'%1$s\' Chcete odstranit kontakt \'%1$s\'\? Upozornění Přidat Odeslat zprávu Chcete zavolat nebo odeslat zprávu \'%1$s\'\? Zrušit Ano Odmítnout Informace Oznámení OK Přijmout Smazat Ne Chyba Potvrzení Upravit Stav Anonymní Neznámý Neplatný SIP nebo tel. URI \'%1$s\' Obnovení Restartovat Záloha O aplikaci Ukončit Zavolat na … Přesměrováno … Volání z … Ztracené Přepojení hovoru Účet \'%1$s\' nemá žádného poskytovatele telefonních služeb Žádost o video Akceptovat odesílání a příjem videa s \'%1$s\'\? Přijmout odeslání videa na \'%1$s\'\? Přijímat video od \'%1$s\'\? Video hovor Hovor je přidržen S konzultací Cíl přepojení Zvolit cílové URI Přepojit Informace o volání Doba trvání: %1$d (secs) Kodeky Aktuální rychlost: %1$s (Kbits/s) Průměrná rychlost: %1$s (Kbits/s) Pakety Nahrávání lze zapnout nebo vypnout pouze v případě, kdy hovor není spojen Naslepo Přepojení selhalo Volání ukončeno Žádná podporovaná videokamera! DTMF Žádné dostupné informace Zpoždění paketů (Jitter:) %1$s (ms) nové zprávy a Poslouchat Ověřit žádost Chcete ověřit SAS <%1$s>\? Máte jednu novou zprávu jednu starou zprávu Nemáte žádné zprávy Baresip se nepodařilo spustit. Příčinou může být neplatná hodnota Nastavení. Zkontrolujte adresu pro naslouchání, soubor certifikátu TLS a soubor certifikační autority TLS. Poté restartujte baresip. Registrace %1$s se nezdařila. Hlasové zprávy staré zprávy Požadavek na přepojení Souhlasíte s přepojením tohoto hovoru na \'%1$s\'\? Toto volání je BEZPEČNÉ a účastník je OVĚŘENÝ! Chcete zrušit ověření účastníka\? Data aplikace byla zálohována do souboru \'%1$s\'. Ve verzi systému Android 9 se soubor nachází ve složce Download. Volání se nezdařilo Toto volání je BEZPEČNÉ, ale účastník není ověřen! Zrušit ověření Tuto aplikaci nemůžete používat bez povolení \"Oznámení\". Bez oprávnění \"Kontakty\" nemáte přístup ke kontaktům systému Android. Odůvodnění povolení Tento hovor NENÍ zabezpečený! Nepodařilo se zálohovat data aplikace do souboru \'%1$s\'. Zkontrolujte Aplikace → baresip → Oprávnění → Úložiště. Data aplikace jsou obnovena. baresip je třeba restartovat. Restartovat nyní\? Nepodařilo se obnovit data aplikace. Zkontrolujte, zda jste zadali správné heslo a zda záložní soubor pochází z této aplikace. Ve verzi systému Android 9 také zkontrolujte Aplikace → baresip → Oprávnění → Úložiště a zda soubor \'%1$s\' existuje ve složce Download. baresip pro hlasové hovory potřebuje oprávnění \"Mikrofon\". Pro uskutečňování nebo přijímání videohovorů udělte oprávnění pro přístup \"Kameře\". Zálohu nelze vytvořit bez oprávnění pro přístup k \"Úložišti\". Bez oprávnění pro přístup k \"Úložišti\" nelze zálohu obnovit. Zaměření zvuku zamítnuto! baresip+ potřebuje oprávnění \"Mikrofon\" pro hlasové hovory, oprávnění \"Kamera\" pro videohovory, oprávnění \"Blízká zařízení\" pro detekci mikrofonu/reproduktoru Bluetooth, oprávnění \"Oznámení\" pro odesílání oznámení a na Androidu 9 oprávnění „Úložiště“ pro operace zálohování a obnovení. Bez připojení k síti! baresip potřebuje oprávnění \"Mikrofon\" pro hlasové hovory, oprávnění \"Blízká zařízení\" pro detekci mikrofonu/reproduktoru Bluetooth, oprávnění \"Oznámení\" pro odesílání oznámení a na Androidu 9 oprávnění „Úložiště“ pro operace zálohování a obnovení. Vybírá, které IP adresy baresip používá. Pokud je vybráno IPv4 nebo IPv6, baresip používá pouze IPv4 nebo IPv6 adresy. Pokud není vybrána žádná z těchto možností, používá baresip jak IPv4, tak IPv6 adresy. Rodina adres Neplatná hodnota zpoždění zvuku \'%1$s\'. Platné hodnoty jsou od 100 do 3000 ms. Doba (v milisekundách), po kterou se čeká na zvuk od volaného při navázání hovoru. Nastavte vyšší hodnotu, pokud na začátku hovoru zvuk volaného neslyšíte. Zpoždění zvuku Automaticky odmítat volání od %1$s Výchozí aplikace telefonu Role číselníku není k dispozici Pokud je zaškrtnuto, je výchozí aplikací telefonu baresip. Nezaškrtávejte, pokud vaše zařízení může potřebovat zpracovávat i jiné než SIP hovory nebo zprávy. Automatické přesměrování na \'%1$s\'\\ Oblíbené Automatické spuštění vyžaduje oprávnění Zobrazení přes ostatní aplikace. Vybírá, zda bude požadavek na přesměrování hovoru následován automaticky, nebo zda bude vyžadováno potvrzení. Souhlasíte s přesměrováním volání na \'%1$s\'? Režim přesměrování Žádost o přesměrování Region tónů Pokud je zaškrtnuto, tak avizovat podporu spolehlivých předběžných odpovědí (RFC 3262). Země vyzvánění, čekajícího hovoru a obsazených tónů volajícího Spolehlivé předběžné odpovědi Nepodařilo se obnovit data aplikace. Systém Android verze 14 a vyšší neumožňuje obnovení dat, která byla zálohována před verzí %1$s %2$s. Hlasitý poslech Pokud je zatrženo, hlasitý poslech se zapne automaticky při zahájení hovoru. In-band RTP nebo SIP INFO Žádost o volání Přijímáte požadavek na volání \'%1$s\'? Snímky za sekundu Snímková frekvence videa, která bude nabídnuta během vyjednávání SDP. Platné hodnoty jsou od 10 do 30. Neplatný počet snímků za sekundu \'%1$d\' Uživatelský agent Vlastní hodnota pole hlavičky User-Agent pro SIP požadavek/odpověď Neplatná hodnota pole hlavičky User-Agent Zesílení mikrofonu Vynásobte hlasitost mikrofonu tímto desetinným číslem. Minimální hodnota je 1,0 (výchozí hodnota), která vypne zesílení mikrofonu. Vyšší hodnoty mohou negativně ovlivnit kvalitu zvuku. Neplatná hodnota zesílení mikrofonu Vyzvánění Vybrat vyzvánění Numerická klávesnice Pokud je zatrženo, zobrazí se při zaměření pole \"Zavolat na ...\" numerická klávesnice. Odpovědět Uložit Přehrávání nahrávky … Hovor byl přijat Hovor byl přijat jinde Zmeškaný hovor Odmítnutý hovor Aplikace nemá oprávnění k externímu úložišti Barvoslepost Používat ikony stavu registrace přizpůsobené barvosleposti Je-li zaškrtnuto, kontakt se zobrazí mezi oblíbenými v horní části seznamu kontaktů. Hardwarové potlačení ozvěny není dostupné! Nahrávání hovoru Pokud je aktivováno, nové příchozí i odchozí hovory budou nahrávány. Nahrávky lze přehrát na stránce historie volání. Mikrofon Při aktivaci během hovoru se mikrofon ztlumí. Reproduktor Po aktivaci se zvuk přehrává přes reproduktor zařízení. Detekce přiblížení Při hovorech používat snímač přiblížení (automatické vypnutí displeje). Dynamické barvy Použijí se dynamické barvy, pokud jsou povoleny v nastavení Androidu SIP klient založený na knihovně baresip

Juha Heinanen <jh@tutpro.com>

Verze %1$s


Nápověda

  • Zkontrolujte, zda výchozí hodnoty nastavení vyhovují vašim potřebám (nápovědu zobrazíte klepnutím na názvy položek).
  • Poté v sekci Účty vytvořte jeden nebo více účtů (pro nápovědu opět klepněte na názvy položek).
  • Stav registrace účtu je indikován barevnou tečkou: zelená (registrace proběhla úspěšně), žlutá (registrace probíhá), červená (registrace se nezdařila), bílá (registrace nezačala).
  • Klepnutím na stavovou ikonu (nalevo od názvu účtu) spustí konfiguraci účtu.
  • Dlouhým dotykem na ikony na liště baresip zobrazíte informace o ikonách.
  • Přejetí prstem dolů po obrazovce (gesto) zahájí opětovnou registraci aktuálně zobrazeného účtu.
  • Dlouhým dotykem na aktuálně zobrazený název účtu povolíte nebo zakážete jeho registraci.
  • Mezi účty se můžete přepnout přejetím prstem vlevo/vpravo po obrazovce (gesto).
  • Poslední volaný kontakt lze znovu rychle vybrat klepnutím na ikonu hovoru, pokud je pole "Zavolat na ..." prázdné.
  • Účastníky hovorů a zpráv lze přidávat do kontaktů dlouhým podržením.
  • Dlouhým podržením lze také odstraňovat hovory, chaty, zprávy a kontakty.
  • Dotyk/dlouhý dotyk na ikonu kontaktu lze použít k přidání/odstranění obrázkového avatara.
  • Dlouhým dotykem na zvukový kodek lze kodek povolit/zakázat.
  • Navštivte Wiki pro více informací.

Zásady ochrany osobních údajů

Zásady ochrany osobních údajů jsou k dispozici zde.


Zdrojový kód

Zdrojový kód je k dispozici na GitHubu, kde je také možno nahlásit problémy, náměty a požadavky.


Překlady aplikace do jiných jazyků

Překlady jsou realizovány skrze projekt baresip Weblate.


Licence

  • BSD-3-Clause kromě následujících:
  • Apache 2.0 AMR kodeky a TLS zabezpečení
  • AGPLv4 ZRTP šifrování médií
  • GNU LGPL 2.1 Codec2 codec
  • Free G.722 codec
  • GNU GPLv3 G.729 kodek
]]>
Ověřovat požadavky na registraci Pokud je tato volba zapnutá, příchozí požadavky jsou povoleny pouze z IP adres, na které byly odeslány registrační požadavky. Blokovat neznámé kontakty Blokovat hovory a zprávy od protějšků, kteří nejsou v kontaktech. volá Zablokovaný hovor od %1$s byl automaticky odmítnut Zablokovaná zpráva od %1$s byla automaticky odmítnuta Zablokované aktivity Zablokované hovory Zablokované zprávy Smazat zablokované požadavky od \'%1$s\'? Přidat \'%1$s\' do kontaktů? Uložit nahrávku Uložit tuto nahrávku? Nahrávka byla uložena Smazat tento hovor z historie? Transportní protokoly Čárkami oddělený seznam podporovaných transportních protokolů SIP pro požadavky a odpovědi. Pokud je pole prázdné, použije se výchozí hodnota ‚udp,tcp,tls,ws,wss‘, která zahrnuje všechny podporované transportní protokoly. Uveďte pouze ty, které skutečně potřebujete. Použití UDP se z bezpečnostních i dalších důvodů nedoporučuje. Neplatný seznam transportních protokolů Zastavit Účet Neplatná přezdívka účtu \'%1$s\' Přezdívka \'%1$s\' již existuje Neplatné zobrazované jméno \'%1$s\' Uživatelské jméno Neplatné heslo \'%1$s\' Uživatelské jméno STUN/TURN Neplatné uživatelské jméno \'%1$s\' Neplatný uživatel@doména[:port][;transport=udp|tcp|tls] \'%1$s\' Šifrovat heslo %1$d zmeškaných hovorů Video kodeky Soubor TLS certifikátu Datový tok Opus Ladění Vyhledávání Již probíhá jiný hovor. Požadavek na restart SIP klient založený na knihovně baresip

Juha Heinanen <jh@tutpro.com>

Verze %1$s


Nápověda

  • Zkontrolujte, zda výchozí hodnoty v nastavení baresipu vyhovují vašim potřebám (nápovědu získáte ťuknutím na názvy položek).
  • Poté v sekci Účty vytvořte jeden nebo více účtů (pro nápovědu opět ťukněte na názvy položek).
  • Stav registrace účtu je indikován barevnou tečkou: zelená (registrace proběhla úspěšně), žlutá (registrace probíhá), červená (registrace se nezdařila), bílá (registrace nezačala).
  • Klepnutí na stavovou ikonu (nalevo od názvu účtu) spustí konfiguraci účtu.
  • Dlouhým dotykem na ikony na liště baresip zobrazíte informace o ikonách.
  • Přejetí prstem dolů po obrazovce (gesto) zahájí opětovnou registraci aktuálně zobrazeného účtu.
  • Dlouhým dotykem na aktuálně zobrazený účet povolíte nebo zakážete jeho registraci.
  • Mezi účty se můžete přepnout přejetím prstem vlevo/vpravo po obrazovce (gesto).
  • Poslední volaný kontakt lze znovu rychle vybrat klepnutím na ikonu hovoru, pokud je pole "Zavolat na ..." prázdné.
  • Účastníky hovorů a zpráv lze přidávat do kontaktů dlouhým podržením.
  • Dlouhým podržením lze také odstraňovat hovory, chaty, zprávy a kontakty.
  • Dotyk/dlouhý dotyk na ikonu kontaktu lze použít k přidání/odstranění obrázkového avatara.
  • Dlouhým dotykem na zvukový kodek lze kodek povolit/zakázat.
  • Navštivte Wiki pro více informací.

Známé problémy

  • Náhled sebe sama se nezobrazuje správně, když je video stream pouze pro odesílání..

Zásady ochrany osobních údajů

Zásady ochrany osobních údajů jsou k dispozici zde.


Zdrojový kód

Zdrojový kód je k dispozici na GitHubu, kde je také možno nahlásit problémy, náměty a požadavky.


Překlady do jiných jazyků

Překlady jsou realizovány skrze projekt baresip Weblate.


Licence

  • BSD-3-Clause kromě následujících:
  • Apache 2.0 AMR codecs a TLS security
  • AGPLv4 šifrování médií ZRTP
  • GNU LGPL 2.1 Codec2 codec
  • Free G.722 codec
  • GNU GPLv3 kodek G.729
  • GNU GPLv2 H.264 a H.265 kodeky
  • AOMedia AV1 kodek
]]>
Jedinečné URI kontaktu Pokud je zaškrtnuto, URI kontaktu je zaručeně jedinečné. Je nutné tuto volbu zapnout, pokud existuje více účtů se stejnou uživatelskou částí SIP URI, a zároveň zlepšuje ochranu účtů proti útokům.
================================================ FILE: app/src/main/res/values-de/strings.xml ================================================ Über baresip Über baresip+ Konto Anzeigename Der Nickname \'%1$s\' existiert bereits Der Nickname (falls vorhanden), der zur Identifikation des Kontos innerhalb der baresip App genutzt wird. Nickname Ungültiger Konto Nickname \'%1$s\' Rufumleitungsmodus Telefonieanbieter Anrufübertragungsanfrage an Über Wenn angeklickt wird das Registrieren aktiviert und REGISTER-Anfragen werden in dem durch das Registrierenintervall angegebenen Intervall gesendet. Registrierenintervall Gibt an, wie oft (in Sekunden) baresip REGISTER-Anfragen sendet. Gültige Werte: 60 bis 3600. Ungültiges Registrierenintervall \'%1$s\' Ungültiger Benutzername \'%1$s\' STUN/TURN Passwort Passwort, wenn der STUN/TURN Server eines verlangt Ungültiges Passwort \'%1$s\' Verschlüsselung Manuell Automatisch Sprachnachrichten URI Ländercode SIP URI zur Überprüfung von Sprachnachrichten. Wenn leer gelassen, werden Voicemail-Nachrichten (Message Waiting Indications) nicht abonniert. Konto \'%1$s\' existiert bereits. Neues Konto konnte nicht zugeordnet werden. Passwort verschlüsseln Möchten Sie Konto \'%1$s\' löschen? Verpasste Anrufe %1$d verpasste Anrufe Chat mit %1$s Heute Möchten Sie den Chat mit \'%1$s\' löschen? Autostart Keine Informationen verfügbar Dauer: %1$d (sek) Aktuelle Übertragungsrate: %1$s (Kbits/s) Mittlere Rate: %1$s (Kbits/s) Pakete SIP oder URI Wenn angeklickt werden RTP und RTCP Pakete auf einen einzelnen Port gemultiplext (RFC 5761). Antwort Modus SIP INFO Anfragen Bestimmt, wie die DTMF Töne 0–9, #, *, und A-D gesendet werden. DTMF Modus Bestimmt wie eingehende Rufe beantwortet werden. SIP URI-Hostteil bei Anrufen zu Telefonnummern. Voreinstellung ist die Domain des Kontos. Wenn nicht angegeben, kann dieses Konto nicht verwendet werden, um Telefonnummern anzurufen. Ungültiger SIP URI-Hostteil \'%1$s \' Wenn angeklickt, wird dieses Konto beim Start von Baresip vorausgewählt. Voreingestelltes Konto Gesprächspartner Ungültiger DNS Server Anrufdetails Name (falls gewünscht) für die Von-URI bei ausgehenden Anfragen. Benutzername für die Authentifizierung, wenn eine Authentifizierung von SIP-Anfragen erforderlich ist. Standardwert ist der Benutzername des Kontos. Ungültiger Anzeigename \'%1$s\' Benutzername für die Authentifizierung Ungültiger Benutzername für die Authentifizierung \'%1$s\' Passwort für die Authentifizierung Passwort mit max. 64 Zeichen. Wenn ein Benutzername eingegeben wurde, aber kein Passwort eingegeben wird, wird beim Start von baresip danach gefragt. Ungültiges Passwort \'%1$s\' SIP URI des Proxy Servers SIP URI eines anderen Proxy Servers Ungültige Proxy Server URI \'%1$s\' Registrieren Wählt ein oder kein media NAT traversal protocol. Mögliche Werte sind STUN (Session Traversal Utilities for NAT, RFC 5389) und ICE (Interactive Connectivity Establishment, RFC 5245). Konten SIP URI des neuen Kontos Passwort entschlüsseln Neue Nachricht Name Auto-abgewiesener Anruf von %1$s Anruf Anrufliste Ungültige Listening-Adresse TLS Zertifikat-Datei Wollen Sie \'%1$s\' anrufen oder eine Nachricht senden? Unbekannt Konto \'%1$s\' hat keinen Telefonanbieter Verloren Dieser Anruf ist SICHER, aber die Gegenstelle ist NICHT verifiziert! Anrufe Anruf Richtung Zeit Möchten Sie \'%1$s\' zu Kontakten hinzufügen oder %2$s aus der Anrufliste löschen? Möchten Sie \'%1$s\' %2$s aus der Anrufliste löschen? Möchten Sie den Anrufverlauf des Kontos \'%1$s löschen? Möchten Sie die Nachricht löschen oder Gesprächspartner \'%1$s\' zu Kontakten hinzufügen? Fehlgeschlagen Einstellungen Wenn angeklickt wird Baresip automatisch ausgeführt nach dem Gerät (Neu)Start. Batterieoptimierungen DNS Server konnte nicht eingestellt werden Sprachnachrichten Sie haben neue Nachrichten Neuer Kontakt benutzer@domain oder Telefonnummer Kontakt \'%1$s\' existiert schon. Profilbild Wollen Sie Kontakt \'%1$s\' löschen? Alarm Nachricht Abbrechen Ja Nein Bestätigung Neustarten Wiederherstellen Beenden Anruf zu … Anruf von … Umgeleitet von … Videoanruf Ruf gehalten Aufnahme kann nur ein- oder ausgeschalten werden wenn der Ruf nicht verbunden ist eine neue Nachricht alte Nachrichten Sie haben keine Nachrichten und Hören Sie haben schon einen aktiven Anruf. ’%1$s‘ konnte nicht registriert werden. Anruf fehlgeschlagen Anruf beendet Dieser Anruf ist NICHT sicher! Geben Sie \"Kamera\" Berechtigung, um Videoanrufe zu machen oder zu empfangen. Sie können auf Android-Kontakte ohne \"Kontakte\" Berechtigung nicht zugreifen. baresip+ benötigt die \"Mikrofon\"-Berechtigung für Sprachanrufe, die \"Kamera\"-Berechtigung für Videoanrufe, die \"Geräte in der Nähe\"-Berechtigung für Bluetooth-Mikrofon/Hörer-Erkennung, die \"Benachrichtigungen\"-Berechtigung für die Benachrichtigungen und bei Android 9 die \"Speicher\"-Berechtigung zur Datensicherung/ Wiederherstellung. Ungültiger Kontaktname \'%1$s\' Favorit Wenn angeklickt, wird der Kontakt zum Android Adressbuch hinzugefügt. Dauer Ungültiger Ländercode \'%1$s Möchten Sie die Nachricht löschen? Kontakt hinzufügen Chatverlauf Deaktivieren Aktivieren eine alte Nachricht Dieser Anruf ist SICHER, und die Gegenstelle ist VERIFIZIERT! Wollen Sie die Gegenstelle unsicher erklären? Unsicher erklären Anwendungsdaten (keine Aufnahmen) gesichert in \'%1$s\'. In Android Version 9 ist die Datei im Download-Ordner. Neustarten Anonym Nachricht senden Kontakte Akzeptieren Löschen Fehler Hinzufügen Bearbeiten Sie können diese Anwendung nicht ohne \"Benachrichtigungen\"-Berechtigung verwenden. Ungültiger SIP or tel URI \'%1$s\' Verweigern Keine Netzwerk Verbindung! Proxies für ausgehende Verbindungen Deaktivieren Sie die Akku-Optimierungen (empfohlen), wenn Sie die Wahrscheinlichkeit reduzieren möchten, dass Android den Zugriff von Baresip auf das Netzwerk einschränkt oder Baresip in den Standby-Zustand versetzt. Standard Telefonie-App Autostart benötigt Erlaubnis \"Über anderen Apps einblenden\". Wenn angeklickt ist Baresip die Standard-Telefon-App. Nicht anklicken, wenn Ihr Gerät auch andere als SIP-Anrufe oder -Nachrichten annehmen soll. Listening Adresse kann nicht als Wählgerät dienen DNS Server Serverzertifikate überprüfen Bei Anklicken werden von Baresip TLS-Zertifikate von SIP User Agent und SIP Proxy Servern bei Verwendung von TLS-Transport überprüft. Sicherung nach \'%1$s\' fehlgeschlagen. Überprüfen Sie Apps → baresip → Berechtigungen → Speicher. Anwendungsdaten wurden wiederhergestellt und baresip muß neugestartet werden. Neustarten? Wiederherstellung der Anwendungsdaten fehlgeschlagen. Überprüfen Sie das eingegebene Passwort und dass die Wiederherstellungsdatei von dieser Anwendung stammt. In Android Version 9: Überprüfen Sie Apps → baresip → Berechtigungen → Speicher, sowie, dass \'%1$s\' im Download Ordner existiert. baresip braucht \"Mikrofon\" Berechtigung für Telefonanrufe. Keine unterstützten Kameras! Eine Sicherungskopie kann nicht ohne die \"Speicher\"-Berechtigung erstellt werden. Sie können keine Sicherung wiederherstellen ohne \"Speicher\"-Berechtigung. Ungültiger STUN/TURN Server URI \'%1$s\' STUN/TURN Benutzername Benutzername, wenn der STUN/TURN Server einen verlangt Ruf Informationen Verpasster Anruf von Nachricht senden fehlgeschlagen Möchten Sie den Chat-Verlauf des Kontos \'%1$s\' löschen? Zuverlässige temporäre Antworten Wenn angeklickt gibt das Unterstützung für zuverlässige vorläufige Antworten an (RFC 3262). Bestimmt ob eine Rufumleitungsanfrage automatisch befolgt wird oder erst nach einer Bestätigung (manuell). Du Ungültiger benutzer@domain[:port][;transport=udp|tcp|tls] \'%1$s\' Möchten Sie den Chat mit Partner \'%1$s\' löschen oder Partner zu den Kontakten hinzufügen? Neuer Chat Partner SIP URI von einem oder zwei Proxies, die beim Senden von Anfragen verwendet werden müssen. Wenn zwei angegeben sind werden REGISTER Anfragen an beide gesendet, andere Anfragen an den ersten antwortenden. Wenn keine Proxies für ausgehende Verbindungen angegeben sind, werden Verbindungen basiert auf DNS NAPTR/SRV/A record lookup des hostparts der URI der Rufadresse gesendet. Wenn der hostpart der SIP URI eine IPv6 Adresse ist, muß diese in eckigen Klammern geschrieben werden []. \nBeispiele: \n • sip:example.com:5061;transport=tls \n • sip:[2001:67c:223:777::10];transport=tcp \n • sip:192.168.43.50:443;transport=wss Ein STUN/TURN Server URI in der Form: schema:host[:port][?transport=udp|tcp], wobei schema \'stun\', \'stuns\', \'turn\', oder \'turns\' ist. Voreingestellter STUN Server für STUN und ICE Protokoll ist \'stun:stun.l.google.com:19302\' der zum öffentlichen Google STUN Server verweist. Es gibt keinen voreingestellten TURN Server. Wählt eine (oder keine) Medientransportverschlüsselung. \n • ZRTP (recommended) means that ZRTP end-to-end media encryption negotiation is tried afterthe call has been established. \n • DTLS-SRTPF means that UDP/TLS/RTP/SAVPF is offered in outgoing call and that RTP/SAVP,RTP/SAVPF, UDP/TLS/RTP/SAVP, or UDP/TLS/RTP/SAVPF is used if offered in incoming call. \n • SRTP-MANDF means that RTP/SAVPF is offered in outgoing call and required in incoming call. \n • SRTP-MAND means that RTP/SAVP is offered in outgoing call and required in incoming call. \n • SRTP means that RTP/AVP is offered in outgoing call and that RTP/SAVP or RTP/SAVPF is usedif offered in incoming call. Baresip konnte nicht gestartet werden. Das kann wegen eines ungültigen Einstellungswertes sein. Überprüfen Sie die Hören (Listen) Adresse, TLS Zertifikat, TLS CA File. Starten Sie dann baresip neu. baresip benötigt die \"Mikrofon\"-Berechtigung für Sprachanrufe, die \"Geräte in der Nähe\"-Berechtigung für Bluetooth-Mikrofon/Hörer-Erkennung, die \"Benachrichtigungen\"-Berechtigung für die Benachrichtigungen und bei Android 9 die \"Speicher\"-Berechtigung zur Datensicherung/ Wiederherstellung. SIP User Agent basierend auf der baresip Bibliothek

Juha Heinanen <jh@tutpro.com>

Version %1$s


Benutzerhinweise

  • Überprüfen Sie, dass die Einstellungen in baresip Ihren Bedürfnissen entsprechen (tippen Sie auf die Überschriften für Hilfe).
  • Unter Konten, erstellen Sie mindestens eines (tippen Sie für Hilfe auf die einzelnen Punkte).
  • Der Registrierungsstatus eines Kontos wird mit einem farbigen Punkt markiert: grün (Registrierung erfolgreich), gelb (Registrierung im Gange), rot (Registrierung fehlgeschlagen), weiß (Konto ist nicht aktiviert).
  • Tippen auf den Status-Punkt öffnet direkt die Kontokonfiguration.
  • Ein langes Antippen der Navigationssymbole zeigt weitere Informationen zu den Symbolen an.
  • Nach-unten-Wischen registriert das aktuelle Konto erneut.
  • Langes Drücken auf das gegenwärtige Konto aktiviert/ deaktiviert die Registrierung.
  • Links/Rechts-Wischen schaltet durch die verschiedenen Konten.
  • Durch Berühren des Anruf-Symbols kann der letzte Anrufer erneut gewählt werden, wenn das Anruffeld leer ist.
  • Anrufer und Absender von Nachrichten können durch langes Drücken zu den Kontakten hinzugefügt werden.
  • Langes Drücken kann ebenfalls verwendet werden um Anrufe, Chats, Nachrichten und Kontakte zu löschen.
  • Drücken/langes Drücken eines Kontaktfotos kann benutzt werden, um das Bild zu installieren/ entfernen.
  • Langes Drücken eines Audiocodecs kann den Codec aktivieren/ deaktivieren.
  • Im Wiki finden sich mehr Informationen.

Datenschutzerklärung

Die Datenschutzerklärung ist hier (Englisch) zu finden.


Quellcode

Der Quellcode ist auf GitHub verfügbar, wo auch Fehler gemeldet werden können.


Lizenzen

  • BSD-3-Clause außer Folgendem:
  • Apache 2.0 AMR-Codecs und TLS-Sicherheit
  • AGPLv4 ZRTP-Medienverschlüsselung
  • GNU LGPL 2.1 G.722, G.726, und Codec2 Codecs
  • GNU GPLv3 G.729 Codec
]]>
SIP User Agent basierend auf der baresip Bibliothek

Juha Heinanen <jh@tutpro.com>

Version %1$s


Benutzerhinweise

  • Überprüfen Sie, dass die Einstellungen in baresip+ Ihren Bedürfnissen entsprechen (tippen Sie auf die Überschriften für Hilfe).
  • Unter Konten, erstellen Sie mindestens eines (tippen Sie für Hilfe auf die einzelnen Punkte).
  • Der Registrierungsstatus eines Kontos wird mit einem farbigen Punkt markiert: grün (Registrierung erfolgreich), gelb (Registrierung im Gange), rot (Registrierung fehlgeschlagen), weiß (Konto ist nicht aktiviert).
  • Tippen auf den Status-Punkt öffnet direkt die Kontokonfiguration.
  • Ein langes Antippen der Navigationssymbole zeigt weitere Informationen zu den Symbolen an.
  • Nach-unten-Wischen registriert das aktuelle Konto erneut.
  • Langes Drücken auf das gegenwärtige Konto aktiviert/ deaktiviert die Registrierung.
  • Links/Rechts-Wischen schaltet durch die verschiedenen Konten.
  • Durch Berühren des Anruf-Symbols kann der letzte Anrufer erneut gewählt werden, wenn das Anruffeld leer ist.
  • Anrufer und Absender von Nachrichten können durch langes Drücken zu den Kontakten hinzugefügt werden.
  • Langes Drücken kann ebenfalls verwendet werden um Anrufe, Chats, Nachrichten und Kontakte zu löschen.
  • Drücken/langes Drücken eines Kontaktfotos kann benutzt werden, um das Bild zu installieren/ entfernen.
  • Langes Drücken eines Audio-/ Videocodecs kann den Codec aktivieren/ deaktivieren.
  • Im Wiki finden sich mehr Informationen.

Bekannte Probleme

  • Bei Videoanrufen muß das Gerät quer gehalten werden, um 90 Grad nach links geneigt, von der Hochkantposition betrachtet.
  • Das eigene Bild wird nicht richtig gezeigt, wenn Video auf Nursenden eingestellt ist.

Datenschutzerklärung

Die Datenschutzerklärung ist hier (Englisch) zu finden.


Quellcode

Der Quellcode ist auf GitHub verfügbar, wo auch Fehler gemeldet werden können.


Lizenzen

  • BSD-3-Clause außer Folgendem:
  • Apache 2.0 AMR-Codecs und TLS-Sicherheit
  • AGPLv4 ZRTP-Medienverschlüsselung
  • GNU LGPL 2.1 G.722, G.726, und Codec2 Codecs
  • GNU GPLv3 G.729 Codec
  • GNU GPLv2 H.264 und H.265 Codecs
  • AOMedia AV1 Codec
]]>
E.164 Ländercode dieses Kontos. Wenn der Benutzerteil der Von-URI des eingehenden Anrufs oder der Nachricht eine Telefonnummer enthält, die nicht mit \'+\'-Zeichen beginnt und wenn der Kontakt nicht bekannt ist, wird der Nummer dieser Ländercode vorangestellt und die Kontaktsuche beginnt erneut. Wenn die Telefonnummer mit einer einzigen Ziffer \'0\' beginnt, wird die Zahl \'0\' entfernt, bevor der Ländercode vorangestellt wird. SIP URI des neuen Kontos nach dem Muster <user>@<domain>[:<port>][;transport=udp|tcp|tls]. Wird <port> angegeben und das Transportprotokoll nicht angegeben, wird das Transportprotokoll standardmäßig auf UDP gesetzt. Wenn <port> nicht und das Transportprotokoll schon angegeben ist, wird <port> 5060 oder 5061 eingestellt (TLS). Wenn weder noch angegeben wird und kein Proxy für ausgehende Verbindungen angegeben wird, wird der Registrar (falls vorhanden) des Kontos ausschließlich auf Basis der DNS-Informationen der Domain ermittelt. IP-Adresse und Port in der Form \'address:port\', an der Baresip für eingehende SIP-Anfragen lauscht. Wenn die IP-Adresse eine IPv6-Adresse ist, muss sie innerhalb von Klammern geschrieben werden []. IPv4 Adresse 0.0.0.0 oder IPv6 Adresse [:] läßt Baresip an allen verfügbaren Adressen lauschen. Wenn leer gelassen (Standard), hört Baresip auf einem zufälligen Port aller verfügbaren Adressen. Wählt, welche IP-Adressen Baresip verwendet. Wird IPv4 oder IPv6 gewählt, verwendet Baresip nur IPv4 oder IPv6 Adressen. Wird nichts gewählt, verwendet Baresip sowohl IPv4 als auch IPv6 Adressen. Kommagetrennte Liste von Adressen von DNS-Servern. Wenn nicht angegeben, werden DNS-Serveradressen dynamisch aus dem System gewonnen. Jede DNS-Adresse ist in der Form \'ip:port\' oder \'ip\'. Wenn der Port weggelassen ist, wird dieser auf 53 gesetzt. Wenn ip eine IPv6-Adresse ist und auch Port angegeben ist, muss ip in Klammern geschrieben werden []. Als Beispiel zeigt die Liste \'8.8.8.8.8:53,[2001:4860:4860:::888888]:53\' auf IPv4 und IPv6 Adressen öffentlicher Google DNS-Server. Falls angeklickt, wurde oder wird eine Datei mit TLS-Zertifikat und privatem Schlüssel dieser Baresip-Instanz geladen. In Android-Version 9 wird eine Datei namens \'cert.pem\' aus dem Download-Ordner geladen. Löschen Sie aus Sicherheitsgründen diese Datei nach dem Laden. Video Codecs Audioeinstellungen Sicherungskopie RTCP Multiplexverfahren Ungültiger Opus Paketverlustprozentsatz Standardmäßige Anruflautstärke STUN/TURN Server RTP Ergeignisse im Frequenzband Audio Codecs TLS CA Datei Wenn Android Kontakte gewählt ist, können diese beim Telefonieren und Verschicken von Direktnachrichten als Referenzen für SIP und tel URIs genutzt werden. Die baresip App speichert keine Android Kontakte und teilt diese auch nicht. Um Android Kontakte in baresip verfügbar zu machen, erfordert Google Ihre Zustimmung zur Nutzung der Kontakte nach den Angaben in der Datenschutzerklärung der App. Mit Rückfrage (attended) Ziel-URI auswählen Transfer Diesen Anruf zu \'%1$s\' übertragen? Anrufsanfrage akzeptieren und \'%1$s\' anrufen? App-Daten konnten nicht wiederhergestellt werden. Ab Android 14 einschließlich können Daten nicht wiederhergestellt werden, die vor %1$s Version %2$s als Sicherungskopie gespeichert wurden. Tonverzögerung Informationen Lautsprecher Falls angekreuzt, wird die Lautsprecherfunktion automatisch aktiviert, wenn der Anruf beginnt. Audiomodule Audio Codecs, die von den ausgewählten Modulen bereitgestellt werden, sind für die Benutzer verfügbar. Opus Bitrate Durchschnittliche maximale Bitrate, die vom Opus Audiostream genutzt wird. Gültige Werte sind 6000-510000. Die Werkseinstellung beträgt 28000. Erwarteter Paketverlust von Opus Erwarteter Paketverlust des Opus Audiostreams in Prozent. Werkseinstellung ist 1. Der Wert 0 deaktiviert die Fehlerkorrektur von Opus (FEC). Ungültige Opus Bitrate Zeit (in Millisekunden), um auf Ton vom Angerufenen zu warten, sobald eine Verbindung zustandekommt. Erhöhen Sie den Wert, wenn Tonverlust am Anfang des Telefonats auftritt. Die Tonverzögerung von \'%1$s\' ist ungültig. Gültige Werte liegen zwischen 100 und 3000. Immer das dunkle Farbschema nutzen Debug Falls angekreuzt, werden Debug- und Info-Level Lognachrichten für Logcat bereitgestellt. Auf Werkseinstellungen zurücksetzen Zurücksetzen Um die neuen Einstellungen zu aktivieren, ist ein Neustart von baresip erforderlich. Jetzt neustraten? Datei \'cert.pem\' konnte nicht gelesen werden. Datei \'ca_certs.crt\' konnte nicht gelesen werden. Videoanfrage Zustimmen, dass Video mit \'%1$s\' gesendet und empfangen wird? Zustimmen, dass Video von \'%1$s\' empfangen wird? Anruftransfer Ohne Rückfrage (blind) Jitter: %1$s (ms) Automatische Weiterleitung zu \'%1$s\'\\ Umleitungsanfrage Einer Anrufsumleitung zu \'%1$s\' zustimmen? Anrufsanfrage Audio Fokus nicht gewährt! Gründe für die Berechtigungen Ungültige Bilder pro Sekunde: \'%1$d\' Wollen Sie wirklich die Einstellungen auf die Werkseinstellungen zurücksetzen? Status SAS <%1$s> verifizieren? Transferanfrage Zustimmen, dass Video zu \'%1$s\' gesendet wird? Anfrage verifizieren SIP Trace (Zurückverfolgung) Falls angekreuzt und falls Debug aktiviert ist, werden Logcat Nachrichten SIP Anfragen und Antwort-Traces enthalten. Wird beim Starten von baresip deaktiviert. OK Fehler beim Laden des Moduls. Wählt aus, ob baresip Kontakte, Android Kontakte, oder beide verwendet werden. Falls beide verwendet werden, werden bei Kontakten, die in beiden Kontaktverzeichnissen vorkommen, die baresip Kontakte genutzt. Beide Falls angekreuzt, werden die Einstellungen auf die Werkseinstellungen zurückgesetzt. Transferziel Transfer fehlgeschlagen DTMF Codecs Dunkles Farbschema RTP oder SIP INFO im Frequenzband Falls angekreuzt, wurde/ wird eine Datei geladen werden, die ein TLS Zertifikat einer Zertifikatsauthorität beinhaltet, die nicht in Android vorhanden ist. In der Android Version 9 wird die Datei \'ca_certs.crt\' aus dem Download-Verzeichnis geladen. Adressenfamilie NAT Durchquerung von Medien Falls aktiviert, liegt die standardmäßige Anruflautstärke auf einer Skala von 1-10. Größe der Videoframes Videoframes pro Sekunde Bildwiederholungsrate, die während einem SDP-Handshake angeboten werden wird. Gültige Werte von 10 bis 30. Größe der gesendeten Videoframes (Breite x Höhe) Herkunftsland der Töne Herkunftsland des Freitons, Halttons und Besetzttons Zustimmungsanfrage Mikrofonverstärkung Die Mikrofonlautstärke wird mit dieser Dezimalzahl multipliziert. Der Minimalwert ist 1.0 (Werkseinstellung) und deaktiviert die Mikrofonverstärkung. Größere Werte können die Tonqualität negativ beeinflussen. Ungültiger Wert für die Mikrofonverstärkung User Agent Benutzerdefinierter Wert für das SIP request/response User-Agent header-Feld Ungültiger Wert für das User-Agent header-Feld Ziffernblock" Falls angekreuzt, wird der Ziffernblock angezeigt, sobald das \"Anruf an ...\"- Feld fokussiert wird." Mikrofon Falls in einem Anruf aktiviert, wird das Mikrofon stummgestellt. Lautsprecher Keine Berechtigung, den externen Speicher zu lesen Falls angekreuzt, wird der Kontakt mit anderen Favoriten am Anfang der Kontaktliste angezeigt. Falls aktiviert, wird der Ton über den Lautsprecher des Geräts abgespielt. Klingelton Klingelton auswählen Keine hardwarebasierte akustische Echounterdrückung! Anrufsaufzeichnung Falls aktiviert, werden ein-/ ausgehende Anrufe aufgenommen. Aufnahmen lassen sich unter den Anrufsdetails abspielen Spiele Aufnahme ab… Antworten Speichern
================================================ FILE: app/src/main/res/values-el/strings.xml ================================================ Σχετικά για το baresip Λογαριασμός Εμφανιζόμενο όνομα Ονομασία (εάν υπάρχει) που χρησιμοποιείται στο URI εξερχόμενων αιτήσεων. SIP URI του διακομιστή μεσολάβησης SIP URI του άλλου διακομιστή μεσολάβησης Εγγραφή ================================================ FILE: app/src/main/res/values-es/strings.xml ================================================ Acerca de Baresip Agente de Usuario SIP basado en la biblioteca baresip

Juha Heinanen <jh@tutpro.com>

Versión %1$s


Consejos de uso

  • Compruebe que los valores predeterminados en la configuración de baresip se ajusten a sus necesidades (toque los títulos de los elementos para obtener ayuda).
  • Luego, en Cuentas, crea una o más cuentas (nuevamente toca los títulos de los elementos para obtener ayuda).
  • El estado de registro de una cuenta se muestra con un punto de color: verde (registro logrado), amarillo (el registro está en proceso), rojo (el registro falló), blanco (el registro está en progreso) no ha sido activado).
  • Al tocar el punto de estado se accede directamente a la configuración de la cuenta.
  • Mantén pulsado un icono de la barra de baresip para ver información sobre dicho icono.
  • El gesto de deslizar hacia abajo provoca el registro nuevo de la cuenta mostrada actualmente.
  • Al mantener pulsada la cuenta que se muestra actualmente, se habilita o deshabilita el registro de la cuenta.
  • El gesto de deslizar hacia la izquierda o hacia la derecha alterna entre las cuentas.
  • Se puede volver a seleccionar la persona con la que se llamó anteriormente tocando el icono de llamada cuando el campo Destinatario esté vacío.
  • Las parejas de llamadas y mensajes se pueden agregar a los contactos mediante toques prolongados.
  • Los toques prolongados también se pueden utilizar para eliminar llamadas, chats, mensajes y contactos.
  • Se puede utilizar el toque o toque prolongado del icono de contacto para instalar o desinstalar la imagen del avatar.
  • Mantener pulsado un códice de audio permite activarlo o desactivarlo.
  • Consulte Wiki para obtener más información.

Directiva de privacidad

La directiva de privacidad está disponible aquí .


Código Fuente

El código fuente está disponible en GitHub , donde también se pueden reportar problemas.


Traducciones de Idioma

Las traducciones de idioma son gestionadas por baresip en el proyecto Weblate.


Licencias

  • Cláusula BSD-3 excepto lo siguiente:
  • Codecs AMR y seguridad TLS de Apache 2.0
  • AGPLv4 Cifrado de medios ZRTP
  • GNU LGPL 2.1 codec Codec2
  • Libre G.722 codec
  • GNU GPLv3 Codec G.729
]]>
Cuenta Nombre para mostrar Nombre (si lo hay) utilizado en el URI de origen de las solicitudes salientes. Nombre de usuario de autenticación Nombre de usuario de autenticación si se requiere la autenticación de las solicitudes SIP. El valor por defecto es el nombre de usuario de la cuenta. Contraseña de autenticación Contraseña de autenticación de hasta 64 caracteres. Si se proporciona el nombre de usuario, pero no la contraseña, esta se le pedirá cuando inicie baresip. Proxies salientes URI SIP de uno o dos proxies que deben utilizarse al enviar las solicitudes. Si se dan dos, las solicitudes de REGISTRO se envían a ambos y las demás solicitudes se envían a uno que responda. Si no se indica ningún proxy de salida, las solicitudes se envían basándose en la búsqueda del registro DNS NAPTR/SRV/A de la parte de host del URI del destinatario. Si la parte del host del URI SIP es una dirección IPv6, la dirección debe escribirse entre corchetes []. \nEjemplos: \n - sip:ejemplo.com:5061;transporte=tls \n • sip:[2001:67c:223:777::10];transport=tcp \n • sip:192.168.43.50:443;transport=wss SIP URI del servidor proxy SIP URI de otro servidor proxy Registrar Si está marcado, el registro está habilitado y las solicitudes de REGISTRO se envían en el intervalo especificado por Intervalo de registro. Códecs de audio Media NAT Transversal Selecciona el protocolo transversal de NAT media (si lo hay). Las posibles opciones son STUN (Utilidades de recorrido de sesión para NAT, RFC 5389) e ICE (conectividad interactiva Establecimiento, RFC 5245). Servidor STUN/TURN Un URI de servidor STUN/TURN de la forma scheme:host[:port][\?transport=udp|tcp], donde scheme es \'stun\', \'stuns\', \'turn\', o \'turns\'. El servidor STUN predeterminado de fábrica para los protocolos STUN e ICE es \'stun:stun.l.google.com:19302\' que apunta al servidor STUN público de Google. No hay servidor TURN por defecto. Cifrado de medios Selecciona el protocolo de cifrado de transporte de medios (si lo hay). \n • ZRTP (recomendado) significa que la negociación de cifrado de medios de extremo a extremo de ZRTP se intenta después que la llamada ha sido establecida. \n • DTLS-SRTPF significa que UDP / TLS / RTP / SAVPF se ofrece en llamadas salientes y que RTP / SAVP, RTP / SAVPF, UDP / TLS / RTP / SAVP o UDP / TLS / RTP / SAVPF se usa si se ofrece en la llamada entrante. \n • SRTP-MANDF significa que RTP / SAVPF se ofrece en llamadas salientes y se requiere en llamadas entrantes. \n • SRTP-MAND significa que RTP / SAVP se ofrece en llamadas salientes y se requiere en llamadas entrantes. \n • SRTP significa que RTP / AVP se ofrece en llamadas salientes y que se utiliza RTP / SAVP o RTP / SAVPF si se ofrece en llamada entrante. Modo de contestación Selecciona cómo se contestan las llamadas entrantes. Manual Automático URI de correo de voz URI de SIP para comprobar los mensajes de correo de voz. Si se deja vacío, no se suscribirá a los mensajes de correo de voz (indicaciones de mensaje en espera). Cuenta predeterminada Si está marcada, esta cuenta se selecciona cuando se inicia baresip. Cuentas usuario@dominio[:puerto][;transport=udp|tcp|tls] «%1$s» no válido Ya existe la cuenta «%1$s». Error al asignar la cuenta nueva. Contraseña de cifrado Contraseña de descifrado ¿Quiere eliminar la cuenta «%1$s»\? Solicitud de transferencia Historial de llamadas Llamada llamadas llamar ¿Quiere añadir a «%1$s» a los contactos o eliminar %2$s del historial de llamadas\? ¿Quiere eliminar «%1$s» %2$s del historial de llamadas\? Desactivar Activar ¿Quiere eliminar el historial de llamadas de la cuenta «%1$s»\? Chatear con %1$s Mensaje nuevo ¿Quiere eliminar el mensaje o añadir el par «%1$s» a los contactos\? ¿Quiere eliminar el mensaje\? Añadir contacto Envío de mensaje fallido Ha fallado Historial de chat Hoy Usted Nuevo compañero de chat ¿Quieres eliminar el chat con un compañero? \'%1$s\' o agregar pares a los contactos? ¿Quiere eliminar el chat con «%1$s»\? ¿Quiere eliminar el historial de chat de la cuenta «%1$s»\? Configuración Comenzar automáticamente Si está marcado, baresip se inicia automáticamente tras arrancar el dispositivo o tras estár instalada una versión nueva de baresip. El inicio está retardado hasta que el dispositivo sea desbloqueado. Dirección de escucha Dirección IP y puerto de formulario \'address:port\' en el que escucha baresip para solicitudes SIP entrantes. Si la dirección IP es una dirección IPv6, debe escribirse dentro soportes []. La dirección IPv4 0.0.0.0 o la dirección IPv6 [::] hace que la escucha de baresip sea Direcciones disponibles. Si se deja vacío (predeterminado de fábrica), baresip escucha en un puerto aleatorio en todas las direcciones disponibles. Dirección de escucha no válida Servidores DNS Lista separada por comas de direcciones de servidores DNS. Si no se da, Las direcciones del servidor DNS se obtienen dinámicamente del sistema. Cada dirección DNS es de forma \'ip:port\' o \'ip\'. Si se omite el puerto, el valor predeterminado es 53. Si ip es una dirección IPv6 y también se da puerto, IP debe estar escrito entre corchetes []. Como ejemplo, lista \'8.8.8.8:53,[2001:4860:4860::8888]:53\' apunta a direcciones IPv4 e IPv6 de servidores DNS públicos de Google. Servidores DNS no válidos Error al establecer servidores DNS Archivo de certificado TLS Si se marca, se ha cargado o se cargará un archivo que contiene el certificado TLS y la clave privada de esta instancia de baresip. En las versiones Android 9, se carga un archivo llamado \'cert.pem\' desde la carpeta Download. Por razones de seguridad, elimine el archivo después de cargarlo. Archivo de CA de TLS Si está marcada, se ha cargado o se cargará un archivo que contiene certificados TLS de dichas Autoridades de Certificación que no están incluidas en el SO Android. En las versión de Android 9, se carga un archivo llamado \'ca_certs.crt\' desde la carpeta Download. Tasa de bits de Opus Velocidad de bits máxima promedio utilizada por la transmisión de audio Opus. Los valores válidos son 6000-510000. El valor predeterminado de fábrica es 28000. Pérdida de paquetes de Opus esperada Porcentaje esperado de la pérdida de los paquetes del flujo de audio Opus, de 0 a 100. El valor predeterminado de fábrica es 1. El valor 0 también desactiva la corrección de errores hacia delante (FEC) de Opus. El valor predeterminado de fábrica es 1. El valor 0 también desactiva Corrección de errores de reenvío de Opus (FEC). Tasa de bits de Opus no válida Porcentaje de pérdida de paquetes opus no válido Volumen de llamada predeterminado Si está configurado, el volumen de audio predeterminado de la llamada en escala 1–10. Depurar Si se marca, proporciona mensajes de registro de nivel de depuración e información a Logcat. Restablecer los valores de fábrica Si está marcado, la configuración se restablece a los valores predeterminados de fábrica. Fallo al leer el archivo \'cert.pem\'. Error al leer el archivo \'ca_certs.crt\'. Contacto nuevo Nombre El nombre de contacto «%1$s» no es válido Ya existe el contacto «%1$s». Contactos ¿Quieres llamar o enviar un mensaje a \'%1$s\'? Enviar mensaje ¿Quiere eliminar el contacto «%1$s»\? Alerta Información Aviso Cancelar De acuerdo No Aceptar Denegar Añadir Eliminar Editar Status Error Copia de respaldo Restaurar Acerca de Reiniciar Salir Llamar a … Llamada de … DTMF Información de llamada Duración: %1$d (segs) Códecs Velocidad actual: %1$s (Kbits/s) Mensajes de correo de voz Tiene un mensaje nuevo mensajes nuevos un mensaje antiguo mensajes antiguos y No tiene mensajes Escuchar Ya tiene una llamada activa. Baresip no se ha podido iniciar. Esto puede deberse a un valor de configuración no válido. Compruebe la dirección de escucha, el archivo de certificado TLS y el archivo TLS CA. Luego reinicie baresip. Registro de \`%1$s\` ha fallado. Verificar la solicitud ¿Quieres verificar SAS <%1$s>\? ¿Acepta transferir la llamada a «%1$s»\? Llamada fallida La llamada está cerrada ¡Esta llamada NO es segura! ¡Esta llamada es SEGURA, pero el par NO está verificado! ¡Esta llamada es SEGURA y el compañero está VERIFICADO! ¿Quieres desverificar al compañero? No verificar Los datos de la aplicación (excluyendo grabaciones) respaldados en el archivo \'%1$s\'. En las versión de Android 9, el archivo está en la carpeta Descargas. Ha fallado la copia de seguridad de los datos de la aplicación en el archivo \'%1$s\'. Compruebe Apps → baresip → Permisos → Almacenamiento. Se restauraron los datos de la aplicación. Baresip necesita reiniciarse. ¿Quiere reiniciar ahora\? No se han podido restaurar los datos de la aplicación. Compruebe que ha dado la contraseña correcta y que el archivo de copia de seguridad es de esta aplicación. En las versión de Android 9, compruebe también Aplicaciones → baresip → Permisos → Almacenamiento y que el archivo \'%1$s\' existe en la carpeta Download. Es necesario reiniciar Baresip para que surta efecto la configuración nueva. ¿Quiere reiniciar ahora\? Módulos de audio Las cuentas pueden utiilzar códecs de audio provistos por los módulos comprobados. Falló la carga del módulo. baresip necesita permiso de \"Micrófono\" para las llamadas de voz. Contraseña incorrecta%1$s Nombre de usuario no válido \'%1$s\' ¡No admite cámaras de vídeo! Concede permiso a \"Cámara\" para realizar o responder videollamadas. Solicitud de reinicio No hay información disponible Error en la transferencia Transferir Destino de transferencia Transferencia de llamadas ¿Aceptar la recepción de vídeo de \'%1$s\'\? ¿Acepta el envío de video a \'%1$s\'\? ¿Aceptar el envío y la recepción de vídeo con \'%1$s\'\? Solicitud de vídeo Videollamada Confirmación Si está marcada, este contacto se añade a los contactos de Android. Restablecer ¿Estás seguro de que quieres restablecer los valores de fábrica\? Si está marcada y si Debug está marcada, los mensajes Logcat incluyen también la petición SIP y la traza de respuesta. Desmarcado automáticamente al iniciar baresip. Seguimiento SIP Tamaño de los cuadros de vídeo transmitidos (ancho x alto) Tamaño del fotograma de vídeo Forzar el tema de la pantalla oscura Tema oscuro Si está marcada, baresip verifica los certificados TLS del Agente de Usuario SIP y de los Servidores Proxy SIP cuando se utiliza el transporte TLS. Verificar los certificados del servidor Solicitud de transferencia de llamada a Llamada perdida desde URL SIP de cuenta nueva de formulario @[:][;transport=udp|tcp|tls]. Si se indica y no se indica el protocolo de transporte, el protocolo de transporte será por defecto udp. Si no se da y se da el protocolo de transporte, es por defecto 5060 o 5061 (TLS). Si no se indica ninguno de los dos y no se especifica ningún proxy de salida, el registrador de la cuenta (si lo hay) se determina únicamente a partir de la información DNS del dominio. URI SIP de cuenta nueva Solicitudes SIP INFO Eventos RTP en banda Selecciona cómo se envían los tonos DTMF 0-9, #, * y A-D. Modo DTMF Contraseña \'%1$s\' no válida Contraseña si lo requiere el servidor STUN/TURN Contraseña STUN/TURN Nombre de usuario inválido \'%1$s\' Nombre de usuario si lo requiere el servidor STUN / TURN Nombre de usuario STUN/TURN URI del servidor STUN/TURN inválido \'%1$s\' Codecs de vídeo URI del servidor proxy inválido \'%1$s\' Nombre de pantalla inválido \'%1$s\' Agente de usuario SIP basado en biblioteca Baresip con video‐llamadas

Juha Heinanen <jh@tutpro.com>

Versión %1$s


Consejos de uso

  • Comprobar que los valores por defecto en la configuración de baresip+ se ajustan a sus necesidades (Toque los títulos de los elementos para obtener ayuda).
  • Luego, en Cuentas, cree una o más cuentas (otra vez toque los títulos de los elementos para obtener ayuda).
  • El estado de registro de una cuenta se muestra con un punto de color: verde (registro Ha sido aprobado), amarillo (el registro está en curso), rojo (el registro falló), blanco (el registro está en curso no se ha activado).
  • Tocar el punto lleva directamente a la configuración de la cuenta.
  • Toque largo en iconos de barra bareship muestra información sobre los iconos.
  • El gesto de deslizar hacia abajo provoca el registro de nuevo de la cuenta que se muestra actualmente.
  • El toque largo en la cuenta mostrada actualmente activa o desactiva el registro de la cuenta.
  • El gesto de deslizar hacia la izquierda/derecha alterna entre las cuentas.
  • La llamada anterior puede ser re‐seleccionada tocando el icono de llamada cuando Callee está vacío.<
  • Las parejas de llamadas y mensajes se pueden agregar a contactos con toques largos.
  • Los toques, toques largos también se pueden usar para eliminar llamadas, charlas, mensajes y contactos.
  • El icono de contacto puede utilizarse para instalar/eliminar el avatar de la imagen.
  • Toque largo en un códice de sonido o vídeo puede ser utilizado para habilitar/inhabilitar el códice.
  • Consulte el Wiki para más información.

Asuntos conocidos

  • La vista en primera persona no se muestra correctamente cuando la transmisión de vídeo es de solo envío.

Normativa de privacidad

La normativa de privacidad está disponible aquí.


Código fuente

El código fuente está disponible en GitHub, También se pueden comunicar problemas.


Traducciones de idiomas

La traducciones de idiomas es gestionada por medio del proyecto Weblate.


Licencias

  • BSD-3-Clause excepto lo siguiente:
  • Los códices Apache 2.0 de AMR y seguridad TLS
  • Cifrado de medios ZRTP bajo AGPLv4
  • Códice Codec2 de GNU LGPL 2.1
  • Códice Free G.722
  • Códice GNU GPLv3 G.729
  • Códice H.264 y H.265 de GNU GPLv2
  • Códice AV1 de AOMedia
]]>
Acerca de baresip+ Intervalo de inscripción Intervalo de registro no válido \'%1$s\' Multiplexación RTCP Código del país Indica la frecuencia (en segundos) con la que baresip envía peticiones REGISTRO. Los valores válidos van de 60 a 3600. Si está marcada, los paquetes RTP y RTCP se multiplexan en un único puerto (RFC 5761). Apodo (si lo hay) utilizado para identificar esta cuenta dentro de la aplicación baresip. Apodo de la cuenta no válido \"%1$s El apodo \'%1$s\' ya existe Apodo Ambos Detalles de la llamada Interlocutores Dirección Duración Ajustes de sonido usuario@dominio o número de teléfono Imagen del perfil Proveedor de telefonía Parte del host SIP URI utilizada en las llamadas a números de teléfono. Por defecto es el dominio de la cuenta. Si no se indica, esta cuenta no puede utilizarse para llamar a números de teléfono. Código del país \"%1$s\" no válido Parte del host SIP URI \'%1$s\' no válida Elige si se utilizan contactos de Baresip, contactos de Android o ambos. Si se utilizan ambos y existe un contacto con el mismo nombre en ambos contactos, se elegirá el contacto bareip. Solicitud de autorización SIP o teléfono URI Llamada en espera No es posible restaurar la copia de seguridad sin el permiso de \"Almacenamiento\". ¡No hay conexion de red! La cuenta \'%1$s\' no tiene proveedor de telefonía Velocidad media: %1$s (Kbits/s) Perdidos Oscilación: %1$s (ms) Fundamentos de los permisos Oculto Asistió %1$d llamadas perdidas Optimizaciones de batería No podrá crear copias de seguridad sin el permiso \"Almacenamiento\". Anónimo Desconocido URI del teléfono o SIP no válido \'%1$s\' Paquetes baresip necesita el permiso «Micrófono» para las llamadas de voz, el permiso «Dispositivos cercanos» para la detección de micrófonos/altavoces Bluetooth, el permiso «Notificaciones» para publicar notificaciones, y permisos de «Almacén» en Android 9 para operaciones de Respaldo/Restauración. Llamadas perdidas Código de país E.164 de esta cuenta. Si la parte de usuario From URI de la llamada entrante o del mensaje contiene un número de teléfono que no empieza por el signo \"+\" y si falla la búsqueda de contacto, se antepone al número este prefijo de país y se vuelve a intentar la búsqueda de contacto. Si el número de teléfono comienza con un solo dígito \"0\", el dígito \"0\" se elimina antes de anteponer el prefijo al número. Tiempo Desviada por … No podrá utilizar esta aplicación sin el permiso de \"Notificaciones\". Desactive las optimizaciones de batería (recomendado) si desea reducir la probabilidad de que Android restrinja el acceso de baresip a la red o lo ponga en estado de espera. Si se eligen contactos de Android, pueden ser usados en llamadas y mensajes como referencias a SIP y tel URIs. baresip aplicación no almacena contactos de Android ni compartirlos con nadie. Para que los contactos de Android estén disponibles en baresip, Google requiere que aceptes su uso según se describe aquí y en la Política de privacidad de la aplicación. No se puede acceder a los contactos de Android sin el permiso \"Contactos\". ¡Enfoque de audio denegado! baresip+ necesita el permiso \"Micrófono\" para las llamadas de voz, el permiso \"Cámara\" para las videollamadas, el permiso \"Dispositivos cercanos\" para la detección de micrófonos/altavoces Bluetooth y el permiso \"Notificaciones\" para la publicación de notificaciones, y en Android 9 los permisos de \"Almacén\" para operaciones de Resguardo/Restauración. Seleccione la URI de destino La grabación puede activarse o desactivarse sólo cuando la llamada no está conectada Elige qué direcciones IP usará Baresip. Si se elige entre IPv4 o IPv6, Barsip usará una solamente. Si no se elige entre ninguna, Baresip ambas (IPv4 e IPv6). Familia de direcciones Retraso del audio Tiempo (en milisegundos) para esperar el audio del destinatario cuando se establece la llamada. Ajústalo a un valor más alto si pierdes el audio del destinatario al principio de la llamada. Retardo de audio no válido \'%1$s\'. Los valores válidos van de 100 a 3000. Llamada rechazada automáticamente de %1$s Aplicación de teléfono predeterminada La función de teléfono no está disponible Si está marcada, baresip es la aplicación de teléfono predeterminada. No verifiques si tu dispositivo puede necesitar manejar también llamadas o mensajes que no sean SIP. Redirección automática a \'%1$s\'\\ Solicitud de redirección ¿Aceptas la redirección de llamadas a \'%1$s\'\? Modo de redirección Selecciona si la solicitud de redirección de las llamadas se realiza automáticamente o si se solicita confirmación. Tono para el país Timbre de llamada, espera y tonos de ocupado de la llamada para un país Si está marcado, indica el soporte para las respuestas provisionales fiables (RFC 3262). Respuestas provisionales fiables Favoritos El inicio automático necesita el permiso mostrar en la parte superior. Error al restaurar los datos de la aplicación. Android versión 14 y superiores no permiten restaurar datos de los que se hizo una copia de seguridad antes de %1$s la versión %2$s. Altavoz del teléfono Si está marcada, el altavoz se enciende automáticamente al iniciar la llamada. RTP en la banda o imformación SIP \'%1$d\' Fps no válido(s) Fotogramas de vídeo por segundo (fps) Velocidad de fotogramas de vídeo que se ofrecerá durante el protocolo de enlace SDP. Los valores válidos son del 10 al 30. Requerimiento ¿Aceptas el requerimiento \'%1$s\'? Agente de usuario Valor del campo de encabezado de agente de usuario de solicitud/respuesta SIP personalizado Valor de campo de encabezado del agente de usuario no válido Multiplica el volumen del micrófono por este número decimal. El valor mínimo es 1.0 (predeterminado de fábrica) que desactiva la ganancia del micrófono. Los valores más altos pueden afectar negativamente a la calidad del audio. Ganancia del micrófono Valor de ganancia de micrófono no válido Comprobar origen Si está marcado, las solicitudes entrantes solo están concedidas desde las direcciones IP donde fueron enviadas las peticiones del registro. Bloque Desconocido Llamadas y mensajes de bloque desde parejas que no son encontradas en contactos. Teclado Numérico" Si se marcó, el teclado numérico se muestra cuando el campo «Llamada a …» está centrada." está llamando Auto‐rechazar llamadas bloqueadas desde %1$s Auto‐rechazar mensaje bloqueado desde %1$s Bloqueado Llamadas Bloqueadas Mensajes Bloqueados ¿Desea borrar solicitudes bloqueadas de «%1$s»? ¿Desea añadir pareja «%1$s» a contactos? Llamada respondida Llamada respondida donde sea Llamada perdida Llamada rechazada Reproduce registro … Guardar Registro ¿Desea guardar esta grabación? Grabación guardada ¿Desea eliminar esta llamada desde el historial? Protocolos de transporte Listado separada por comas de los protocolos de transporte de solicitud/respuesta SIP compatibles. Si se deja vacía, el valor predeterminado es \'udp,tcp,tls,ws,wss\', que incluye todos los protocolos de transporte compatibles. Incluya solo los que necesite. No se recomienda el uso de UDP por motivos de seguridad y otros. Listado no válidos del Protocolos de Transporte Sin permiso de lectura en Almacén Externo URI de Contacto único Si está marcado, está garantizada la URI de contacto para ser única. Necesita ser comprobado si hay más que una cuenta con el mismo URI SIP como parte de usuario, pero además mejora la protección de las cuentas desde ataques. Colores dinámicos Utilice colores dinámicos si habilitó un Ajuste de Androin Daltonía Utilizar registro amigable de estado de iconos para daltónicos Detección de proximidad"> Si está marcada, la detección de proximidad estará activa durante las llamadas. Tono de llamada Seleccionar tono de llamada Si está marcada, se muestra el contacto entre otros favoritos en la cima del listado de contactos. Búsqueda Detener Responder Guardar Llamada conectada Pareja no admite característica REPLACES ¡Sin Cancelación de Eco Acústico por hardware! Grabación de Llamada Si está activado, las llamadas entrantes y salientes serán grabadas. Las grabaciones pueden ser reproducidas en la página Detalles de Llamada. Micrófono Si está activado durante la llamada, el micrófono está enmudecido. Altavoz Si está activado, el sonido es reproducido por medio de dispositivo de altavoz. La llamada está sonando Parámetros personales Listado separador por punto y coma de parámetros de cuenta personal
================================================ FILE: app/src/main/res/values-fi/strings.xml ================================================ baresip sovelluksesta baresip+ sovelluksesta Baresip-kirjastoon perustuva äänipuhelut ja pikaviestit mahdollistava SIP-sovellus

Juha Heinanen <jh@tutpro.com>

Versio %1$s


Käyttövihjeitä

  • Tarkista, että baresip-sovelluksen Asetukset vastaavat tarpeitasi. Kunkin otsikon kosketus tarjoaa apua.
  • Sen jälkeen luo yksi tai useampi tili. Jälleen kunkin otsikon kosketus tarjoaa apua.
  • Tilin rekisteröintitila kerrotaan värillisellä pisteellä: vihreä (rekisteröinti onnistui), keltainen (rekisteröinti on meneillään), punainen (rekisteröinti epäonnistui), valkoinen (rekisteröintiä ei ole aktivoitu).
  • Pisteen kosketus johtaa suoraan tilin asetuksiin.
  • Pitkä kosketus baresip-palkin ikoneihin näyttää tietoa ikoneista.
  • Pyyhkäisy alas aikaansaa parhaillaan näkyvissä olevan tilin uudelleen rekisteröinnin.
  • Parhaillaan näkyvissä olevan tilin pitkä kosketus aktivoi tai passivoi tilin rekisteröinnin.
  • Pyyhkäisy vasemmalle/oikealle vaihtaa näkyvissä olevaa tiliä.
  • Voit valita edellisen puhelun uudelleen koskettamalla virheää luuria silloin, kun puhelun kohde on tyhjä.
  • Voit lisätä puheluiden ja viestien kohteet yhteystietoihin pitkällä kosketuksella.
  • Pitkillä kosketuksilla voit myös poistaa puheluita, viestiketjuja, viestejä ja yhteystietoja.
  • Voit lisätä/poistaa yhteystiedon avatar-kuvan koskettamalla yhteystiedon ikonia lyhyesti/pitkästi.
  • Audio-koodekin saa pitkällä kosketuksella käytöön/pois käytöstä.
  • Katso lisätietoja Wiki-sivulta.

Tietosuoja

Sovelluksen tietosuojakäytäntö on luettavissa täältä.


Lähdekoodi

Lähdekoodi on saatavilla GitHub:ssa, missä voi myös raportoida virheistä.


Kielikäännökset

Kielikäännöksiä varten on olemassa baresip Weblate projekti.


Lisenssit

  • BSD-3-Clause paitsi seuraavat:
  • Apache 2.0 AMR koodausmenetelmät ja TLS turvallisuus
  • AGPLv4 ZRTP median salausmenetelmä
  • GNU LGPL 2.1 Codec2 koodausmenetelmä
  • Ilmainen G.722 koodausmenetelmä
  • GNU GPLv3 G.729 koodausmenetelmä
]]>
Baresip-kirjastoon perustuva videopuhelut ja pikaviestit mahdollistava SIP-sovellus

Juha Heinanen <jh@tutpro.com>

Versio %1$s


Käyttövihjeitä

  • Tarkista, että baresip+-sovelluksen Asetukset vastaavat tarpeitasi. Kunkin otsikon kosketus tarjoaa apua.
  • Sen jälkeen luo yksi tai useampi tili. Jälleen kunkin otsikon kosketus tarjoaa apua.
  • Tilin rekisteröintitila kerrotaan värillisellä ympyrällä: vihreä (rekisteröinti onnistui), keltainen (rekisteröinti on meneillään), punainen (rekisteröinti epäonnistui), valkoinen (rekisteröintiä ei ole aktivoitu).
  • Ympyrän kosketus johtaa suoraan tilin asetuksiin.
  • Pitkä kosketus baresip-palkin ikoneihin näyttää tietoa ikoneista.
  • Pyyhkäisy alas aikaansaa parhaillaan näkyvissä olevan tilin uudelleen rekisteröinnin.
  • Parhaillaan näkyvissä olevan tilin pitkä kosketus aktivoi tai passivoi tilin rekisteröinnin.
  • Pyyhkäisyllä vasemmalle/oikealle voit vaihtaa näkyvissä olevaa tiliä.
  • Voit valita edellisen puhelun uudelleen koskettamalla virheää luuria silloin, kun puhelun kohde on tyhjä.
  • Voit lisätä puheluiden ja viestien kohteet yhteystietoihin pitkällä kosketuksella.
  • Pitkillä kosketuksilla voit myös poistaa puheluita, viestiketjuja, viestejä ja yhteystietoja.
  • Voit lisätä/poistaa yhteystiedon avatar-kuvan koskettamalla yhteystiedon ikonia lyhyesti/pitkästi.
  • Audio- ja video-koodekin saa pitkällä kosketuksella käytöön/pois käytöstä.
  • Katso lisätietoja Wiki-sivulta.

Tunnetut ongelmat

  • Oman videokuvan näyttö ei toimi, jos video on vain lähetyssuuntainen.

Tietosuoja

Sovelluksen tietosuojakäytäntö on luettavissa täältä.

Lähdekoodi

Lähdekoodi on saatavilla GitHub:ssa, missä voi myös raportoida virheistä.


Kielikäännökset

Kielikäännöksiä varten on olemassa baresip Weblate projekti.


Lisenssit

  • BSD-3-Clause paitsi seuraavat:
  • Apache 2.0 AMR koodausmenetelmät ja TLS turvallisuus
  • AGPLv4 ZRTP median salausmenetelmä
  • GNU LGPL 2.1 Codec2 koodausmenetelmä
  • Ilmainen G.722 koodausmenetelmä
  • GNU GPLv3 G.729 koodausmenetelmä
  • GNU GPLv2 H.264 ja H.265 koodausmenetelmät
  • AOMedia AV1 koodausmenetelmä
]]>
Tili Lempinimi (jos annettu) millä tämä tili identifioidaan baresip sovelluksessa. Lempinimi Virheellinen tilin lempinimi \'%1$s\' Lempinimi \'%1$s\' on jo olemassa Tilin käyttäjän nimi Tilin käyttäjän nimi, joka esiintyy SIP-sanomien From URI:ssa (vapaaehtoinen). Virheellinen tilin käyttäjän nimi \'%1$s\' Käyttäjätunnus Todentamiseen käytettävä käyttäjätunnus, jos välityspalvelin vaatii sellaisen. Oletusarvo on tilin käyttäjätunnus. Virheellinen todennuskäyttäjänimi \'%1$s\' Todennuksen salasana Todentamiseen käytettävä salasana, jonka pituus on enintään 64 ASCII merkkiä. Jos käyttäjätunnus on annettu, mutta salasanaa ei ole annettu, se kysytään, kun baresip käynnistetään. Virheellinen todennussalasana \'%1$s\' Välityspalvelimet Yhden tai kahden välityspalvelimen SIP URI, joille SIP-sanomat lähetetään. Jos välityspalvelimia on annettu kaksi, REGISTER-sanomat yritetään lähettää molemmille välityspalvelimille ja muut sanomat yhdelle toiminnassa olevalle välityspalvelimelle. Jos välityspalvelimia ei ole annettu, SIP-sanomat lähetetään palvelimelle, mikä selviää kohteen domainille tehtävien NAPTR/SRV/Animipalvelukyselyiden perusteella. Jos välityspalvelimen osoite SIP URI:ssa on IPv6-osoite, osoite pitää kirjoittaa sulkujen [] sisään. \nEsimerkkejä: \n • sip:example.com:5061;transport=tls \n • sip:[2001:67c:223:777::10];transport=tcp \n • sip:192.168.43.50:443;transport=wss Välityspalvelimen SIP URI Toisen välityspalvelimen SIP URI Virheellinen välityspalvelimen URI \'%1$s\' Rekisteröi Jos merkitty, rekisteröinti on aktiivinen ja REGISTER-sanomat lähetetään Rekisteröintitaajuus-asetuksen mukaisesti. Rekisteröintitaajuus Kertoo kuinka usein baresip lähettää REGISTER-sanomia. Arvon on oltava välillä 60-3600 (sekunttia). Virheellinen Rekisteröintitaajuus \'%1$s\' Tarkista lähdeosoite Merkitty, sisään tulevat SIP-sanomat sallitaan vain niistä lähdeosoitteista, joihin REGISTER-sanomat lähetettiin. Media NAT hallinta Valitsee media NAT hallintaprotokollan (vapaaehtoinen). Vaihtoehtoja ovat STUN (Session Traversal Utilities for NAT, RFC 5389) ja ICE (Interactive Connectivity Establishment, RFC 5245). STUN/TURN-palvelin STUN/TURN-palvelimen muotoa \'kaava:palvelin[:portti][\?transport=udp|tcp]\' oleva URI, missä kaava on \'stun\', \'stuns\', \'turn\' tai \'turns\'. Oletus STUN-palvelin STUN- ja ICE-protokollille on \'stun:stun.l.google.com:19302\', joka osoittaa Google:n julkiseen STUN-palvelimeen. TURN-palvelimella ei ole oletusarvoa. Virheellinen STUN/TURN-palvelimen URI \'%1$s\' STUN/TURN-käyttäjätunnus Käyttäjätunnus jos STUN/TURN-palvelin vaatii sellaisen Virheellinen käyttäjänimi %1$s STUN/TURN-salasana Salasana jos STUN/TURN-palvelin vaatii sellaisen Virheellinen salasana \'%1$s\' Median salaus Valitsee median salausprotokollan (vapaaehtoinen). \n • ZRTP (suositeltu) tarkoittaa, että ZRTP-salausta yritetään neuvotella sen jälkeen, kun puhelu on alkanut. \n • DTLS-SRTPF tarkoittaa, että UDP/TLS/RTP/SAVPF-protokollaa tarjotaan lähteviin puheluihin ja että RTP/SAVP-, RTP/SAVPF-, UDP/TLS/RTP/SAVP- tai UDP/TLS/RTP/SAVPF-protokollaa käytetään, jos sellaista tarjotaan tulevassa puhelussa. \n • SRTP-MANDF tarkoittaa, että RTP/SAVPF-protokollaa tarjotaan lähtevissä puheluissa ja että se vaaditaan tulevissa puheluissa. \n • SRTP-MAND takoittaa, että RTP/SAVP-protokollaa tarjotaan lähteviin puheluihin ja että se vaaditaan tulevissa puheluissa. \n • SRTP tarkoittaa, että RTP/AVP-protokollaa tarjotaan lähteviin puheluihin ja että RTP/SAVP- tai RTP/SAVPF-protokollaa käytetään, jos sellaista tarjotaan tulevissa puheluissa. RTCP-multipleksaus Jos merkitty, RTP- and RTCP-paketit multipleksataan samaan porttiin (RFC 5761). Luotettavat alustavat vastaukset Jos merkitty, kutsussa ilmoitetaan luotettavien alustavien vastausten tukemisesta (RFC 3262). DTMF-moodi Valitsee tavan, miten DTMF-merkit 0–9, #, * ja A-D lähetetään. RTP-tapahtuma SIP INFO -pyyntö RTP tai SIP INFO Vastaustapa Valitsee tulevien puheluiden vastaustavan. Manuaalinen Automaattinen Estä tuntemattomat Estä puhelut ja viestit, joiden lähdettä ei löydy yhteystiedoista. Uudelleenohjaustapa Valitsee toteutetaanko puhelun uudelleenohjauspyyntö automaattisesti vai kysytäänkö vahvistusta. Puhepostin URI SIP URI, jota käytetään puhepostiviestien kuunteluun. Jos URI:a ei ole annettu, tietoa mahdollista puhepostiviesteistä (Message Waiting Indications) ei tilata. Maakoodi Tämän tilin E.164-maakoodi. Jos tulevan puhelun tai viestin From URI:n käyttäjäosa sisältää puhelinnumeron, joka ei ala \'+\' merkillä, ja jos sitä ei löydy yhteystiedoista, niin tämä maakoodi lisätään numeron eteen ja etsintä tehdään uudelleen. Jos puhelinnumero alkaa yhdellä numerolla \'0\', niin numero \'0\' poistetaan ennen maakoodin lisäämistä. Virheellinen maakoodi \'%1$s\' Puhelinpalvelun tarjoaja SIP URI:n domain-osa, jota käytetään soitettaessa puhelinnumeroihin. Tehdasasetus on tilin domain-osa. Jos tyhjä, niin tätä tiliä ei voi käyttää soitettaessa puhelinnumeroihin. Numeronäppäimistö" Jos valittu, \"Puhelu ulos …\" kentässä käytetään numeronäppäimistöä." Virheellinen SIP URI:n domain-osa \'%1$s\' Oletustili Jos merkitty, niin tämä tili on valittuna, kun baresip käynnistetään. Räätälöidyt parametrit Puolipisteellä toisistaan eroteltu lista räätälöityjä tilin parametrejä Tilit Uuden tilin SIP URI Uuden tilin SIP URI muotoa <käyttäjä>@<domain>[:<portti>][;transport=udp|tcp|tls]. Jos <portti> on annettu, mutta protokollaa ei ole annettu, protokolla on udp. Jos <portti> ei ole annettu, mutta protokolla on annettu, portti on joko 5060 tai 5061 (tls). Jos kumpaakaan ei ole annettu eikä välityspalvelinta ole määritelty, tilin mahdollinen rekisteröintipalvelin päätellään pelkästään domainin DNS-informaation perusteella. Virheellinen käyttäjä@domain[:portti][;transport=udp|tcp|tls] \'%1$s\' Tili \'%1$s\' on jo olemassa. Uuden tilin luonti epäonnistui. Tallenna salasanalla Palauta salasanalla Haluatko poistaa tilin \'%1$s\'\? soittaa Vastaamaton puhelu soittajalta Vastaamattomia puheluita %1$d vastaamatonta puhelua Puhelun siirtopyyntö kohteeseen Automaattisesti hylätty puhelu soittajalta \`%1$s\` Estetty puhelu hylätty soittajalta \`%1$s\` Estetty viesti hylätty lähettäjältä \`%1$s\` Estetyt Estetyt puhelut Estetyt viestit Haluatko poistaa tilin \'%1$s\' estetyt pyynnöt\? Haluatko lisätä osoitteen \'%1$s\' yhteystietoihin\? Puheluhistoria Soita puhelut puhelun Suunta Kumppani Aika Kesto Haluatko luoda uuden yhteystiedon \'%1$s\' tai poistaa %2$s puheluhistoriasta\? Haluatko poistaa \'%1$s\' %2$s puheluhistoriasta\? Poista käytöstä Ota käyttöön Haluatko tyhjentää tilin \'%1$s\' puheluhistorian\? Puheluun vastattu Puheluun vastattu muualla Puheluun ei vastattu Puhelu hylätty Tallenteen kuuntelu … Talleta tallenne Haluatko tallentaa tämän tallenteen? Tallenne tallennettu Haluatko poistaa tämän puhelun historiata? Viestiketju %1$s Uusi viesti Haluatko poistaa viestin tai luoda uuden yhteystiedon \'%1$s\'\? Haluatko poistaa viestin\? Lisää yhteystieto Viestin lähetys epäonnistui Epäonnistui Viestiketjut Tänään Sinä Uusi viestin kohde Haluatko poistaa viestiketjun tai luoda uuden yhteystiedon \'%1$s\'\? Haluatko poistaa viestiketjun \'%1$s\'\? Haluatko tyhjentää tilin \'%1$s\' viestihistorian\? Audion koodausmenetelmät Videon koodausmenetelmät Asetukset Käynnistä automaattisesti Jos merkitty, baresip käynnistyy automaattisesti, kun laite käynnistyy tai kun uusi baresip-versio on asennettu. Käynnistys tapahtuu, kun laite on avattu. Automaattinen käynnistys vaatii "Näytä päällimmäisenä" käyttöoikeuden. Akun käytön optimointi Ota akun käytön optimointi pois päältä (suositeltu), jos haluat vähentää todennäköisyyttä, että Android rajoittaa baresip-sovelluksen toimintaa ja pääsyä verkkoon. Oletuspuhelinsovellus Puhelinrooli ei ole saatavana Jos merkity, baresip on oletuspuhelinsovellus. Älä merkitse, jos laitteesi täytyy hallita myös muita kuin SIP-puheluita tai -viestejä. Kuunteluosoite IP-osoite ja portti muotoa \'osoite:portti\', missä baresip kuuntelee sisään tulevia SIP-sanomia. Jos IP-osoite on IPv6-osoite, se täytyy kirjoittaa sulkujen [] sisään. IPv4-osoite 0.0.0.0 tai IPv6-osoite [::] tarkoittaa kaikkia käytössä olevia osoitteita. Oletusarvo on tyhjä, jolloin baresip kuuntelee satunnaisesti valittua porttia kaikilla osoitteilla. Virheellinen kuunteluosoite Osoiteperhe Valitsee, mitä IP-osoitteita baresip käyttää. Jos IPv4 tai IPv6 on valittu, baresip käyttää ainoastaan IPv4- tai IPv6-osoitteita. Jos kumpaakaan ei ole valittu, baresip käyttää sekä IPv4- että IPv6-osoitteita. Kuljetusprotokollat Pilkulla toisistaan erotettu lista tuettuja SIP-sanomien kuljetusprotokollia. Jos jätetään tyhjäksi, oletusarvo on \'udp,tcp,tls,ws,wss\', joka sisältää kaikki tuetut kuljetusprotokollat. Listaa vain ne joita tarvitset. UDP-protokollan käyttöä ei suositella turvallisuus- ja muista syistä. Virheellinen kuljetusprotokollalistas DNS-palvelimet Pilkulla toisistaan erotettu luettelo DNS-palvelijoiden osoitteita. Jos jätetään antamatta (oletus), osoitteet hankitaan dynaamisesti järjestelmästä. Kukin osoite on muotoa \'ip:portti\' tai \'ip\', missä ip on IPv4 tai IPv6 osoite. Jos ip on IPv6-osoite ja myös portti annetaan, pitää osoite kirjoittaa sulkujen [] sisään. Esimerkiksi luettelo \'8.8.8.8:53,[2001:4860:4860::8888]:53\' osoittaa Googlen julkisiin DNS-palvelijoihin. Virheelliset DNS-palvelimet DNS-palvelinten asetus epäonnistui TLS-sertifikaattitiedosto Jos merkitty, tiedosto joka sisältää tämän baresip-sovelluksen julkisen ja yksityisen TLS-sertifikaatin, on joko jo ladattu tai tullaan lataamaan. Android versiossa 9 tiedosto nimeltään \'cert.pem\' ladataan Download-kansiosta. Turvallisuusyistä tuhoa tiedosto heti lataamisen jälkeen. Tarkista palvelinten sertifikaatit Jos merkitty, baresip tarkistaa SIP-palvelinten sertifikaatit, kun TLS-tiedonsiirto on käytössä. TLS CA-tiedosto Jos merkitty, tiedosto on joko jo ladattu tai tullaan lataamaan, joka sisältää sellaisten sertifikaattiauktoriteettien (CA) julkiset sertifikaatit, jotka eivät sisälly Android-käyttöjärjestelmään. Android versiossa 9 tiedosto nimeltään \'ca_certs.crt\' ladataan Download-kansiosta. Ei ulkoisen muistin lukuoikeutta Uniikki Contact URI Jos merkitty, Contact URI:n taataan olevan uniikki. Merkittävä, jos on useammalla tilillä on same SIP URI:n käyttäjäosa, mutta myös suojaa tilejä hyökkäyksiltä. Audio-asetukset Kaiutin Jos merkitty, kaiutin kytketään sutomaattisesti päälle kun puhelu alkaa. Audio-modulit Merkittyjen modulien tarjoamat audio-koodekit ovat tilien käytettävissä. Modulin lataaminen epäonnistui. Mikrofonin äänikerroin Kertoo mikrofonin äänen voimakkuuden tällä desimaaliluvulla. Minimikerroin on 1.0 (tehdasasetus), mikä ei muuta mikrofonin äänen voimakkuutta. Suuremmat arvot voivat vaikuttaa negatiivisesti äänen laatuun. Virheellinen mikrofonin äänikerroin Opus-koodekin bittinopeus Opus-koodekin käyttämä keskimääräinen enimmäisnopeus. Mahdollisia arvoja ovat 6000-510000. Oletusarvo on 28000. Odotettu opus-pakettihäviö Odotettu opus audio virran pakettihäviö prosentteina. Mahdolliset arvot ovat 0-100. Oletusarvo on 1. Arvo 0 poistaa käytöstä ennakoivan virheenkorjauksen. Virheellinen Opus-koodekin bittinopeus Virheellinen Opus-koodekin odotettu pakettihäviö Audioviive Audion odotusviive (millisekunneissa) soitetun puhelun alkaessa. Aseta korkeampi arvo, jos et kuule vastaajan ääntä heti, kun puhelu alkaa. Virheellinen audioviive \'%1$s\'. Sallittu arvo on välillä 100–3000. Oletus äänen voimakkuus Jos valittu, puhelun äänen voimakkuus asteikolla 1–10. Puheluäänien maa Soitto-, odotus- ja varattuäänien maa Tumma teema Käytä aina tummaa näyttöteemaa Dynaamiset värit Käytä dynaamisia värejä, jos ne on otettu käyttöön Android-asetuksissa Värisokea Käytä värisokealle ystävällisiä rekisteröintitilaikoineita Läheisyyden tunnistus Jos merkitty, läheisyyden tunnistus on aktiivinen puhelun aikana. Videon kehyskoko Lähetettävän videon kehyskoko (leveys x korkeus) Videokehysten lähetystaajuus Videokehysten lähetystaajuus per sekuntti, jota tarjotaan SDP-neuvottelun aikana. Arvon on oltava välillä 10-30. Virheellinen videokehysten lähetystaajuus \'%1$d\' Soittoääni Valitse soittoääni User Agent Räätälöity SIP sanoman User-Agent otsikkokentän arvo Virheellinen User-Agent otsikkokentän arvo Valitsee, käytetäänkö Baresip-yhteystietoja, Android-yhteystietoja vai molempia. Jos molempia käytetään ja molemmissa yhteystiedoissa on samanniminen yhteystieto, valitaan baresip-yhteystieto. Molemmat Lokiviestit Jos merkitty, baresip tuottaa debug- ja info-tason Logcat-viestejä. SIP-sanomien jäljitys Jos merkitty ja jos Lokiviestit on merkitty, Logcat-viestit sisältävät myös lähetetyt ja vastaanotetut SIP-sanomat. On aina merkitsemättä sovelluksen käynnistyessä. Palauta oletusasetukset Jos merkitty, oletusasetukset palautetaan, kun baresip seuraavan kerran käynnistetään. Oletko varma, että haluat palauttaa oletusasetukset\? Palauta Tiedoston \'cert.pem\' luku epäonnistui. Tiedoston \'ca_certs.crt\' epäonnistui. baresip täytyy käynnistää uudelleen, jotta saat uudet asetukset käyttöön. Käynnistä nyt\? Suostumuspyyntö Jos Androidin yhteystiedot on valittu, voit käyttää niitä puheluissa ja viesteissä SIP ja tel URI:en ohella. Baresip ei talleta Androidin yhteystietoja eikä jaa niitä kenellekään. Jotta Baresip saisi käyttöönsä Androidin yhteystiedot, Google vaatii, että annat siihen suostumuksen hyväksymällä Baresip-sovelluksen yksityisyyskäytännöt. Uusi yhteystieto Nimi SIP tai tel URI käyttäjä@domain tai puhelinnumero Suosikki Jos ruksattu, kontakti näytetään muiden suosikkien kanssa yhteystietolistan alussa. Virheellinen yhteystiedon nimi \'%1$s\' Yhteystieto \'%1$s\' on jo olemassa. Jos merkitty, tämä yhteystieto lisätään Androidin yhteystietoihin. Profiilikuva Yhteystiedot Haluatko soittaa tai lähettää viestin ositteeseen \'%1$s\'\? Lähetä viesti Haluatko poistaa yhteystiedon \'%1$s\'\? Etsi Varoitus Tieto Huomio Peruuta Lopeta Vastaa Talleta OK Kyllä Ei Hyväksy Estä Lisää Poista Muokkaa Tila Virhe Vahvistus Anonyymi Tuntematon Virheellinen SIP tai puh. URI \'%1$s\' Tallenna Palauta Tietoja Käynnistä uudelleen Lopeta Puhelu ulos … Puhelu sisään … Siirtäjä … Tilille \'%1$s\' ei ole konfiguroitu puhelinpalvelun tarjoajaa Videopuhelu Videopyyntö Salli video videon lähetys ja vastaanotto puhelussa \'%1$s\'\? Salli video videon lähetys puhelussa \'%1$s\'\? Salli video videon vastaanotto puhelussa \'%1$s\'\? Puhelu soi Puhelu on pidossa Puhelu on yhdistetty Tallennus voidaan asettaa päälle tai pois vain silloin, kun puhelu ei ole yhdistetty Puhelun siirto Sokeasti Osallistu Siirron kohde Valitse kohteen URI Siirto Siirto epäonnistui Toinen osapuoli ei tue REPLACES ominaisuutta DTMF Puhelutiedot Ei saatavilla Kesto: %1$d (sek) Koodekit Nykyinen nopeus: %1$s (Kbit/s) Keskinopeus: %1$s (Kbit/s) Paketit Hukkunut Vaihtelu: %1$s (ms) Puhepostiviestit Sinulla on yksi uusi viesti uutta viestiä yksi vanha viesti vanhaa viestiä ja Sinulla ei ole viestejä Kuuntele Sinulla on jo puhelu käynnissä. Sovelluksen käynnistäminen epäonnistui. Tämä voi johtua virheellisestä Asetukset arvosta. Tarkista Kuunteluosoite, TLS-varmennintiedosto ja TLS CA-tiedosto. Sen jälkeen käynnistä baresip uudelleen. Tilin \'%1$s\' rekisteröinti epäonnistui. Todennuspyyntö Todennatko SAS:n <%1$s>\? Siirtopyyntö Hyväksytkö tämän puhelun siirron kohteeseen \'%1$s\'\? Soittopyyntö Hyväksytkö pyynnön soittaa kohteeseen \'%1$s\'\? Automaattinen uudelleenohjaus kohteeseen \'%1$s\'\ Uudelleenohjauspyyntö Hyväksytkö puhelun uudelleenohjauksen kohteeseen \'%1$s\'\? Puhelu epäonnistui Puhelu on päättynyt Tämä puhelu EI ole turvallinen! Tämä puhelu on turvallinen, mutta puhelun toista osapuolta ei ole todennettu! Tämä puhelu on turvallinen ja puhelun toinen osapuoli on todennettu! Haluatko poistaa todennuksen\? Poista todennus Sovelluksen tiedot (äänityksiä lukuunottamatta) talletettiin tiedostoon \'%1$s\'. Android versiossa 9 tiedosto löytyy Download-kansiosta. Sovelluksen tietojen talletus tiedostoon \'%1$s\' epäonnistui. Android versiossa 9 tarkista Asetukset → Sovellukset → baresip → Käyttöluvat → Tallennustila. Uudelleenkäynnistyspyyntö Sovelluksen tiedot palautettiin. baresip pitää käynnistää uudelleen. Käynnistä uudelleen nyt\? Sovelluksen tietojen palauttaminen epäonnistui. Tarkista, että annoit oikean salasanan ja että palautustiedosto kuuluu tällä sovellukselle. Android versiossa 9 tarkista myös Asetukset → Sovellukset → baresip → Käyttöluvat → Tallennustila ja että tiedosto \'%1$s\' on Download-kansiossa. Sovelluksen tietojen palauttaminen epäonnistui. Android versiosta 14 alkaen ei salli tietojen palauttamista, jos ne on tallettu ennen %1$s versioa %2$s. Et voi käyttää tätä sovellusta ilman Ilmoitukset-lupaa. Et voi soittaa puheluita tai vastata niihin ilman Mikrofoni-lupaa. Salli Kamera-lupa jotta voit soittaa videopuheluita ja vastata niihin. Et voi tallentaa sovelluksen tietoja ilman Tallennustila-lupaa. Et voi palauttaa sovelluksen tietoja ilman Tallennustila-lupaa. Ei tuettuja videokameroita! Ei verkkoyhteyttä! Ei laitteistolla tuettua kaiun poistoa! Puhelutiedot Et voi käyttää Androidin yhteystietoja ilman Yhteystiedot-lupaa. Audiofokus on evätty! Tarvittavat luvat baresip tarvitsee Mikrofoni-luvan puheluita varten, Lähellä olevat laitteet -luvan Bluetooth-mikrofonin/kaiuttimen havaitsemista varten, Ilmoitukset-luvan ilmoitusten lähettämistä varten ja Android versiossa 9 Tallennustila-luvan Talleta/Palauta-toimintoja varten. baresip+ tarvitsee Mikrofoni-luvan puheluita varten, Kamera-luvan videopuheluita varten, Lähellä olevat laitteet -luvan Bluetooth-mikrofonin/kaiuttimen havaitsemista varten, Ilmoitukset-luvan ilmoitusten näyttämistä varten ja Android versiossa 9 Tallennustila-luvan Talleta/Palauta-toimintoja varten. Puheluiden talletus Jos aktivoitu, uudet soitetut ja vastatut puhelut talletetaan. Tallennukset voi kuunnella Puhelutiedot-sivulla. Mikrofoni Jos aktivoidaan puhelun aikana, mikrofoni suljetaan. Kaiutin Jos aktivoitu, laitteen kaiutin on päällä.
================================================ FILE: app/src/main/res/values-fr/strings.xml ================================================ Transférer Demande de vidéo Appel vidéo Appel entrant de… Appel sortant à… Quitter Redémarrer À propos Restaurer Sauvegarder Confirmation Erreur Statut Modifier Supprimer Ajouter Refuser Accepter Non Oui OK Annuler Infos Envoyer le message Contacts Le contact « %1$s » existe déjà. Nom de contact invalide « %1$s » Nom Nouveau contact Débogage Serveurs DNS Démarrer automatiquement Paramètres Vous Aujourd’hui Échec de l’envoi du message Ajouter un contact Nouveau message Voulez-vous supprimer l’historique des appels du compte « %1$s » \? Activer Désactiver appel appels Appel Historique des appels Demande de transfert d’appel vers Voulez-vous supprimer le compte « %1$s » \? Déchiffrer le mot de passe Chiffrer le mot de passe URI SIP du nouveau compte Comptes Chiffrement des médias Mot de passe STUN/TURN Nom d’utilisateur « %1$s » invalide Nom d’utilisateur STUN / TURN Messages vocaux URI de la messagerie vocale Nom d’utilisateur d’authentification Nom d’affichage invalide « %1$s » Nom (le cas échéant) utilisé dans l\'URI From des demandes sortantes. Nom d’affichage Compte À propos de baresip+ À propos de baresip Mot de passe d\'authentification invalide « %1$s » Mot de passe d\'authentification jusqu\'à 64 caractères. Si le nom d\'utilisateur d\'authentification est donné mais que le mot de passe n\'est pas donné, il sera demandé au démarrage de baresip. Mot de passe d\'authentification Nom d\'utilisateur d\'authentification invalide « %1$s » Vous avez déjà un appel actif. Écouter Vous n\'avez aucun message et anciens messages un ancien message nouveaux messages un nouveau message Vous avez Débit : %1$s (kbit/s) Infos de l\'appel Durée %1$d Aucune information disponible Nom d\'utilisateur d\'authentification si l\'authentification des demandes SIP est requise. La valeur par défaut est le nom d\'utilisateur du compte. Codecs vidéo Échec S\'enregistrer Codecs audio Serveur STUN/TURN Manuel Mode de réponse Compte par défaut DTMF Alerte Notice Automatique Codecs Historique des conversations Annuler la vérification Adresse d\'écoute Restaurer Modules audio Surnom de compte invalide « %1$s » Le surnom « %1$s » existe déjà Surnom (le cas échéant) utilisé pour identifier ce compte dans l\'application baresip. Surnom Proxies sortants URI SIP d’un ou deux proxies qui doit être utilisé lors de l’envoi des requêtes. Si deux sont données, les demandes REGISTER sont envoyées aux deux et les autres demandes sont envoyées à celui qui répond. Si aucun proxy sortant n’est donné, les demandes sont envoyées en fonction de la recherche d’enregistrement DNS NAPTR/SRV/A de l’hôte URI appelé. Si la partie hôte de l’URI SIP est une adresse IPv6, l’adresse doit être écrite entre crochets []. \nExemples : \n • sip:exemple.com:5061;transport=tls \n • sip:[2001:67c:223:777::10];transport=tcp \n • sip:192.168.43.50:443;transport=wss URI SIP du serveur proxy URI SIP d’un autre serveur proxy URI de serveur proxy invalide \'%1$s’ Si coché, l’enregistrement est activé et les demandes REGISTER sont envoyées à l’intervalle spécifié par intervalle d’enregistrement. Intervalle d’enregistrement Indique à quelle fréquence (en secondes) baresip envoie des demandes REGISTER. Les valeurs valides sont comprises entre 60 et 3600. Intervalle d’enregistrement invalide \'%1$s\' Mot de passe invalide \'%1$s\' Multiplexage RTCP Si coché, les paquets RTP et RTCP sont multiplexés sur un seul port (RFC 5761). Mode DTMF Sélectionne comment les tonalités DTMF 0–9, #, *, et A-D sont envoyées. Sélectionne la façon dont les appels entrants sont répondus. Mode redirection Sélectionne si la demande de redirection d’appel est suivie automatiquement ou si une confirmation est demandée. URI SIP pour la vérification des messages vocaux. Si laissé vide, les messages vocaux (indications d’attente de message) ne sont pas souscrits. Code pays Code de pays invalide \'%1$s’ Fournisseur téléphonie Partie hôte URI SIP utilisée dans les appels vers des numéros de téléphone. La valeur par défaut d’usine est le domaine du compte. Si ce compte n’est pas indiqué, il ne peut pas être utilisé pour appeler des numéros de téléphone. Pavé numérique" Si coché, le pavé numérique s’affiche lorsque le champ \"Appel à …\" est désigné." Si coché, ce compte est sélectionné lorsque Baresip est démarré. Le compte \'%1$s\' existe déjà. Échec d’allocation d’un nouveau compte. Enregistrer Appel manqué de la part de Appels manqués %1$d appels manqués Appel rejeté automatiquement de %1$s Détails appel Direction Durée Lecture d’enregistrement … Voulez-vous ajouter \'%1$s\' aux contacts ou supprimer %2$s de l’historique des appels ? Voulez-vous supprimer \'%1$s\' %2$s de l’historique des appels ? Appel répondu Appel répondu ailleurs Appel manqué Appel rejeté Voulez-vous supprimer le message ? Si coché, Baresip démarre automatiquement après le (re)démarrage de l’appareil. Optimisations batterie Désactivez les optimisations de la batterie (recommandé) si vous souhaitez réduire la probabilité qu’Android limite l’accès de Baresip au réseau ou entre en mode veille. App de téléphone par défaut Adresse d’écoute invalide Famille d\'adresse Choisit les adresses IP que Baresip utilise. Si IPv4 ou IPv6 est choisi, Baresip n’utilise que des adresses IPv4 ou IPv6. Si aucun des deux n’est choisi, Baresip utilise les adresses IPv4 et IPv6. Liste séparée par des virgules des adresses des serveurs DNS. Si elle n’est pas fournie, les adresses des serveurs DNS sont obtenues dynamiquement à partir du système. Chaque adresse DNS est de la forme \'ip:port\' ou \'ip\'. Si le port est omis, il vaut 53 par défaut. Si l’ip est une adresse IPv6 et que le port est également donné, l’ip doit être écrite entre crochets []. À titre d’exemple, liste \'8.8.8.8:53,[2001:4860:4860::8888]:53\' pointe vers les adresses IPv4 et IPv6 des serveurs DNS publics de Google. Serveurs DNS invalides Échec de la configuration des serveurs DNS Fichier de certificat TLS Si coché, un fichier contenant le certificat TLS et la clé privée de cette instance Baresip a été ou sera chargé. Dans la version Android 9, un fichier appelé \'cert.pem\' est chargé à partir du dossier de téléchargement. Pour des raisons de sécurité, supprimez le fichier après le chargement. Vérifier certificats serveur Si coché, Baresip vérifie les certificats TLS de l’agent utilisateur SIP et des serveurs proxy SIP lorsque le transport TLS est utilisé. Fichier CA TLS Si cette case est cochée, un fichier contenant les certificats TLS des autorités de certification qui ne sont pas incluses dans le système d’exploitation Android a été ou sera chargé. Dans la version 9 d’Android, un fichier appelé \'ca_certs.crt\' est chargé à partir du dossier de téléchargement. Pas d’autorisation de lecture pour le stockage externe Paramètres audio Téléphone avec haut-parleur Si coché, le téléphone haut-parleur est automatiquement allumé lorsque l’appel commence. Les codecs audio fournis par les modules vérifiés sont disponibles pour une utilisation par les comptes. Échec du chargement du module. Gain du microphone Multipliez le volume du microphone par ce nombre décimal. La valeur minimale est de 1,0 (défaut d’usine) qui désactive le gain du microphone. Des valeurs plus grandes peuvent affecter négativement la qualité audio. Valeur de gain du microphone invalide Débit binaire Opus Débit binaire maximal moyen utilisé par le flux audio Opus. Les valeurs valides sont 6000-510000. La valeur par défaut est 28000. Perte de paquets prévue d’Opus Pourcentage attendu de perte de paquets du flux audio Opus, à partir de 0–100. La valeur par défaut d’usine est 1. La valeur 0 désactive également la correction d’erreur directe (FEC) Opus. Délai audio Thème sombre Forcer le thème d’affichage sombre Daltonien Utilisez des icônes de statut d’inscription conviviales pour les personnes daltoniennes Détection de proximité"> Si coché, la détection de proximité est active pendant les appels. Taille trame vidéo Taille des trames vidéo transmises (largeur x hauteur) Trames vidéo par seconde Taux de trame vidéo qui sera offert lors de la poignée de main SDP. Les valeurs valides sont comprises entre 10 et 30. Trames invalides par seconde \'%1$d\' Sonnerie Sélection sonnerie utilisateur@domaine ou numéro de téléphone Favoris Si coché, le contact est affiché parmi les autres favoris en haut de la liste des contacts. Si coché, ce contact est ajouté aux contacts Android. Image de profil Voulez-vous appeler ou envoyer un message à \'%1$s\' ? Voulez-vous supprimer le contact \'%1$s\' ? Anonyme Inconnu(e) Le compte \'%1$s\' n’a pas de fournisseur de téléphonie Accepter l’envoi et la réception de vidéos avec \'%1$s\' ? Accepter l’envoi de vidéo à \'%1$s\' ? Accepter de recevoir une vidéo de \'%1$s\' ? L\'appel est en attente L’enregistrement peut être activé ou désactivé uniquement lorsque l’appel n’est pas connecté Transfert d\'appel Destination de transfert Choisir l’URI de destination Transfert échoué Paquets Perdu(s) Baresip n’a pas réussi à démarrer. Cela peut être dû à une valeur de paramètres invalide. Vérifiez l’adresse d’écoute, le fichier de certificat TLS et le fichier CA TLS. Puis redémarrez Baresip. L’enregistrement de %1$s a échoué. Vérifier requête Voulez-vous vérifier SAS <%1$s> ? Acceptez-vous de transférer cet appel à \'%1$s\' ? Requête d\'appel Acceptez-vous la demande d’appeller \'%1$s\' ? Redirection automatique vers \'%1$s\'\\ Requête de redirection Acceptez-vous la redirection d’appel vers \'%1$s\' ? Appel échoué Appel est fermé Cet appel n’est PAS sécurisé ! Cet appel est SÉCURISÉ, mais le pair n’est PAS vérifié ! Cet appel est SÉCURISÉ et le pair est VÉRIFIÉ ! Voulez-vous dévérifier le pair ? Données de l’application (à l’exclusion des enregistrements) sauvegardées dans le fichier \'%1$s\'. Dans la version 9 d\'Android, le fichier est dans le dossier Download. Échec à sauvegarder les données de l’application dans le fichier \'%1$s\'. Vérifiez Apps → Baresip → Permissions → Stockage. Requête redémarrage Données de l’application restaurées. Baresip a besoin d\'être redémarré. Redémarrer maintenant ? Échec à restaurer les données de l’application. Android version 14 et ultérieure ne permet pas de restaurer les données qui ont été sauvegardées avant %1$s version %2$s. Vous ne pouvez pas utiliser cette application sans la permission \"Notifications\". Baresip a besoin de la permission \"Microphone\" pour les appels vocaux. Accorder à la \"Caméra\" l’autorisation de passer ou de répondre aux appels vidéo. Vous ne pouvez pas créer de sauvegarde sans la permission \"Stockage\". Vous ne pouvez pas restaurer la sauvegarde sans l’autorisation \"Stockage\". Vous ne pouvez pas accéder aux contacts Android sans la permission \"Contacts\". Pas de caméras vidéo prises en charge ! Pas de connexion réseau ! Pas d’annulation d’écho acoustique matérielle ! Focus audio refusé ! Baresip a besoin de l’autorisation \"Microphone\" pour les appels vocaux, de l’autorisation \"Appareils à proximité\" pour la détection Bluetooth microphone/haut-parleur, de l’autorisation \"Notifications\" pour publier des notifications, et dans Android 9, de l’autorisation \"Stockage\" pour les opérations de sauvegarde/restauration. Enregistrement d\'appel Si activé, les nouveaux appels entrants et sortants seront enregistrés. Les enregistrements peuvent être lus sur la page Détails d’appel Microphone Si activé pendant l’appel, le microphone est coupé. Nom d’utilisateur si requis par le serveur STUN/TURN Mot de passe si requis par le serveur STUN/TURN Requêtes INFO SIP Pair Si coché, Baresip est l’application par défaut de téléphone. Ne cochez pas si votre appareil peut également avoir besoin de gérer d’autres appels ou messages que SIP. Taux de débit Opus invalide Pourcentage de perte de paquet Opus invalide Temps (en millisecondes) pour attendre l’audio de l’appelé lorsque l’appel est établi. Réglez sur une valeur plus élevée si vous manquez l’audio de l’appelé au début de l’appel. Délai audio invalide \'%1$s\'. Les valeurs valides sont comprises entre 100 et 3000. Volume d’appel par défaut Heure ================================================ FILE: app/src/main/res/values-hr/strings.xml ================================================ ================================================ FILE: app/src/main/res/values-in/strings.xml ================================================ ================================================ FILE: app/src/main/res/values-iw/strings.xml ================================================ אודות baresip אודות baresip+ סוכן משתמש SIP המבוסס על ספריית baresip

Juha Heinanen <jh@tutpro.com>

גירסה %1$s


טיפים לשימוש

  • בדוק שערכי ברירת מחדל בהגדרות baresip מתאימים לצרכיך (גע בכותרות הפריטים לקבלת עזרה).
  • לאחר מכן, תחת "חשבונות", צור חשבון אחד או יותר (שוב, גע בכותרות הפריטים לקבלת עזרה).
  • סטטוס הרישום של חשבון מוצג באמצעות נקודה בצבעים שונים: ירוק (הרישום הצליח), צהוב (הרישום בתהליך), אדום (הרישום נכשל), לבן (הרישום לא הופעל).
  • נגיעה בנקודת הסטטוס מובילה ישירות להגדרת החשבון.
  • נגיעה ממושכת על אייקוני סרגל baresip מציגה מידע אודות האייקונים.
  • מחוות החלקה כלפי מטה גורמת לרישום מחדש של החשבון המוצג כרגע.
  • נגיעה ממושכת על החשבון המוצג כרגע מאפשרת או מבטלת את רישום החשבון.
  • מחוות החלקה שמאלה/ימינה מעבירה בין החשבונות.
  • הצד השני בשיחה האחרונה יכול להיבחר מחדש על ידי נגיעה באייקון השיחה כאשר שדה איש הקשר ריק.
  • השותפים בשיחות ובהודעות יכולים להתווסף לאנשי קשר על ידי נגיעה ממושכת.
  • נגיעות ממושכות יכולות לשמש גם כדי להסיר שיחות, צ\'אטים, הודעות ואנשי קשר.
  • נגיעה/נגיעה ממושכת על אייקון אנשי קשר יכולה לשמש להתקנה/הסרה של תמונת אווטאר.
  • נגיעה ממושכת על קודק אודיו יכולה לשמש להפעלה/השעיית הקודק.
  • ראה את ה־ויקי למידע נוסף.

מדיניות פרטיות

מדיניות פרטיות זמינה כאן.


קוד מקור

קוד מקור זמין ב־GitHub, שם ניתן גם לדווח על תקלות.


תרגומים

תרגומי שפות מנוהלים דרך פרויקט Weblate של baresip.


רשיונות

  • BSD-3-Clause חוץ מהבאים:
  • Apache 2.0 AMR codecs and TLS security
  • AGPLv4 ZRTP media encryption
  • GNU LGPL 2.1 Codec2 codec
  • Free G.722 codec
  • GNU GPLv3 G.729 codec
]]>

סוכן משתמש SIP המבוסס על ספריית baresip


Juha Heinanen <jh@tutpro.com>

גירסה %1$s


טיפים לשימוש

  • בדוק שערכי ברירת מחדל בהגדרות baresip מתאימים לצרכיך (גע בכותרות הפריטים לקבלת עזרה).
  • לאחר מכן, תחת "חשבונות", צור חשבון אחד או יותר (שוב, גע בכותרות הפריטים לקבלת עזרה).
  • סטטוס הרישום של חשבון מוצג באמצעות נקודה בצבעים שונים: ירוק (הרישום הצליח), צהוב (הרישום בתהליך), אדום (הרישום נכשל), לבן (הרישום לא הופעל).
  • נגיעה בנקודת הסטטוס מובילה ישירות להגדרת החשבון.
  • נגיעה ממושכת על אייקוני סרגל baresip מציגה מידע אודות האייקונים.
  • מחוות החלקה כלפי מטה גורמת לרישום מחדש של החשבון המוצג כרגע.
  • נגיעה ממושכת על החשבון המוצג כרגע מאפשרת או מבטלת את רישום החשבון.
  • מחוות החלקה שמאלה/ימינה מעבירה בין החשבונות.
  • הצד השני בשיחה האחרונה יכול להיבחר מחדש על ידי נגיעה באייקון השיחה כאשר שדה איש הקשר ריק.
  • השותפים בשיחות ובהודעות יכולים להתווסף לאנשי קשר על ידי נגיעה ממושכת.
  • נגיעות ממושכות יכולות לשמש גם כדי להסיר שיחות, צ\'אטים, הודעות ואנשי קשר.
  • נגיעה/נגיעה ממושכת על אייקון אנשי קשר יכולה לשמש להתקנה/הסרה של תמונת אווטאר.
  • נגיעה ממושכת על קודק אודיו יכולה לשמש להפעלה/השעיית הקודק.
  • ראה את ה־ויקי למידע נוסף.

תקלות ידועות

  • תצוגה עצמית אינה מוצגת כראוי כאשר זרם הווידאו מוגדר כשליחה בלבד.

מדיניות פרטיות

מדיניות פרטיות זמינה כאן.


קוד מקור

קוד מקור זמין ב־GitHub, שם ניתן גם לדווח על תקלות.


תרגומים

תרגומי שפות מנוהלים דרך פרויקט Weblate של baresip.


רשיונות

  • BSD-3-Clause חוץ מהבאים:
  • Apache 2.0 AMR codecs and TLS security
  • AGPLv4 ZRTP media encryption
  • GNU LGPL 2.1 Codec2 codec
  • Free G.722 codec
  • GNU GPLv3 G.729 codec
  • GNU GPLv2 H.264 and H.265 codecs
  • AOMedia AV1 codec
]]>
חשבון כינוי (אם יש) המשמש לזיהוי החשבון הזה בתוך אפליקציית baresip. כינוי כינוי חשבון לא תקין \'%1$s\' כינוי \'%1$s\' כבר קיים שם תצוגה שם (אם יש) המשמש ב־From URI של בקשות יוצאות. שם תצוגה לא תקין \'%1$s\' שם משתמש לאימות שם משתמש לאימות אם נדרש אימות לבקשות SIP. ברירת המחדל היא שם המשתמש של החשבון. שם משתמש לאימות לא תקני \'%1$s\' סיסמה לאימות סיסמת אימות עד 64 תוים. אם שם המשתמש לאימות נמסר, אך הסיסמה לא נמסרה, היא תתבקש כאשר baresip יתחיל. סיסמת אימות לא תקינה \'%1$s\' פרוקסים יוצאים כתובת SIP URI של פרוקסי אחד או שניים שיש להשתמש בהם בעת שליחת בקשות. אם ניתנים שניים, בקשות REGISTER נשלחות לשניהם, ושאר הבקשות נשלחות לזה שמגיב. אם לא הוגדר פרוקסי יוצא (outbound proxy), הבקשות נשלחות על פי חיפוש רשומות DNS מסוג NAPTR/SRV/A של ה־hostpart ב־URI של היעד. אם ה־hostpart של כתובת ה־SIP URI הוא כתובת IPv6, יש לכתוב את הכתובת בתוך סוגריים מרובעים []. \nדוגמאות: \n • sip:example.com:5061;transport=tls \n • sip:[2001:67c:223:777::10];transport=tcp \n • sip:192.168.43.50:443;transport=wss כתובת SIP URI של שרת פרוקסי כתובת SIP URI של שרת פרוקסי נוסף כתובת URI של שרת פרוקסי לא תקינה \'%1$s\' הרשמה אם האפשרות מסומנת, הרישום מופעל ובקשות REGISTER נשלחות במרווח הזמן המוגדר ב\'מרווחי זמן רישום\'. מרווחי זמן רישום מציין כל כמה זמן (בשניות) baresip שולח בקשות REGISTER. הערכים התקינים הם בין 60 ל־3600. מרווח זמן רישום לא תקין \'%1$s\' בדוק מקור אם האפשרות מסומנת, בקשות נכנסות מותרות רק מכתובות IP שאליהן נשלחו בקשות רישום. מעבר NAT למדיה בוחר את פרוטוקול מעבר ה־NAT למדיה (אם יש). האפשרויות הזמינות הן STUN ‏(Session Traversal Utilities for NAT, ‏RFC 5389) ו־ICE ‏(Interactive Connectivity Establishment, ‏RFC 5245). שרת STUN/TURN כתובת URI של שרת STUN/TURN בפורמט scheme:host[:port][?transport=udp|tcp], כאשר ‎scheme‎ הוא אחד מהבאים: ‎\'stun\' ‎, ‎\'stuns\'‎, \'turn\'‎ או ‎\'turns\'‎. שרת ה־STUN המוגדר כברירת מחדל מהיצרן עבור פרוטוקולי STUN ו־ICE הוא \'stun:stun.l.google.com:19302\', המצביע על שרת STUN ציבורי של Google. לא קיים שרת TURN המוגדר כברירת מחדל מהיצרן. כתובת URI לא חוקית של שרת STUN/TURN \'%1$s\' הצפנת מדיה בוחר את פרוטוקול הצפנת המדיה לתעבורה (אם קיים). \n • ZRTP (מומלץ) משמעותו שמתבצע ניסיון לניהול משא ומתן להצפנת מדיה מקצה לקצה באמצעות ZRTP לאחר שהשיחה כבר נוצרה. \n • DTLS-SRTPF משמעותו שהפרוטוקול UDP/TLS/RTP/SAVPF מוצע בשיחה יוצאת, ושבמקרה של שיחה נכנסת ייעשה שימוש באחד מהפרוטוקולים הבאים אם יוצעו: RTP/SAVP, RTP/SAVPF, UDP/TLS/RTP/SAVP או UDP/TLS/RTP/SAVPF. \n • SRTP-MANDF משמעותו שהפרוטוקול RTP/SAVPF מוצע בשיחה יוצאת ונדרש בשיחה נכנסת. \n • SRTP-MAND משמעותו שהפרוטוקול RTP/SAVP מוצע בשיחה יוצאת ונדרש בשיחה נכנסת. \n • SRTP משמעותו שהפרוטוקול RTP/AVP מוצע בשיחה יוצאת, ובשיחה נכנסת ייעשה שימוש ב-RTP/SAVP או ב-RTP/SAVPF אם הם מוצעים. ריבוב (Multiplexing) RTCP אם האפשרות מסומנת, חבילות RTP ו־RTCP מרובבות (multiplexed) על גבי יציאה אחת (RFC 5761). תגובות זמניות אמינות אם האפשרות מסומנת, תצוין תמיכה בתגובות זמניות אמינות (RFC 3262). אינך יכול לגשת לאנשי הקשר של אנדרואיד ללא הרשאת \"אנשי קשר\". שחזור נתוני היישום נכשל. ודא שהזנת סיסמה נכונה ושהקובץ הגיבוי שייך ליישום זה. בגרסאות אנדרואיד 9, בדוק גם יישומים → baresip → הרשאות → אחסון ושקובץ \'%1$s\' קיים בתיקיית \"הורדות\". שחזור נתוני היישום נכשל. גרסאות אנדרואיד 14 ומעלה אינן מאפשרות שחזור נתונים שגובו לפני %1$s גרסה %2$s. השיחה מאובטחת והצד השני מאומת! האם ברצונך לבטל את אימות הצד השני? האם אתה מאשר בקשת שיחה אל \'%1$s\'? הפעלת Baresip נכשלה. ייתכן שהסיבה לכך היא ערך שגוי בהגדרות. בדוק את כתובת ההאזנה (Listen Address), את קובץ תעודת ה־TLS ואת קובץ ה־TLS CA. לאחר מכן הפעל מחדש את Baresip. הודעות חדשות קצב ממוצע: %1$s (Kbits/s) ההעברה נכשלה ניתן להפעיל או לכבות הקלטה רק כאשר אין שיחה מחוברת לחשבון \'%1$s\' אין ספק טלפוניה אישור ביטול האם אתה רוצה להתקשר או לשלוח הודעה אל \'%1$s\'? מועדף עליך להפעיל מחדש את baresip כדי להפעיל את ההגדרות החדשות. להפעיל מחדש כעת? אם האפשרות מסומנת וגם מצב Debug מסומן, הודעות ה־Logcat יכללו גם מעקב אחר בקשות ותשובות SIP. האפשרות תבוטל אוטומטית בעת הפעלת baresip. איפוס לברירות מחדל של היצרן ערך שדה סוכן-משתמש לא תקין קצב פריימים לשניה השתמש בצבעים דינמיים אם הם מופעלים בהגדרות אנדרואיד ברירת מחדל עוצמת שמע של שיחה צפוי אובדן חבילות Opus מקודדי השמע המסופקים על ידי המודולים המסומנים זמינים לשימוש עבור החשבונות. קובץ TLS CA רשימה מופרדת בפסיקים של כתובות שרתי DNS. אם לא מצוין ערך, כתובות שרתי ה־DNS מתקבלות באופן דינמי מהמערכת. כל כתובת DNS היא בתבנית \'ip:port\' או \'ip\'. אם לא צוין מספר יציאה, ברירת המחדל היא 53. אם ה־ip הוא כתובת IPv6 וגם מצוין מספר יציאה, יש לכתוב את כתובת ה־ip בתוך סוגריים מרובעים []. לדוגמה, הרשימה \'8.8.8.8:53,[2001:4860:4860::8888]:53\' מצביעה על כתובות IPv4 ו־IPv6 של שרתי ה־DNS הציבוריים של Google. מיטוב סוללה האם ברצונך למחוק היסטוריית צ\'אט של חשבון \'%1$s\'? נכשל האם ברצונך למחוק שיחה זו מההיסטוריה? השיחה נענתה במקום אחר משך זמן האם ברצונך להוסיף \'%1$s\' לאנשי קשר או למחוק %2$s מהיסטוריית השיחות? שיחה חסום שיחה שלא נענתה מ user@domain[:port][;transport=udp|tcp|tls] \'%1$s\' לא תקין אם האפשרות מסומנת, לוח מקשים מספרי יוצג כאשר השדה \"חייג אל…\" נמצא במיקוד." כתובת SIP URI לבדיקת הודעות דואר קולי. אם השדה נשאר ריק, לא תתבצע הרשמה לקבלת התראות על הודעות דואר קולי (Message Waiting Indications). מצב הפניה מצב DTMF סיסמה לא תקינה \'%1$s\' שם משתמש STUN/TURN שם משתמש אם נדרש על ידי שרת STUN/TURN שם משתמש לא תקין \'%1$s\' סיסמת STUN/TURN בקשות SIP INFO SIP INFO או RTP בתוך הפס (In-band) מצב מענה בוחר כיצד שיחות נכנסות נענות. בוחר האם בקשת הפניית שיחה תבוצע באופן אוטומטי, או שתידרש בקשת אישור מהמשתמש. מדריך למשתמש אוטומטית חסום \"לא מזוהה\" חסום שיחות והודעות ממתקשרים שלא קיימים באנשי הקשר. כתובת URI של דואר קולי קידומת מדינה קוד מדינה בתבנית E.164 עבור חשבון זה. אם חלק המשתמש (userpart) ב־URI של שיחה נכנסת או הודעה נכנסת מכיל מספר טלפון שאינו מתחיל בסימן \'+\' ואם חיפוש איש הקשר נכשל, המספר יקבל קידומת של מדינה זו ויתבצע ניסיון נוסף לאיתור איש הקשר. אם מספר הטלפון מתחיל בספרה בודדת \'0\', הספרה \'0\' תוסר לפני הוספת קוד המדינה. קידומת מדינה לא חוקית \'%1$s\' ספק טלפוניה חלק ה־host של כתובת ה־SIP URI שישמש בשיחות למספרי טלפון. ברירת המחדל של היצרן היא הדומיין של החשבון. אם לא צוין ערך, לא ניתן יהיה להשתמש בחשבון זה לביצוע שיחות למספרי טלפון. לוח מקשים מספרי" חלק ה־host בכתובת ה־SIP URI אינו חוקי \'%1$s\' חשבון ברירת מחדל אם האפשרות מסומנת, חשבון זה נבחר בהפעלת baresip. חשבונות כתובת SIP URI של חשבון חדש כתובת SIP URI של חשבון חדש בתבנית ‎<user>@<domain>[:<port>][;transport=udp|tcp|tls]. אם מצוין <‎port> אך לא מצוין פרוטוקול תעבורה, ברירת המחדל של פרוטוקול התעבורה תהיה UDP. אם לא מצוין <‎<port אך מצוין פרוטוקול תעבורה, ברירת המחדל של <‎<port תהיה 5060 או 5061 ‏(TLS). אם אף אחד מהם לא מצוין ולא הוגדר פרוקסי יוצא, שרת הרישום (registrar) של החשבון (אם קיים) ייקבע אך ורק על סמך מידע ה־DNS של הדומיין. חשבון \'%1$s\' כבר קיים. הקצאת חשבון חדש נכשלה. הצפנת סיסמה סיסמה אם נדרשת על ידי שרת STUN/TURN בוחר כיצד יישלחו צלילי DTMF עבור הספרות 0-9 והתווים ‎#, *, ו־A-D. אירועי RTP בתוך־הפס פענוח סיסמה האם ברצונך למחוק את החשבון \'%1$s\'? מתקשר שיחות שלא נענו %1$d שיחות שלא נענו בקשת העברת שיחה אל שיחה נדחתה אוטומטית מ־ %1$s שיחה חסומה נדחתה אוטומטית מ־ %1$s הודעה חסומה נדחתה אוטומטית מ־ %1$s שיחות חסומות הודעות חסומות האם ברצונך למחוק בקשות חסומות של \'%1$s\'? האם ברצונך להוסיף את המתקשר \'%1$s\' לאנשי הקשר? היסטוריית שיחות פרטי שיחה שיחות שיחה צד (peer) כיוון זמן האם ברצונך למחוק \'%1$s\' %2$s מהיסטוריית השיחות? השבת הפעל האם ברצונך למחוק היסטוריית שיחות של חשבון \'%1$s\'? שיחה נענתה שיחה שלא נענתה שיחה נדחתה משמיע הקלטה … שמירת הקלטה האם ברצונך לשמור הקלטה זו? ההקלטה נשמרה צ\'אט עם %1$s הודעה חדשה האם ברצונך למחוק הודעה או להוסיף צד (peer) \'%1$s\' לאנשי הקשר? האם ברצונך למחוק את ההודעה? הוספת איש קשר שליחת ההודעה נכשלה היסטוריית צ\'אט היום אתה צד (peer) צ\'אט חדש האם ברצונך למחוק צ\'אט עם צד (peer) \'%1$s\' או להוסיף צד לאנשי הקשר? האם ברצונך למחוק צ\'אט עם \'%1$s\'? מקודדי שמע מקודדי וידאו הגדרות הפעל אוטומטית אם האפשרות מסומנת, baresip יופעל אוטומטית לאחר אתחול המכשיר או לאחר התקנת גרסה חדשה של baresip. ההפעלה תתבצע רק לאחר ביטול נעילת המכשיר. הפעלה אוטומטית דורשת הרשאת \"הצג מעל יישומים אחרים\". השבת מיטובי סוללה (מומלץ) אם ברצונך להפחית את הסיכוי שאנדרואיד תגביל את הגישה של baresip לרשת או תעביר את baresip למצב המתנה. יישומון הטלפון המוגדר כברירת מחדל תפקיד חייגן אינו זמין אם האפשרות מסומנת, baresip יוגדר כיישומון טלפון ברירת מחדל. אין לסמן אפשרות זו אם המכשיר שלך צריך לטפל גם בשיחות או הודעות שאינן מסוג SIP. כתובת האזנה כתובת IP ומספר יציאה בתבנית \'‎address:port\' שבהם baresip מאזין לבקשות SIP נכנסות. אם כתובת ה־IP היא כתובת IPv6, יש לכתוב אותה בתוך סוגריים מרובעים []. כתובת IPv4 ‏0.0.0.0 או כתובת IPv6 ‏[::] מאפשרות ל־baresip להאזין בכל הכתובות הזמינות. אם השדה נשאר ריק (ברירת מחדל של היצרן), baresip יאזין ביציאה שרירותית בכל הכתובות הזמינות. כתובת האזנה לא חוקית משפחת כתובות בוחר באילו כתובות IP ישתמש baresip. אם נבחר IPv4 או IPv6,‏ baresip ישתמש רק בכתובות מסוג IPv4 או IPv6. אם אף אחת מהאפשרויות לא נבחרה, baresip ישתמש גם בכתובות IPv4 וגם בכתובות IPv6. פרוטוקולי תעבורה רשימה מופרדת בפסיקים של פרוטוקולי תעבורה נתמכים עבור בקשות/תשובות SIP. אם השדה ריק, ערך ברירת המחדל הוא \'udp,tcp,tls,ws,wss\' הכולל את כל פרוטוקולי התעבורה הנתמכים. יש לציין ברשימה רק את הפרוטוקולים הנחוצים לך. השימוש ב־UDP אינו מומלץ מטעמי אבטחה וסיבות נוספות. רשימת פרוטוקולי תעבורה לא חוקית שרתי DNS שרתי DNS לא חוקיים נכשלה הגדרת שרתי DNS קובץ תעודת TLS אם האפשרות מסומנת, ייטען או נטען כבר קובץ המכיל תעודת TLS ומפתח פרטי עבור מופע זה של baresip. באנדרואיד גירסה 9, ייטען קובץ בשם \'cert.pem\' מתיקיית ההורדות. מטעמי אבטחה, מומלץ למחוק את הקובץ לאחר טעינתו. אימות תעודות שרת אם האפשרות מסומנת, baresip יאמת את תעודות ה־TLS של סוכן המשתמש (SIP User Agent) ושל שרתי ה־Proxy של SIP כאשר נעשה שימוש בתעבורת TLS. אם האפשרות מסומנת, ייטען או נטען כבר קובץ המכיל תעודות TLS של רשויות אישורי תעודות (Certificate Authorities) שאינן כלולות במערכת ההפעלה אנדרואיד. באנדרואיד גירסה 9, ייטען קובץ בשם \'ca_certs.crt\' מתיקיית ההורדות. אין הרשאת קריאה מאחסון חיצוני הגדרות שמע רמקול אם האפשרות מסומנת, הרמקול יופעל אוטומטית עם תחילת השיחה. מודולי שמע טעינת מודול נכשלה. רמת הגברת מיקרופון (Gain) הכפל את עוצמת המיקרופון במספר עשרוני זה. הערך המינימלי הוא ‎1.0 (ברירת מחדל של היצרן) אשר מבטל את הגברת המיקרופון. ערכים גבוהים יותר עלולים לפגוע באיכות השמע. ערך הגברת מיקרופון (Gain) לא חוקי קצב סיביות של Opus קצב הסיביות המרבי הממוצע המשמש את זרם השמע מסוג Opus. ערכים חוקיים הם בטווח 6000-510000. ברירת המחדל של היצרן היא 28000. אחוז אובדן המנות הצפוי בזרם השמע מסוג Opus, בטווח 0-100. ערך ברירת המחדל של היצרן הוא 1. הערך 0 גם מבטל את מנגנון תיקון השגיאות קדימה (FEC) של Opus. קצב סיביות של Opus לא חוקי אחוז אובדן מנות Opus אינו חוקי השהיית שמע זמן (באלפיות שנייה) להמתנה לקבלת שמע מהצד הנקרא לאחר יצירת השיחה. יש להגדיר ערך גבוה יותר אם חסר שמע מהצד השני בתחילת השיחה. השהיית שמע לא חוקית \'%1$s\'. ערכים חוקיים הם בטווח 100 עד 3000. אם מוגדר ערך, זוהי עוצמת השמע המוגדרת כברירת מחדל לשיחה, בסולם של 1-10. מדינת צלילים המדינה שלפיה נקבעים צלילי הצלצול, ההמתנה ותפוס אצל הנמען ערכת נושא כהה כפה שימוש בערכת נושא כהה צבעים דינמיים עיוורון צבעים השתמש בסמלי מצב רישום המותאמים לעיוורי צבעים חישת קירבה"> אם האפשרות מסומנת, חישת קירבה מופעלת בזמן שיחות. גודל פריים וידאו גודל פריימי הווידאו המשודרים (רוחב x גובה) קצב פריימים של הווידאו שיוצע במהלך תהליך ה־SDP handshake. ערכים חוקיים הם בטווח 10 עד 30. קצב פריימים לשניה לא חוקי \'%1$d\' צלצול בחר צלצול סוכן משתמש ערך מותאם אישית לשדה הכותרת סוכן-משתמש בבקשות/תשובות SIP בוחר האם להשתמש באנשי קשר של baresip, באנשי הקשר של אנדרואיד, או בשניהם. אם נבחר להשתמש בשניהם וקיים איש קשר עם אותו שם בשתי הרשימות, תינתן עדיפות לאיש הקשר של baresip. גם וגם ניפוי שגיאות (Debug) אם האפשרות מסומנת, יוצגו הודעות יומן ברמות Debug ו־Info ב־Logcat. מעקב SIP אם האפשרות מסומנת, ההגדרות יאופסו לערכי ברירת המחדל של היצרן. האם אתה בטוח שברצונך לאפס את ההגדרות לערכי ברירת המחדל של היצרן? איפוס קריאת קובץ \'cert.pem\' נכשלה. קריאת קובץ \'ca_certs.crt\' נכשלה. בקשת הסכמה אם נבחר להשתמש באנשי הקשר של אנדרואיד, ניתן יהיה להשתמש בהם לצורך שיחות והודעות כהפניות לכתובות SIP ו־tel URI. אפליקציית baresip אינה שומרת את אנשי הקשר של אנדרואיד ואינה משתפת אותם עם אף גורם. כדי לאפשר שימוש באנשי הקשר של אנדרואיד בתוך baresip, נדרש אישור מ־Google עבור השימוש בהם כמתואר כאן ובמדיניות הפרטיות של האפליקציה. איש קשר חדש שם כתובת SIP או tel URI user@domain או מספר טלפון אם האפשרות מסומנת, איש הקשר יוצג יחד עם שאר המועדפים בראש רשימת אנשי הקשר. שם איש קשר לא חוקי \'%1$s\' איש קשר \'%1$s\' כבר קיים. אם האפשרות מסומנת, איש קשר זה יתווסף לאנשי הקשר של אנדרואיד. תמונת פרופיל אנשי קשר שלח הודעה האם ברצונך למחוק את איש הקשר \'%1$s\'? חיפוש התראה מידע הודעת מערכת עצור השב שמור OK כן לא קבל דחה הוסף מחק ערוך מצב שגיאה אנונימי לא ידוע כתובת SIP או tel URI לא חוקית \'%1$s\' גיבוי שחזור אודות הפעל מחדש יציאה התקשר אל … שיחה מ … הועבר על ידי … שיחת וידאו בקשת וידאו לאשר שליחה וקבלה של וידאו עם \'%1$s\'? לאשר שליחת וידאו אל \'%1$s\'? לאשר קבלת וידאו מאת \'%1$s\'? השיחה בהמתנה העברת שיחה עיוורת מלווה יעד העברה בחר URI של יעד העברה DTMF פרטי שיחה אין מידע זמין משך זמן: %1$d (שניות) מקודדים קצב נוכחי: %1$s (Kbits/s) מנות מנות שאבדו ריצוד: %1$s (ms) הודעות דואר קולי יש לך הודעה חדשה אחת הודעה ישנה אחת הודעות ישנות ו אין הודעות האזן כבר יש לך שיחה פעילה. רישום של %1$s נכשל. בקשת אימות האם ברצונך לאמת SAS <%1$s>? בקשת העברה האם לאשר העברת שיחה זו אל \'%1$s\'? בקשת שיחה הפניה אוטומטית אל \'%1$s\'\\ בקשת הפניה האם לאשר הפניית שיחה אל \'%1$s\'? שיחה נכשלה השיחה נסגרה שיחה זו אינה מאובטחת! שיחה זו מאובטחת, אך הצד השני אינו מאומת! בטל אימות נתוני היישום (למעט הקלטות) גובו לקובץ \'%1$s\'. באנדרואיד גירסה 9, הקובץ נמצא בתיקיית ההורדות. גיבוי נתוני היישום לקובץ \'%1$s\' נכשל. יש לבדוק: יישומים ← baresip ← הרשאות ← אחסון. בקשת הפעלה מחדש נתוני היישום שוחזרו. יש להפעיל מחדש את baresip. האם להפעיל מחדש כעת? לא ניתן להשתמש ביישומון זה ללא הרשאת \"התראות\". baresip זקוק להרשאת \"מיקרופון\" לצורך ביצוע שיחות קוליות. הענק הרשאת \"מצלמה\" כדי לבצע או לענות לשיחות וידאו. לא ניתן ליצור גיבוי ללא הרשאת \"אחסון\". לא ניתן לשחזר גיבוי ללא הרשאת \"אחסון\". לא נמצאו מצלמות וידאו נתמכות! אין חיבור לרשת! אין ביטול הד אקוסטי בחומרה! נדחתה בקשת מיקוד שמע! הסבר על הרשאות baresip זקוק להרשאת \"מיקרופון\" לצורך ביצוע שיחות קוליות, להרשאת \"מכשירים בקרבת מקום\" לצורך זיהוי מיקרופון/רמקול בלוטות\', להרשאת \"התראות\" לצורך הצגת הודעות מערכת, ובאנדרואיד גירסה 9 גם להרשאת \"אחסון\" לצורך פעולות גיבוי ושחזור. baresip+ זקוק להרשאת \"מיקרופון\" לצורך ביצוע שיחות קוליות, להרשאת \"מצלמה\" לצורך שיחות וידאו, להרשאת \"מכשירים בקרבת מקום\" לצורך זיהוי מיקרופון/רמקול בלוטות\', להרשאת \"התראות\" לצורך הצגת הודעות מערכת, ובאנדרואיד גירסה 9 גם להרשאת \"אחסון\" לצורך פעולות גיבוי ושחזור. הקלטת שיחה אם האפשרות מופעלת, שיחות נכנסות ויוצאות חדשות יוקלטו. ניתן יהיה להאזין להקלטות בדף פרטי השיחה. מיקרופון אם האפשרות מופעלת במהלך שיחה, המיקרופון יושתק. רמקול אם האפשרות מופעלת, השמע יושמע דרך הרמקול של המכשיר. URI ייחודי של איש קשר אם מסומן, URI של איש הקשר מובטח להיות ייחודי. יש לסמן זאת אם קיימים יותר מחשבון אחד עם חלק המשתמש זהה ב־SIP URI, אך גם משפר את ההגנה על החשבונות מפני תקיפות.
================================================ FILE: app/src/main/res/values-ja-rJP/strings.xml ================================================ Baresip library based SIP User Agent

Juha Heinanen <jh@tutpro.com>

Version %1$s

Usage Hints

  • Check that default values in Settings meet your needs (touch item titles for help).
  • Then in Accounts, create one or more accounts (again touch item titles for help).
  • Registration status of an account is shown with a colored dot: green (registration succeeded, yellow (registration is in progress), red (registration failed), white (registration has not been activated).
  • Touch on the dot leads directly to account configuration.
  • Swipe down gesture causes re-registration of the currently shown account.
  • Peers of calls and messages can be added to contacts by long touches.
  • Long touches can also be used to remove calls, chats, messages, and contacts.
  • Touch/long touch of contact icon can be used to install/remove image avatar.
  • You can re-reselect the previous call party by touching the call icon when Callee is empty.

Known Issues

  • Due to limitations in underlying libraries, baresip does not currently support multiple, concurrently active network interfaces. Active network interface preference order is VPN, Internet, other.

Source code

Source code is available at GitHub, where also issues can be reported.

Licenses

  • BSD-3-Clause except the following:
  • Apache 2.0 AMR codec and TLS security
  • LGPL 2.1 G.722 and G.726 codecs
  • AGPLv4 ZRTP media encryption
  • GNU GPLv3 G.729 codec
]]>
Baresip library based SIP User Agent with video calls

Juha Heinanen <jh@tutpro.com>

Version %1$s

Usage Hints

  • Check that default values in Settings meet your needs (touch item titles for help).
  • Then in Accounts, create one or more accounts (again touch item titles for help).
  • Registration status of an account is shown with a colored dot: green (registration succeeded, yellow (registration is in progress), red (registration failed), white (registration has not been activated).
  • Touch on the dot leads directly to account configuration.
  • Swipe down gesture causes re-registration of the currently shown account.
  • Peers of calls and messages can be added to contacts by long touches.
  • Long touches can also be used to remove calls, chats, messages, and contacts.
  • Touch/long touch of contact icon can be used to install/remove image avatar.
  • You can re-reselect the previous call party by touching the call icon when Callee is empty.

Known Issues

  • Due to limitations in underlying libraries, multiple concurrently active network interfaces are not supported. Active network interface preference order is VPN, Internet, other.
  • In video calls, the device needs to be held in landscape mode rotated 90 degrees left from portrait orientation.
  • Selfview is not properly shown when video stream is sendonly.

Source code

Source code is available at GitHub, where also issues can be reported.

Licenses

  • BSD-3-Clause except the following:
  • Apache 2.0 AMR codec and TLS security
  • LGPL 2.1 G.722 and G.726 codecs
  • AGPLv4 ZRTP media encryption
  • GNU GPLv2 H.264 codec
  • GNU GPLv3 G.729 codec
]]>
SIPリクエストの認証が必要な場合は、認証ユーザー名を入力して下さい。 デフォルト値はアカウントのユーザー名です。 認証パスワードは64文字までです。 もし、ユーザー名を入力したのにパスワードが入力されていない場合は、baresipの起動時に問い合わせます。 リクエストを送るときに、1つか2つSIP URIを使う必要がある。 2つとも入力された場合、両方にREGISTERリクエストが送られ、他のリクエストは応答する方に送られます。 outboundプロキシが与えられない場合、リクエストはcalllee URI hostpartのDNS NAPTR/SRV/Aレコード検索に基づいて送信される。 SIP URIのhostpartがIPv6アドレスの場合、アドレスは括弧[]内に記載しなければなりません。 \n記入例: \n • sip:example.com:5061;transport=tls \n • sip:[2001:67c:223:777::10];transport=tcp \n • sip:192.168.43.50:443;transport=wss チェックを入れると、登録が有効になり、12 分間隔で REGISTER 要求が送信されます。 必要であればメディアのNAT探索プロトコルを選択してください。 選択肢としては、STUN(Session Traversal Utilities for NAT、RFC 5389) とICE(Interactive Connectivity Establishment、RFC 5245)があります。 A STUN/TURN Server URI of form scheme:host[:port], where scheme is \'stun\' or \'turn\'. Factory default STUN Server for STUN and ICE protocols is \'stun:stun.l.google.com:19302\' pointing to public Google STUN server. There is no factory default TURN server. Selects media transport encryption protocol (if any). \n • ZRTP (recommended) means that ZRTP end-to-end media encryption negotiation is tried after the call has been established. \n • DTLS-SRTPF means that UDP/TLS/RTP/SAVPF is offered in outgoing call and that RTP/SAVP, RTP/SAVPF, UDP/TLS/RTP/SAVP, or UDP/TLS/RTP/SAVPF is used if offered in incoming call. \n • SRTP-MANDF means that RTP/SAVPF is offered in outgoing call and required in incoming call. \n • SRTP-MAND means that RTP/SAVP is offered in outgoing call and required in incoming call. \n • SRTP means that RTP/AVP is offered in outgoing call and that RTP/SAVP or RTP/SAVPF is used if offered in incoming call. SIP URI for checking of voicemail messages. If left empty, voicemail messages (Message Waiting Indications) are not subscribed to. If checked, this account is selected when baresip is started. Account\'s port number and transport protocol may be optionally given when a new account is created: username@domain[:port][;transport=udp|tcp|tls]. If port is given and transport protocol is not given, transport protocol defaults to udp. If port is not given and transport protocol is given, port defaults to 5060 or 5061 (TLS). If neither is given and no outbound proxy is specified, account\'s registrar (if any) is determined solely based on domain\'s DNS information. Do you want to add \'%1$s\' to contacts or delete %2$s from call history? Do you want to delete chat with peer \'%1$s\' or add peer to contacts? IP address and port of form \'address:port\' at which baresip listens for incoming SIP requests. If IP address is an IPv6 address, it must be written inside brackets []. IPv4 address 0.0.0.0 or IPv6 address [::] makes baresip listen at all available addresses. If left empty (factory default), baresip listens at port 5060 of all available addresses. Comma separated list of addresses of DNS servers. If not given, DNS server addresses are obtained dynamically from the system. Each DNS address is of form \'ip:port\' or \'ip\'. If port is omitted, it defaults to 53. If ip is an IPv6 address and also port is given, ip must be written inside brackets []. As an example, list \'8.8.8.8:53,[2001:4860:4860::8888]:53\' points to IPv4 and IPv6 addresses of public Google DNS servers. If checked, file \'cert.pem\' containing TLS certificate and private key of this baresip instance has been or will be loaded from Download directory. For security reasons, delete the file after loading. If checked, file \'ca_certs.crt\' containing TLS certificates of Certificate Authorities has been or will be loaded from Download directory. Audio codecs provided by the checked modules are available for use by the accounts. Average maximum bit rate used by Opus audio stream. Valid values are 6000-510000. Factory default is 28000. Expected Opus audio stream packet loss percentage, from 0–100. By default 0, turning off Opus Forward Error Correction (FEC). Size of transmitted video frames (width x height) If checked and if Debug is checked, provides also SIP request and response trace to Logcat. Unchecked automatically at baresip start. You need to restart baresip in order to activate the new settings. Restart now? Baresip failed to start. This may be due to an invalid Settings value. Check Listen Address, TLS Certificate File, and TLS CA File. Then restart baresip. This call is SECURE and peer is VERIFIED! Do you want to unverify the peer? Failed to back up application data to Download folder file \'%1$s\'. Check Apps → baresip → Permissions → Storage. Failed to restore application data from Download folder. Check Apps → baresip → Permissions → Storage and that backup file \'%1$s\' exists in the folder and, if so, you gave correct Decrypt Password. Application data restored. baresip needs to be restarted. Restart now?  %1$s  %2$s を通話履歴から削除してもよいですか?  %1$s のメッセージと連絡先を削除してもよいですか? 無効な認証ユーザー名 %1$s です 転送 テレビ電話リクエスト テレビ電話 着信中 発信中 終了 再起動 詳細 復元 バックアップ 確認 エラー 状態 編集 削除 追加 拒否 承認 いいえ OK キャンセル 情報 メッセージ送信 連絡先 連絡先名 %1$s は既に存在します 無効な連絡先名 %1$s です 名前 新規連絡先 デバック DNSサーバー 自動的に開始 設定 あなた 今日 メッセージの送信に失敗 連絡先の追加 新規メッセージ アカウント %1$s の通話履歴を削除しますか? 有効化 無効化 通話 通話 着信中 通話履歴 転送リクエスト アカウント %1$s を削除しますか? 復号化パスワード 暗号化パスワード 新規アカウント アカウント メディアの暗号化 STUN/TURNパスワード 無効な表示名 %1$s です STUN/TURNユーザー名 Voicemailメッセージ VoicemailのURI 認証ユーザー名 必要であればFrom URIで使用される名前を入力 表示名 アカウント baresip+について baresipについて 無効な表示名 %1$s です 無効な表示名 %1$s です 着信失敗 はい 認証パスワード 無効な認証パスワード %1$s です 発信プレフィクス SIP URIのプロキシサーバー SIP URIの認証プロキシサーバー 無効なプロキシサーバー %1$s です 登録 オーディオコーデック ビデオコーデック メディアのNAT探索 STUN/TURNサーバー 無効なSTUN/TURNサーバーのURI %1$s です 無効なuser@domain[:port][;transport=udp|tcp|tls] %1$s です 応答モード マニュアル 自動 既定のアカウント アカウント %1$s は既に存在します 新しいアカウントの割り当てに失敗しました。  %1$s とチャット メッセージを削除しますか? 失敗 チャット履歴 新しいチャット相手  %1$s とのチャットを削除しますか? アカウント %1$s とのチャット履歴を削除しますか? 受信対象アドレス 受信対象アドレスが無効です DNSサーバーが無効です DNSサーバーの設定に失敗しました TLS証明書ファイル TLS CA証明書ファイル オーディオモジュール モジュールの読み込みに失敗しました Opusのビットレート 期待されるOpusのパケットロス Opusのビットレートが無効です Opusパケットロス率が無効です 既定の通話ボリューム ダークテーマ ビデオフレームサイズ SIP追跡 工場出荷時設定にリセット ダウンロード・ディレクトリから \'cert.pem\' の読み込みに失敗しました。 ダウンロード・ディレクトリから \'ca_certs.crt\' の読み込みに失敗しました。 警告 通知 通話転送 転送先 転送失敗 DTMF 情報はありません 電話情報 通話時間 %1$d コーデック レート: %1$s あなたは 1件のメッセージ 新規メッセージ 1件の古いメッセージ 古いメッセージ メッセージはありません すでに通話中です 確認要求 転送要求 呼び出し失敗 通話終了 この通話は安全ではありません! この通話は安全ですが、相手は確認されていません! 未検証 再起動要求 カメラにビデオ通話の許可を与えます 対応しているビデオカメラがありません STUN/TURN サーバーで必要な場合のユーザー名 STUN/TURN サーバーで必要な場合のパスワード 着信した電話にどのように応答するかを選択します。 チェックを入れると、デバイスの再起動後に自動的にbaresipが起動します 設定されている場合、デフォルトの通話音声の音量は1~10段階です ダークテーマを強制する チェックを入れると、デバッグおよび情報レベルのログメッセージをLogcatに提供します。 チェックを入れると、設定は工場出荷時のデフォルト値にリセットされます。  %1$s に電話をかけるか、メッセージを送信しますか? 連絡先 %1$s を削除しますか?  %1$s でテレビ通話の送受信を許可しますか?  %1$s へのテレビ通話を許可しますか?  %1$s からのテレビ通話を許可しますか? 受信  %1$s への登録失敗 SAS <%1$s> を検証しますか\?  %1$s への通話転送を許可しますか? アプリケーションデータがダウンロードフォルダ %1$s にバックアップされました マイクへの権限付与がなく電話をかけたり、応答したりすることはできません。
================================================ FILE: app/src/main/res/values-ko/strings.xml ================================================ ================================================ FILE: app/src/main/res/values-nb-rNO/strings.xml ================================================ Om Konto Visningsnavn Navn (hvis noe) brukt i skjema-URI for utgående forespørsler. Brukernavn for identitetsbekreftelse Brukernavn for identitetsbekreftelse av SIP-forespørsler er påkrevd. Forvalgt verdi er kontoens brukernavn. Passord for identitetsbekreftelse Passord for identitetsbekreftelse opptil 64 tegn. Hvis det angis, og passord ikke angis, vil det bli anmodet inntastet når baresip startes. Utgående mellomtjenere Lyd-kodeker STUN-tjener Mediakryptering Forvalgt konto Kontoer Ugyldig bruker@domene[:port][;transport=udp|tcp|tls] \"%1$s\" Kontoen \"%1$s\" finnes allerede. Klarte ikke å tildele ny konto. Krypter passord Dekrypter passord Ønsker du å slette kontoen \"%1$s\"\? Anropslogg Ring samtaler samtale Ønsker du å legge til «%1$s» i kontaktlisten, eller slette «%2$s» fra anropsloggen\? Skru av Skru på Sludre med %1$s Ny melding Ønsker du å slette meldingen eller legge til likemannen \"%1$s\" til i kontaktlisten\? Ønsker du å slette meldingen\? Legg til kontakt Meldingsforsendelse mislyktes Sludrehistorikk I dag Deg Ny sludringslikemann Ønsker du å slette samtalen med likemannen \"%1$s\" eller legge vedkommende til i kontaktlisten\? Oppsett Start automatisk Lytteadresse DNS-tjenere Opus-bitrate Tilbakestill til fabrikkforvalg Ny kontakt Navn \"%1$s\" er et ugydlig kontaktnavn Kontakter Send melding Ønsker du å slette kontakten \"%1$s\"\? Varsel Info Merknad Avbryt OK Ja Nei Godta Nekt Legg til Slett Rediger Status Feil Om Avslutt Utgående anrop til … Innkommende anrop fra … Varighet %1$d (sek) Kodeker Du har én ny melding nye meldinger én gammel melding gamle meldinger og Du har ingen meldinger Lytt Bekreft Denne samtalen er IKKE sikker! Denne samtalen er SIKKER, men likemannen er IKKE bekreftet! Denne samtalen er SIKKER, og likemannen er BEKREFTET! Ønsker du å avkrefte likemannen\? Avkreft <h1>Baresip-bibliotek basert på SIP-brukeragent</h1> <p>Juha Heinanen <jh@tutpro.com></p> <p>Version %1$s</p> <h2>Brukshint</h2> <ul> <li>Sjekk at oppsettets forvalgte verdier passer for deg (klikk på elementtitlene for hjelp).</li><li>Registreringstatus for konto vises med en fargelagt prikk: Grønn (vellykket registrering, gul (registrering underveis), rød (mislykket registrering), hvit (registrering har ikke blitt aktivert).</li> <li>Opprett så én eller flere kontoer (igjen, klikk på elementtitlene for hjelp).</li> <li>Likemenn i samtaler og meldinger kan legges til ved lange trykk.</li> <li>Lange trykk kan også brukes til å fjerne samtaler, sludringer, meldinger, og kontakter.</li> <li>Trykk/langt trykk på kontaktikon kan brukes for å installere/fjerne billedavatar.</li> <li>Klikk når ringeren ikke er angitt for å ringe opp forrige samtalepartner.</li> <li>Hvis du ikke kan høre motparten, prøv å øke media-lydstyrken.</li> </ul> <h2>Kjente problemer</h2> <ul> <li>På grunn av en biblioteksbegrensning, støtter ikke baresip flerfoldige samtidige nettverksgrensesnitt.</li> </ul> <h2>Kildekode</h2> Tilgjengelig på <a href=https://github.com/juha-h/baresip-studio>GitHub</a>, der feil også kan innrapporteres. SIP URI-til én eller to mellomtjenere som må brukes ved forsendelse av forespørsler. Hvis to angis, vil REGISTER-forespørsler sendes til begge, og andre forespørsler sendes til den som svarer. Hvis ingen utgående mellomtjener angis, blir forespørsler sent basert på DNS NAPTR/SRV/A-oppføringsoppslag fra ringerens URI-vertspart. Hvis vertsdelen av en SIP URI er en IPv6-adresse, må adressen skrives i hakeparenteser []. \nEksempler: \n • sip:foo.com:5060;transport=tls \n • sip:[2001:67c:223:777::10]:5060;transport=tcp SIP URI for mellomtjeneren SIP URI til en annen mellomtjener Registrer Skrur på registrering og REGISTER-forespørsler blir sendt hvert tolvte minutt. Media NAT-gjennomgang Velger media NAT-gjennomgangsprotokoll (hvis noen). Mulige valg er STUN (øktgjennomgangsverktøy for NAT, RFC 5389) og ICE (interaktivt tilkoblingstilknytning, RFC 5245). En STUN-tjener i utførelsen host[:port]. Forvalgt fabrikkverdi er \'stun.l.google.com:19302\', som peker til offentlig Google STUN-tjener. Brukernavn og passord støttes foreløpig ikke. Velger krypteringsprotokoll for mediatransport (hvis noen). \n • ZRTP (anbefalt) betyr at ZRTP ende-til-ende -forhandling av mediakryptering forsøkes etter at samtalen har blitt opprettet. \n • DTLS-SRTPF betyr at UDP/TLS/RTP/SAVPF tilbys i utgående samtale, og at RTP/SAVP, RTP/SAVPF, UDP/TLS/RTP/SAVP, eller UDP/TLS/RTP/SAVPF brukes hvis tilbudt i innkomende anrop. \n • SRTP-MANDF betyr at RTP/SAVPF tilbys i utgående anrop, og krever et innkommende anrop. \n • SRTP-MAND betyr at RTP/SAVP tilbys i utgående anrop, og kreves i innkommende anrop. \n • SRTP betyr at RTP/AVP tilbys i utgående anrop, og at RTP/SAVP eller RTP/SAVPF brukes hvis tilbudt i innkommende anrop. Telefonsvarer-URI SIP-URI for sjekk av telefonsvarermeldinger. Hvis levnet tom, vil telefonsvarermeldinger (melding venter-anvisninger) ikke bli levert. Velger denne kontoen når baresip startes. Samtaleoverføringsforespørsel til Ønsker du å slette \"%1$s\" %2$s fra anropshistorikk\? Mislyktes Ønsker du å slette sludringen med \"%1$s\"\? Starter baresip automatisk sammen med enheten. IP-adresse og port i utførelsen \"adresse:port\" som baresip lytter til for innkommende SIP-forespørsler. Hvis en IP-adresse er en IPv6-adresse, må den skrives i klammeparenteser []. IPv4-adresse 0.0.0.0 eller IPv6-adresse [::] får baresip til å lytte til alle tilgjengelige adresser. Hvis levnet tom (fabrikksforvalg), vil baresip lytte til port 5060 for alle tilgjengelige adresser. Kommainndelt liste over adresser til DNS-tjenere. Hvis ikke angitt, vil DNS-tjeneradresser hentes dynamisk fra systemet. Hver DNS-adresse er utførelsen \"ip:port\" eller \"ip\". Hvis porten utelates, brukes forvalget 53. Hvis IP-en er en IPv6-adresse, og port også angis, må IP-en skrives i hakeparenteser []. Som etksempel, list \'8.8.8.8:53,[2001:4860:4860::8888]:53\' peker til en IPv4-adresser og IPv6-adresser for offentlige Google DNS-tjenere. Klarte ikke å laste inn modul. Gjennomsnittlig maksimal bitrate ved bruk av Opus-lydstrøm. Gyldig verdi er 6000-510000. Fabrikksforvalg er 28000. Forventet Opus-pakketap Forventet prosentvis pakketap for Opus-lydstrøm. Gyldige verdier fra 0–100. Fabrikksforvalg er 0, som skrur av Opus-fremtidsfeilkorrigering (FEC). Forvalgt samtalelydstyrke Setter samtalelydstyrken på en skala fra 1-10. Feilretting Gjør feilrettings- og infonivå-loggmeldinger tilgjengelige i Logcat. Tilbakestiller oppsettet til fabrikkforvalgte verdier. Kontakten \"%1$s\" finnes allerede. Ønsker du å ringe eller sende melding til \"%1$s\"\? Tonesignalering Samtaleinfo Takt: %1$s Telefonsvarermeldinger Du har allerede et aktivt anrop. Baresip kunne ikke starte. Dette kan ha oppstått på grunn av en ugyldig oppstartsverdi. Sjekk lytteadressen, TLS-sertifikatsfilen, og TLS CA-filen. Start så programmet på ny. Registrering av %1$s mislyktes. Ønsker du å bekrefte SAS <%1$s>\? Anrop mislyktes. Samtale lukket. TLS-sertifikatsfil Hvis valgt vil eller har filen \"cert.pem\" som inneholder TLS-sertifikatet og den private nøkkelen tilhørende denne baresip-instansen blitt lastet inn fra Nedlastinger. Av sikkerhetshensyn må du slette filen etter innlasting. TLS-CA-fil Hvis valgt vil filen \"ca_certs.crt\" inneholdende TLS-sertifikatene tilhørende sertifikatsmyndighetene ha blitt lastet inn fra Nedlastinger. Klarte ikke å lese filen \"cert.pem\" fra Nedlastinger. Klarte ikke å lese filen \"ca_certs.crt\" fra Nedlastinger. Ugyldig lyttadresse Ugyldige DNS-tjenere Klarte ikke å sette DNS-tjenere Ugyldig Opus-bitrate Ugyldig Opus-pakketapsprosent Svarsmodus Angir hvordan innkommende anrop besvares. Manuell Automatisk Programomstart Ønsker du virkelig å slette samtalehistorikk for kontoen «%1$s»\? Ønsker du å slette sludrehistorikken for kontoen «%1$s»\? Ønsker du å starte baresip på ny for å aktivere de nye innstillingene nå\? Sikkerhetskopier Gjenopprett Programdata sikkerhetskopiert til nedlastingsmappen som \'%1$s\'. Klarte ikke å sikkerhetskopiere programdata til nedlastingsmappe som \'%1$s\'. Sjekk \"Programmer → baresip → Tilganger → Lagring. Programdata gjenopprettet. Start baresip på ny nå\? Klarte ikke å sikkerhetskopiere programdata til nedlastingsmappe. Sjekk \"Programmer → baresip → Tilganger → Lagring og at sikkerhetskopifilen \'%1$s\' finnes i mappen, og om det er tilfelle, at du har oppgitt rett dekrypteringspassord. Lydmoduler Lydkodeker tilbudt av de avkryssede modulene er tilgjengelig for bruk av kontoene. Ny konto Du kan ikke utføre eller besvare anrop uten mikrofontilgang. Det er mulig å sette kontoportnummer og transportprotokoll som username@domain[:port][;transport=udp|tcp|tls] ved opprettelse av en ny konto. UDP brukes hvis kun port angis. Port 5060 eller 5061 (TLS) brukes hvis kun transportprotokoll angis. Kontoens registrar (hvis noen) angis kun basert på info fra domenets DNS hvis hverken port, transportprotokoll, og heller ikke utgående mellomtjener angis. Ugyldig mellomtjener-URI \"%1$s\" Ugyldig identitetsbekreftelsespassord \"%1$s\" Ugylidg identitetsbekreftelsesbrukernavn \"%1$s\" Ugyldig visningsnavn \"%1$s\" Ugyldig STUN-tjener \"%1$s\" Ingen info tilgjengelig Du har ikke noen støttede videokameraer. Start forespørsel på ny Videoforespørsel Videosamtale Bekreftelse «%1$s» er et ugyldig passord Passord hvis påkrevd av STUN/TURN-tjener STUN/TURN-passord «%1$s» er et ugyldig brukernavn Brukernavn hvis påkrevd av STUN/TURN-tjener STUN/TURN-brukernavn Videokodek Videresend til Du kan ikke utføre eller svare på videoanrop uten kamera-tilgang. Ønsker du å sette over denne samtalen til «%1$s»\? Kunne ikke videresende anrop Sett over Videresending av anrop Godta videosamtale med «%1$s»\? Om baresip+ Godta mottak av video fra «%1$s»\? Godta forsendelse av video til «%1$s»\? Videorammestørrelse Mørk drakt Tapt anrop fra Tid Varighet Lydinnstillinger ================================================ FILE: app/src/main/res/values-night/colors.xml ================================================ #84C1E6 #ffffff #2E7D32 #F9A825 #C62828 ================================================ FILE: app/src/main/res/values-pl/strings.xml ================================================ ================================================ FILE: app/src/main/res/values-pt/strings.xml ================================================ Os dados da aplicação foram restaurados. O baresip precisa ser reiniciado. Reiniciar agora\? Quer apagar o chat com \'%1$s\'\? Se marcado, baresip iniciar automaticamente depois que o aparelho reiniciar. Quer verificar SAS <%1$s>\? Se estiver marcada, foi ou será carregado um ficheiro que contém certificados TLS de tais autoridades de certificação que não estão incluídas no sistema operativo Android. Na versão 9 do Android, um ficheiro chamado “ca_certs.crt” é carregado da pasta Download. Se selecionado, disponibiliza mensagens de depuração no registo log e informações para o Logcat. Esperada percentagem de perda de pacotes do fluxo de áudio do Opus, de 0-100. Por padrão o valor é 1. O valor 0 desativa o Opus Forward Error Correction (FEC). URI SIP de um ou dois proxies que precisam ser usados para enviar solicitações. Se dois forem informados, requisições de REGISTO serão enviados para ambos e as outras requisições serão enviadas para aquele que respondeu. Se nenhum proxy foi informado, requisições serão enviadas baseadas em DNS NAPTR/SRV/A pesquisa de registo da URI do host do destinatário. Se o host da URI SIP for um endereço IPv6, o endereço precisa ser escrito dentro de colchetes []. \nExemplos: \n • sip:example.com:5061;transport=tls \n • sip:[2001:67c:223:777::10];transport=tcp \n • sip:192.168.43.50:443;transport=wss Enviar Mensagem Falha ao alocar nova conta. Nenhuma informação disponível Recusar Quer apagar o histórico de chamadas da conta \'%1$s\'\? Adicionar Contato Um URI do servidor STUN/TURN na forma de scheme:host[:porta][\?transport=udp|tcp]. Onde o scheme é \'stun\', \'stuns\', \'turn\', ou \'turns\'. O Servidor predefinido de fábrica para o STUN e os protocolos ICE são \'stun:stun.l.google.com:19302\' apontando para o servidor STUN público do Google. Não há qualquer servidor STUN predefinido de fábrica. Baresip falhou ao iniciar. Isso pode ser devido a um valor inválido das Configurações. Verifique Endereço de Escuta, Ficheiro de Certificado TLS e Ficheiro CA TLS. Em seguida, reinicie o baresip. Já tem uma chamada ativa. chamada SIP URI da nova conta no formato: <utilizador>@@<domínio>[:<porta>][;transport=udp|tcp|tls]. Se a <porta> for informada e o protocolo de transporte não, o padrão será UDP. Se a <porta> não for informada e o protocolo de transporte for, a <porta> padrão é 5060 ou 5061 (TLS). Se nenhum dos dois ou nenhum proxy for informado, o utilizador da conta (caso haja) é determinado exclusivamente com base nas informações do DNS do domínio. Palavra-passe STUN/TURN Redefinir os Padrões de Fábrica Aceitar o envio e o recebimento de vídeo com \'%1$s\'\? Chamada de … Adicionar Alerta uma nova mensagem Quer apagar o contato \'%1$s\'\? Volume de Chamadas prefinido Criptografar Palavra-passe SIP URI da nova conta Nome Quer ligar ou enviar mensagem para \'%1$s\'\? Ficheiro TLS CA URI SIP de outro Servidor Proxy Contato \'%1$s\' já existe. URI SIP do Servidor Proxy Cópia de Segurança Módulos de Áudio Transferir Estado A chama é SEGURA, porém o par NÃO é verificado! Desativar Quer apagar a mensagem ou adicionar o ponto \'%1$s\' aos contatos\? URI SIP para verificar mensagens de correio de voz. Se deixado em branco, as mensagens do correio de voz (Indicação de Mensagem em Espera) não serão assinadas. Se marcado, o registo é ativado e as solicitações de REGISTO são enviadas no intervalo determinado pelo Intervalo de registo. Taxa atual: %1$s (Kbits/s) Hoje Servidores DNS Conceda permissão à \"Câmara\" para fazer ou atender chamadas de vídeo. Falha ao definir servidores DNS Palavra-passe, caso seja necessário pelo servidor STUN/TURN Quer apagar o chat com o par \'%1$s\' ou adicionar o par aos contatos\? NAT Transversal de Mídia Mensagens do Correio de Voz Chamada de vídeo Se marcado, esta conta é selecionada quando o baresip for iniciado. Falha ao fazer o backup dos dados da aplicação no ficheiro \'%1$s\'. Verifique Aplicações → baresip → Permissões → Armazenamento. Nome (se houver) usado em From URI de solicitações de saída. A ligação falhou Verificar a Solicitação Novo Contato Codecs de áudio providos pelos módulos selecionados estão disponíveis para uso das contas. mensagens antigas Próxies de Saída Histórico de Chamadas Contatos Falar com %1$s Iniciar Automaticamente usuario@dominio[:porta][;transport=udp|tcp|tls] inválido \'%1$s\' URI do Servidor Proxy Inválido \'%1$s\' Ouvir Aceitar Quer apagar o histórico de chat da conta \'%1$s\'\? Se ajustado, volume de áudio da chamada predefinida na escala de 1 a 10. Contas Nome de contato \'%1$s\' inválido Descriptografar Palavra-passe Nome de Exibição Inválido\'%1$s\' Erro Não Nova mensagem Informações Manual Novo Par de Chat Apagar Quer apagar a mensagem\? Confirmação Precisa reiniciar o baresip para aplicar as novas configurações. Reiniciar agora\? Chamada Codecs de Vídeo Automático Quer adicionar \'%1$s\' aos contatos ou deletar %2$s do histórico de chamadas\? Você Codecs Falha ao carregar módulo. Ligar a … Servidor STUN/TURN Tem Não tem mensagens Duração: %1$d (segs) Informações da Chamada Histórico de Mensagens A chamada é SEGURA e o par é VERIFICADO! Deseja desverificar o par\? Taxa de bits máxima média usada pelo fluxo de áudio do Opus. Os valores válidos são 6000-510000. A predefinição de fábrica é 28000. Solicitação para Reinicialização Registo Restaurar Perda de pacotes Opus esperada Aceita transferir esta chamada para \'%1$s\'\? Cancelar Quer apagar a conta \'%1$s\'\? Pedido de Vídeo e Taxa de Bits Opus Nome do Utilizador STUN/TURN Conta Predefinida OK Falhou A chamada está terminada Taxa de bits Opus inválida Pedido de Transferência Palavra-passe de Autenticação O registo de %1$s falhou. Ficheiro de Certificado TLS Servidores de DNS inválidos Selecione o protocolo de criptografia de transporte de mídia (se houver). \n • ZRTP (recomendado) significa que a negociação de criptografia de mídia ZRTP ponta-a-ponta é tentada depois que a ligação foi estabelecida. \n • DTLS-SRTPF significa que UDP/TLS/RTP/SAVPF é oferecido em chamadas efetuadas que RTP/SAVP, RTP/SAVPF, UDP/TLS/RTP/SAVP, ou UDP/TLS/RTP/SAVPF é usado se oferecida em chamadas recebidas. \n • SRTP-MANDF significa que RTP/SAVPF é oferecido em chamadas efetuadas e requerido em chamadas recebidas. \n • SRTP-MAND significa que RTP/SAVP é oferecido em chamadas efetuadas e requerido em chamadas recebidas. \n • SRTP significa que RTP/AVP é oferecido em chamadas efetuadas e que RTP/SAVP ou RTP/SAVPF é usado se oferecido em chamadas recebidas. novas mensagens Biblioteca Baresip baseado em agente de utilizador SIP

Juha Heinanen <jh@tutpro.com>

Versão %1$s


Dicas de Uso

  • Verifique se os valores padrão nas configurações do baresip atendem às suas necessidades (toque nos títulos dos elementos para obter ajuda).
  • Em seguida, em Contas, crie uma ou mais contas (toque novamente nos títulos dos elementos para obter ajuda).
  • O estado de registo de uma conta é mostrado com um ponto colorido: verde (registo bem-sucedido), amarelo (registo em andamento), vermelho (falha no registo), branco (registo não foi ativado).
  • O toque no ponto leva diretamente à configuração da conta.
  • O gesto de deslizar para baixo causa o novo registo da conta exibida no momento.
  • Um toque longo na conta exibida no momento ativa ou desativa o registo da conta.
  • O participante da chamada anterior pode ser selecionado novamente tocando no ícone de chamada quando Callee estiver vazio.
  • É possível adicionar pares de chamadas e mensagens aos contatos por meio de toques longos.
  • Também é possível usar toques longos para remover chamadas, bate-papos, mensagens e contatos.
  • Toque/toque longo no ícone do contato para instalar/remover o avatar.
  • Para mais informações, consulte Wiki.

Política de privacidade

A política de privacidade está disponível emhere.


Código fonte

O código fonte está disponível no GitHub, onde os problemas encontrados também podem ser reportados.


Licenças

  • BSD-3-Clause exceto os seguintes:
  • Apache 2.0 codecs AMR e segurança TLS
  • AGPLv4 criptografia de média ZRTP
  • GNU LGPL 2.1 G.722, G.726 e codecs Codec2
  • GNU GPLv3 codec G.729
]]>
Tamanho do Quadro do Vídeo Falha ao restaurar os dados da aplicação. Verifique se inseriu a palavra-passe correta e que o ficheiro de backup seja deste aplicação, Nas versões do Android 9 e anteriores, verifique também em Apps → baresip → Permissões → Armazenamento e se o ficheiro \'%1$s\' existe na pasta Download. Reiniciar Nome de Utilizador de Autenticação Nome de utilizador de autenticação se a autenticação de solicitações SIP for necessária. O valor predefinido é o nome de utilizador da conta. Sim Selecione o protocolo de NAT transversal de mídia (se houver). Escolhas possíveis são STUN (Session Traversal Utilities para NAT, RFC 5389) e ICE (Interactive Connectivity Establishment, RFC 5245). Servidor URI STUN/TURN Inválido \'%1$s\' Nome de Utilizador de Autenticação Inválido \'%1$s\' Não possui câmaras de vídeo compatíveis. chamadas Codecs de Áudio Ativar DTMF O baresip precisa de permissão do \"Microfone\" para realizar chamadas de voz. Houve uma falha ao ler o ficheiro \'ca_certs.crt\'. A conta \'%1$s\' já existe. O endereço IP e a porta do formulário \"address:port\" são os endereços onde o baresip escuta as solicitações SIP recebidas. Se o endereço IP for IPv6, deve ser escrito entre colchetes []. O endereço IPv4 0.0.0.0 ou o endereço IPv6 [::] faz com que o baresip escute em todos os endereços disponíveis. Se for deixado em branco (padrão de fábrica), o baresip escuta numa porta arbitrária em todos os endereços disponíveis. Se selecionado, ajustes serão redefinidos para os valores de predefinição de fábrica. Esta chamada NÃO é segura! Houve uma falha ao ler o ficheiro \'cert.pem\'. Criptografia de Mídia Endereço de Escuta Nome de Exibição Sobre Envio de mensagem falhou Endereço de Escuta inválido Nome do utilizador, caso seja necessário pelo servidor STUN/TURN Palavra-passe de Autenticação Inválida \'%1$s\' Palavra-passe de Autenticação com até 64 caracteres. Se o Nome de Utilizador de Autenticação for informado mas a palavra-passe não, ela será solicitada quando o baresip for iniciado. Modo de Atendimento Conta Editar Depuração Percentagem de Perda de Pacotes do Opus inválida Lista de endereço de servidores de DNS separados por vírgula. Endereços de servidores DNS são obtidos dinamicamente pelo sistema. Cada endereço DNS é na forma \'ip:porta\' ou \'ip\'. Se a porta é omitida, a predefinição será 53. Se o ip é um endereço IPv6 e também a porta é fornecida, o ip precisa ser escrito dentro de colchetes []. Por exemplo, escutar \'8.8.8.8:53,[2001:4860:4860::8888]:53\' aponta para endereços IPv4 e IPv6 dos servidores de DNS do Google. Aviso Sobre o baresip Palavra-passe inválida \'%1$s\' uma mensagem antiga Ajustes Selecione como chamadas recebidas são respondidas. Tamanho dos quadros de vídeo que foram transmitidos (largura x altura) URI do Correio de Voz Os dados as aplicações foram armazenados num backup no ficheiro \'%1$s\'. Nas versões do Android 9 e anteriores, o ficheiro está na pasta Download. Sair Desverificar Se marcado, o ficheiro que contém o certificado TLS e a chave privada desta instância do baresip foi ou será carregado. Na versão do Android 9, um ficheiro chamado \'cert.pen\' será lido a partir da pasta Download. Por motivos de segurança, apague o ficheiro após o carregamento. Nome do utilizador inválido \'%1$s\' Quer apagar %1$s\' %2$s do histórico de chamadas\? A transferência falhou Destino da transferência Transferência de chamada Aceitar o recebimento de vídeo a partir do \'%1$s\'\? Aceitar o envio de vídeo para \'%1$s\'\? Se for marcado, este contato será adicionado nos contatos do Android. Se selecionado em conjunto com Debug, as mensagens Logcat incluem também o pedido SIP e o rastreamento da resposta. A opção não é ativada automaticamente durante a inicialização do baresip. Rastreio SIP Impor o uso do tema escuro no ecrã Tema Escuro Se estiver marcado, o baresip verifica os certificados TLS do SIP User Agent e o SIP Proxy Servers quando o transporte TLS for usado. Verifique os certificados do servidor Chamada de solicitação de transferência para Ligação perdida de Solicitações INFO SIP Eventos na Banda RTP Selecione como os tons DTMF 0–9, #, * e A-D serão enviados. Modo DTMF Biblioteca Baresip baseado em agente de utilizador SIP com vídeo chamadas

Juha Heinanen <jh@tutpro.com>

Versão %1$s

Dicas de uso

  • Verifique se os valores padrão nas configurações do baresip+\ atendem às as suas necessidades (toque nos títulos dos elementos para obter ajuda).
  • Então em Contas, crie uma ou mais contas (novamente clique nos títulos para ajuda).
  • O estado de uma conta é mostrado com um ponto colorido: verde (registo bem sucedido), amarelo (registo em progresso), vermelho (registo falhou), branco (registo não foi ativado).
  • Toque nos três pontos para ir direto para a configuração da conta.
  • O gesto de deslizar para baixo causa o novo registo da conta exibida no momento.
  • Um toque longo na conta exibida no momento ativa ou desativa o registo da conta.
  • Gestos para a esquerda/direita alternam as contas.
  • O participante da chamada anterior pode ser selecionado novamente tocando no ícone de chamada quando o "Callee" estiver vazio.
  • Os pares de chamadas e mensagens podem ser adicionados aos contatos por meio de toques longos.
  • Os toques longos também podem ser usados para remover chamadas, bate-papos, mensagens e contatos.
  • Toque/toque longo no ícone do contato pode ser usado para instalar/remover o avatar da imagem.
  • Consulte a Wiki para obter mais informações.

Problemas conhecidos

  • Nas chamadas com vídeo, o dispositivo precisa ser mantido no modo paisagem, girado 90 graus para a esquerda em relação à orientação retrato.
  • A própia visualização automática não é exibida corretamente quando o fluxo de vídeo é somente envio.

Política de privacidade

A política de privacidade está disponível aqui.

Código fonte

O código fonte está disponível no GitHub, onde os problemas encontrados também podem ser reportados.

Licenças

  • Cláusula BSD-3 exceto os seguintes:
  • Apache 2.0, codecs AMR e segurança TLS
  • AGPLv4, criptografia de média ZRTP
  • GNU LGPL 2.1 G.722, G.726 e codecs Codec2
  • GNU GPLv3 codec G.729
  • GNU GPLv2 codecs H.264 e H.265
  • AOMedia codec AV1
]]>
Sobre o baresip+ Otimizações da bateria Chamadas perdidas %1$d chamadas perdidas Desative as otimizações da bateria (recomendado) caso queira reduzir a probabilidade do Android restringir o acesso do baresip à rede ou ponha o baresip no modo de espera. Tem certeza de que deseja redefinir as configurações para os valores predefinidos de fábrica\? Redefinir Imagem do perfil Pacotes Perda Variação: %1$s (ms) Não pode restaurar o backup sem a permissão de \"Armazenamento\". Chamada em espera Nenhuma conexão de rede! Taxa média: %1$s (Kbits/s) Não pode criar backups sem a permissão de \"Armazenamento\". Configurações de áudio Par Direção Detalhes da chamada Hora Duração Parte do host SIP URI usada em chamadas para números de telefone. O padrão de fábrica é o domínio da conta. Se não for fornecida, esta conta não pode ser usada para ligar para números de telefone. Você não pode acessar os contatos do Android sem a permissão \"Contatos\". SIP ou tel URI usuário@domínio ou número de telefone Escolhe se serão usados os contatos do baresip, os contatos do Android ou ambos. Se ambos forem usados e houver um contato com o mesmo nome em ambos os contatos, o contato baresip será escolhido. Ambos SIP ou tel URI \'%1$s\' inválido Cego Participou Provedor de telefonia Parte do host URI SIP inválida \'%1$s\' Intervalo de registo Intervalo de registo inválido\'%1$s\' Se marcada, os pacotes RTP e RTCP são multiplexados numa única porta (RFC 5761). Multiplexagem RTCP Código do país A conta \'%1$s\' não tem provedor telefonico Não pode usar esta aplicação sem a permissão \"Notificações\". Foco de áudio negado! Código do país inválido \'%1$s\' Desviado por … Escolha a URI de destino Anônimo Desconhecido Solicitação de consentimento O código do país E.164 desta conta. Se durante o recebimento da chamada ou mensagem do utilizador a parte URI recebida tiver um número de telefone que não começa com o sinal \'+\' e se a busca de contato falhar, o número será prefixado com este código do país e a busca pelo contato será testada novamente. Se o número de telefone começar com um único dígito \'0\', o dígito \'0\' será removido antes que o número seja prefixado. Apelido (se houver) é utilizado para identificar essa conta dentro da app baresip. Apelido inválido da conta \'%1$s\' O apelido \'%1$s\' já existe Apelido Informa com que frequência (em segundos) o baresip envia solicitações REGISTER. Os valores válidos são entre 60 a 3600. Se os contatos so android forem escolhidos, eles podem ser usados nas chamadas e nas mensagens como referências ao SIP e so tel das URIs. a app baresip não armazena os contatos do Android nem os compartilha com ninguém. Para disponibilizar os contatos android no baresip, o Google exige que aceite o uso como descrito aqui e na Política de Privacidade. Justificativa das permissões O baresip precisa da permissão \"Microfone\" para realizar chamadas de voz, da permissão \"Aparelhos próximos\" para fazer a detecção do microfone/alto-falante Bluetooth e a permissão \"Notificações\" para postar notificações. O baresip+ precisa da permissão \"Microfone\" para realizar chamadas de voz, da permissão \"Câmara\" para realizar chamadas de vídeo. da permissão \"Aparelhos próximos\" para fazer a detecção do microfone/alto-falante Bluetooth caso queira usar tal aparelho, assim como a permissão \"Notificações\" para postar notificações. Família de endereço Escolhe quais endereços IP o baresip usa. Se IPv4 ou IPv6 for escolhido, o baresip usa apenas endereços de IPv4 ou IPv6. Se nenhum for escolhido, o baresip usará endereços de IPv4 e IPv6. A gravação só pode ser ativada ou desativada quando não estiver numa chamada Modo de redirecionamento Chamada rejeitada automaticamente de %1$s Aplicação de telefone padrão Se for marcado, o baresip será a aplicaöäao de telefone padrão. Não marque caso o seu dispositivo também precise lidar com outras chamadas ou mensagens SIP. Favorito Redirecionamento automático para \'%1$s\'\\ Tempo (em milissegundos) para o áudio esperar de quem chama quando a chamada for estabelecida. Defina um valor mais alto se perde o áudio de quem chama no início da chamada. Atraso do áudio Atraso inválido de áudio \'%1$s\'. Os valores válidos são entre 100 a 3000. A inicialização automática precisa aparecer no topo das permissões. Solicitação de redirecionamento Aceita o redirecionamento de chamadas para \'%1$s\'? Tom do país Toque da chamada do país, espera e tom de chamada ocupada Respostas provisórias confiáveis Se for marcado, indica suporte para respostas provisórias confiáveis (RFC 3262). Pedido de chamada Aceita o pedido de chamada para \'%1$s\'? Quadros de vídeo por segundo Multiplique o volume do microfone por este número decimal. O valor mínimo é 1,0 (padrão de fábrica), que desativa o ganho do microfone. Valores maiores podem afetar negativamente a qualidade do áudio. Taxa de quadros de vídeo que será oferecida durante o handshake SDP. Os valores válidos são de 10 a 30. Quadros por segundo inválidos \'%1$d\' Valor do campo de cabeçalho User-Agent personalizado da solicitação/resposta SIP Valor do campo do cabeçalho User-Agent inválido Seleciona caso a solicitação de redirecionamento da chamada seja seguida automaticamente ou se caso uma confirmação seja solicitada. Alto-falante do telefone Se estiver marcada, o alto-falante do telefone é ligado automaticamente quando a chamada for iniciada. Valor inválido do ganho do microfone Agente do utilizador Ganho do microfone Se estiver marcada, o contacto será exibido entre os favoritos na parte superior da lista. INFO RTP ou SIP em banda A função de discador não está disponível Falha ao restaurar os dados da aplicação. A versão 14 e superior do Android não permite a restauração de dados cujo backup foi feito %1$s antes da versão %2$s.
================================================ FILE: app/src/main/res/values-pt-rBR/strings.xml ================================================ Codecs Status DTMF Cópia de Segurança O baresip precisa de permissão do \"Microfone\" para realizar chamadas de voz. Falha ao restaurar os dados do aplicativo. Verifique se inseriu a senha correta e que o arquivo de backup seja deste aplicativo, Na versão 9 do Android, verifique também em Aplicativos → baresip → Permissões → Armazenamento e se o arquivo \'%1$s\' existe na pasta Download. Os dados do aplicativo foram restaurados. O baresip precisa ser reiniciado. Reiniciar agora\? Falha ao fazer o backup dos dados do aplicativo no arquivo \'%1$s\'. Verifique Aplicativos → baresip → Permissões → Armazenamento. Os dados do aplicativo (excluindo gravações) em um backup no arquivo \'%1$s\'. Na versão do 9 Android, o arquivo está na pasta Download. Desverificar A chamada é SEGURA e o par é VERIFICADO! Deseja desverificar o par\? A chama é SEGURA, porém o par NÃO é verificado! Esta chamada NÃO é segura! A chamada está encerrada A ligação falhou Você quer verificar SAS <%1$s>\? Verificar a Solicitação O registro de %1$s falhou. Baresip falhou ao iniciar. Isso pode ser devido a um valor inválido das Configurações. Verifique Endereço de Escuta, Arquivo de Certificado TLS e Arquivo CA TLS. Em seguida, reinicie o baresip. Você já tem uma chamada ativa. Ouvir Você não tem mensagens e mensagens antigas uma mensagem antiga novas mensagens uma nova mensagem Você tem Mensagens do Correio de Voz Taxa atual: %1$s (Kbits/s) Duração: %1$d (segs) Nenhuma informação disponível Informações da Chamada Chamada de … Chamando … Sair Reiniciar Sobre Restaurar Erro Editar Excluir Adicionar Recusar Aceitar Não Sim OK Cancelar Aviso Informações Alerta Você quer excluir o contato \'%1$s\'\? Enviar Mensagem Você quer ligar ou enviar mensagem para \'%1$s\'\? Contatos Contato \'%1$s\' já existe. Nome de contato \'%1$s\' inválido Nome Novo Contato Você precisa reiniciar o baresip para aplicar as novas configurações. Reiniciar agora\? Houve uma falha ao ler o arquivo \'ca_certs.crt\'. Houve uma falha ao ler o arquivo \'cert.pem\'. Se selecionado, ajustes serão redefinidos para os valores de padrão de fábrica. Redefinir os Padrões de Fábrica Depuração Se selecionado, disponibiliza mensagens de depuração no registro log e informações para o Logcat. Se ajustado, volume de áudio da chamada padrão na escala de 1 a 10. Volume de Chamadas Padrão Porcentagem de Perda de Pacotes do Opus inválida Taxa de bits Opus inválida Esperada porcentagem de perda de pacotes do fluxo de áudio do Opus, de 0-100. Por padrão o valor é 1. O valor 0 desativa o Opus Forward Error Correction (FEC). Perda de pacotes Opus esperada Taxa de bits máxima média usada pelo fluxo de áudio do Opus. Os valores válidos são 6000-510000. O padrão de fábrica é 28000. Taxa de Bits Opus Falha ao carregar módulo. Codecs de áudio providos pelos módulos selecionados estão disponíveis para uso das contas. Módulos de Áudio Se estiver marcada, foi ou será carregado um arquivo que contém certificados TLS de tais autoridades de certificação que não estão incluídas no sistema operacional Android. Na versão 9 do Android, um arquivo chamado “ca_certs.crt” é carregado da pasta Download. Arquivo TLS CA Se marcado, o arquivo que contém o certificado TLS e a chave privada desta instância do baresip foi ou será carregado. Na versão do Android 9, um arquivo chamado \'cert.pen\' será lido a partir da pasta Download. Por motivos de segurança, exclua o arquivo após o carregamento. Arquivo de Certificado TLS Falha ao definir servidores DNS Servidores de DNS inválidos Lista de endereço de servidores de DNS separados por vírgula. Endereços de servidores DNS são obtidos dinamicamente pelo sistema. Cada endereço DNS é na forma \'ip:porta\' ou \'ip\'. Se a porta é omitida, o padrão será 53. Se o ip é um endereço IPv6 e também a porta é fornecida, o ip precisa ser escrito dentro de colchetes []. Por exemplo, escutar \'8.8.8.8:53,[2001:4860:4860::8888]:53\' aponta para endereços IPv4 e IPv6 dos servidores de DNS do Google. Servidores DNS Endereço de Escuta inválido O endereço IP e a porta do formulário \"address:port\" são os endereços onde o baresip escuta as solicitações SIP recebidas. Se o endereço IP for IPv6, deve ser escrito entre colchetes []. O endereço IPv4 0.0.0.0 ou o endereço IPv6 [::] faz com que o baresip escute em todos os endereços disponíveis. Se for deixado em branco (padrão de fábrica), o baresip escuta em uma porta arbitrária em todos os endereços disponíveis. Endereço de Escuta Se marcado, baresip iniciar automaticamente depois que o dispositivo reiniciar. Iniciar Automaticamente Ajustes Você quer excluir o histórico de chat da conta \'%1$s\'\? Você quer excluir o chat com \'%1$s\'\? Você quer excluir o chat com o par \'%1$s\' ou adicionar o par aos contatos\? Novo Par de Chat Você Hoje Histórico de Mensagens Falhou Envio de mensagem falhou Adicionar Contato Você quer excluir a mensagem\? Você quer excluir a mensagem ou adicionar o ponto \'%1$s\' aos contatos\? Nova mensagem Falar com %1$s Você quer excluir o histórico de chamadas da conta \'%1$s\'\? Habilitar Desabilitar Você quer apagar %1$s\' %2$s do histórico de chamadas\? Você quer adicionar \'%1$s\' aos contatos ou deletar %2$s do histórico de chamadas\? chamada chamadas Chamada Histórico de Chamadas Pedido de Transferência Você quer excluir a conta \'%1$s\'\? Descriptografar Senha Criptografar Senha Houve uma falha ao alocar a nova conta. A conta \'%1$s\' já existe. usuario@dominio[:porta][;transport=udp|tcp|tls] inválido \'%1$s\' SIP URI da nova conta no formato: <usuário>@@<domínio>[:<porta>][;transport=udp|tcp|tls]. Se a <porta> for informada e o protocolo de transporte não, o padrão será UDP. Se a <porta> não for informada e o protocolo de transporte for, a <porta> padrão é 5060 ou 5061 (TLS). Se nenhum dos dois ou nenhum proxy for informado, o usuário da conta (caso haja) é determinado exclusivamente com base nas informações do DNS do domínio. SIP URI da nova conta Contas Se marcado, esta conta é selecionada quando o baresip for iniciado. Conta Padrão URI SIP para checar mensagens de correio de voz. Se deixado em branco, as mensagens do correio de voz (Indicação de Mensagem em Espera) não serão assinadas. URI do Correio de Voz Automático Manual Selecione como chamadas recebidas são respondidas. Modo de Atendimento Selecione o protocolo de criptografia de transporte de mídia (se houver). \n • ZRTP (recomendado) significa que a negociação de criptografia de mídia ZRTP ponta-a-ponta é tentada depois que a ligação foi estabelecida. \n • DTLS-SRTPF significa que UDP/TLS/RTP/SAVPF é oferecido em chamadas efetuadas que RTP/SAVP, RTP/SAVPF, UDP/TLS/RTP/SAVP, ou UDP/TLS/RTP/SAVPF é usado se oferecida em chamadas recebidas. \n • SRTP-MANDF significa que RTP/SAVPF é oferecido em chamadas efetuadas e requerido em chamadas recebidas. \n • SRTP-MAND significa que RTP/SAVP é oferecido em chamadas efetuadas e requerido em chamadas recebidas. \n • SRTP significa que RTP/AVP é oferecido em chamadas efetuadas e que RTP/SAVP ou RTP/SAVPF é usado se oferecido em chamadas recebidas. Criptografia de Mídia Servidor URI STUN/TURN Inválido \'%1$s\' Um URI do servidor STUN/TURN na forma de scheme:host[:porta][\?transport=udp|tcp]. Onde o scheme é \'stun\', \'stuns\', \'turn\', ou \'turns\'. O Servidor predefinido de fábrica para o STUN e os protocolos ICE são \'stun:stun.l.google.com:19302\' apontando para o servidor STUN público do Google. Não há qualquer servidor STUN predefinido de fábrica. Servidor STUN/TURN Selecione o protocolo de NAT transversal de mídia (se houver). Escolhas possíveis são STUN (Session Traversal Utilities para NAT, RFC 5389) e ICE (Interactive Connectivity Establishment, RFC 5245). NAT Transversal de Mídia Codecs de Áudio Se marcado, o registro é ativado e as solicitações de REGISTRO são enviadas no intervalo determinado pelo Intervalo de registro. Registro URI do Servidor Proxy Inválido \'%1$s\' URI SIP de outro Servidor Proxy URI SIP do Servidor Proxy URI SIP de um ou dois proxies que precisam ser usados para enviar solicitações. Se dois forem informados, requisições de REGISTRO serão enviados para ambos e as outras requisições serão enviadas para aquele que respondeu. Se nenhum proxy foi informado, requisições serão enviadas baseadas em DNS NAPTR/SRV/A pesquisa de registro da URI do host do destinatário. Se o host da URI SIP for um endereço IPv6, o endereço precisa ser escrito dentro de colchetes []. \nExemplos: \n • sip:example.com:5061;transport=tls \n • sip:[2001:67c:223:777::10];transport=tcp \n • sip:192.168.43.50:443;transport=wss Próxies de Saída Senha de Autenticação Inválida \'%1$s\' Senha de Autenticação com até 64 caracteres. Se o Nome de Usuário de Autenticação for informado mas a senha não, ela será solicitada quando o baresip for iniciado. Senha de Autenticação Nome de Usuário de Autenticação Inválido \'%1$s\' Nome de usuário de autenticação se a autenticação de solicitações SIP for necessária. O valor padrão é o nome de usuário da conta. Nome de Usuário de Autenticação Nome de Exibição Inválido\'%1$s\' Nome (se houver) usado em From URI de solicitações de saída. Nome de Exibição Conta Biblioteca Baresip baseado em agente de usuário SIP

Juha Heinanen <jh@tutpro.com>

Versão %1$s


Dicas de Uso

  • Verifique se os valores padrão nas configurações do baresip atendem às suas necessidades (toque nos títulos dos itens para obter ajuda).
  • Em seguida, em Contas, crie uma ou mais contas (toque novamente nos títulos dos itens para obter ajuda).
  • O status de registro de uma conta é mostrado com um ponto colorido: verde (registro bem-sucedido), amarelo (registro em andamento), vermelho (falha no registro), branco (registro não foi ativado).
  • O toque no ponto leva diretamente à configuração da conta.
  • O gesto de deslizar para baixo causa o novo registro da conta exibida no momento.
  • Um toque longo na conta exibida no momento ativa ou desativa o registro da conta.
  • O participante da chamada anterior pode ser selecionado novamente tocando no ícone de chamada quando Callee estiver vazio.
  • É possível adicionar pares de chamadas e mensagens aos contatos por meio de toques longos.
  • Também é possível usar toques longos para remover chamadas, bate-papos, mensagens e contatos.
  • Toque/toque longo no ícone do contato para instalar/remover o avatar.
  • Para mais informações, consulte Wiki.

Política de privacidade

A política de privacidade está disponível emhere.


Código fonte

O código fonte está disponível no GitHub, onde os problemas encontrados também podem ser reportados.


Licenças

  • BSD-3-Clause exceto os seguintes:
  • Apache 2.0 codecs AMR e segurança TLS
  • AGPLv4 criptografia de mídia ZRTP
  • GNU LGPL 2.1 G.722, G.726, e codecs Codec2
  • GNU GPLv3 codec G.729
]]>
Sobre o baresip Você não possui câmeras de vídeo compatíveis. Conceda permissão à \"Câmera\" para fazer ou atender chamadas de vídeo. Solicitação para Reinicialização Transferir Aceitar o envio e o recebimento de vídeo com \'%1$s\'\? Pedido de Vídeo Chamada de vídeo Confirmação Tamanho dos quadros de vídeo que foram transmitidos (largura x altura) Tamanho do Quadro do Vídeo Senha inválida \'%1$s\' Senha, caso seja necessário pelo servidor STUN/TURN Senha STUN/TURN Nome do usuário inválido \'%1$s\' Nome do usuário, caso seja necessário pelo servidor STUN/TURN Nome do Usuário STUN/TURN Codecs de Vídeo Você aceita transferir esta chamada para \'%1$s\'\? A transferência falhou Destino da transferência Transferência de chamada Chamada de solicitação de transferência para Biblioteca Baresip baseado em agente de usuário SIP com vídeo chamadas

Juha Heinanen <jh@tutpro.com>

Versão %1$s

Dicas de uso

  • Verifique se os valores padrão nas configurações do baresip+\ atendem às suas necessidades (toque nos títulos dos itens para obter ajuda).
  • Então em Contas, crie uma ou mais contas (novamente clique nos títulos para ajuda).
  • O estado de uma conta é mostrado com um ponto colorido: verde (registro bem sucedido), amarelo (registro em progresso), vermelho (registro falhou), branco (registro não foi ativado).
  • Toque nos três pontos para ir direto para a configuração da conta.
  • O gesto de deslizar para baixo causa o novo registro da conta exibida no momento.
  • Um toque longo na conta exibida no momento ativa ou desativa o registro da conta.
  • Gestos para a esquerda/direita alternam as contas.
  • O participante da chamada anterior pode ser selecionado novamente tocando no ícone de chamada quando o "Callee" estiver vazio.
  • Os pares de chamadas e mensagens podem ser adicionados aos contatos por meio de toques longos.
  • Os toques longos também podem ser usados para remover chamadas, bate-papos, mensagens e contatos.
  • Toque/toque longo no ícone do contato pode ser usado para instalar/remover o avatar da imagem.
  • Consulte a Wiki para obter mais informações.

Problemas conhecidos

  • Nas chamadas com vídeo, o dispositivo precisa ser mantido no modo paisagem, girado 90 graus para a esquerda em relação à orientação retrato.
  • A própia visualização automática não é exibida corretamente quando o fluxo de vídeo é somente envio.

Política de privacidade

A política de privacidade está disponível aqui.

Código fonte

O código fonte está disponível no GitHub, onde os problemas encontrados também podem ser reportados.

Licenças

  • Cláusula BSD-3 exceto os seguintes:
  • Apache 2.0, codecs AMR e segurança TLS
  • AGPLv4, criptografia de mídia ZRTP
  • GNU LGPL 2.1 G.722, G.726, e codecs Codec2
  • GNU GPLv3 codec G.729
  • GNU GPLv2 codecs H.264 e H.265
  • AOMedia codec AV1
]]>
Sobre o baresip+ Se selecionado em conjunto com Debug, as mensagens Logcat incluem também o pedido SIP e o rastreamento da resposta. A opção não é ativada automaticamente durante a inicialização do baresip. Rastreio SIP Aceitar o recebimento de vídeo a partir do \'%1$s\'\? Aceitar o envio de vídeo para \'%1$s\'\? Ligação perdida de Impor o uso do tema escuro na tela Tema Escuro Se for marcado, este contato será adicionado nos contatos do Android. Se estiver marcado, o baresip verifica os certificados TLS do SIP User Agent e o SIP Proxy Servers quando o transporte TLS for usado. Verifique os certificados do servidor Solicitações INFO SIP Eventos na Banda RTP Selecione como os tons DTMF 0–9, #, *, e A-D serão enviados. Modo DTMF Redefinir Tem certeza de que deseja redefinir as configurações para os valores predefinidos de fábrica\? Nenhuma conexão de rede! %1$d chamadas perdidas Chamadas perdidas Imagem do perfil Você não pode restaurar o backup sem a permissão de \"Armazenamento\". Você não pode criar backups sem a permissão de \"Armazenamento\". Otimizações da bateria Desative as otimizações da bateria (recomendado) caso queira reduzir a probabilidade do Android restringir o acesso do baresip à rede ou coloque o baresip no modo de espera. Perda Taxa média: %1$s (Kbits/s) Pacotes Chamada em espera Variação: %1$s (ms) Configurações do áudio Duração Par Detalhes da chamada Direção Hora Operadora de telefonia Parte inválida do host SIP URI \'%1$s\' SIP ou tel. URI usuário@domínio ou o número de telefone SIP URI host é parcialmente usado nas chamadas para números de telefone. O padrão de fábrica é domínio da conta. Caso não seja informada, essa conta não pode ser usada para ligar para os números de telefone. SIP ou tel. URI inválido \'%1$s\' Escolhe se serão usados os contatos do baresip, os contatos do Android ou ambos. Se ambos forem usados e houver um contato com o mesmo nome em ambos os contatos, o contato baresip será escolhido. Ambos Você não pode acessar os contatos do Android sem a permissão \"Contatos\". Cego Participou Anônimo Desconhecido Desviado por … Solicitação de consentimento Se os contatos do Android forem escolhidos, eles podem ser usados em chamadas e mensagens como referências a URIs SIP e tel. O aplicativo baresip não armazena contatos do Android nem os compartilha com ninguém. Para tornar os contatos do Android disponíveis no baresip, o Google exige que você aceite seu uso conforme descrito aqui e na política de privacidade. Código do país O código do país E.164 desta conta. Se durante o recebimento da chamada ou mensagem do usuário a parte URI recebida tiver um número de telefone que não começa com o sinal \'+\' e se a busca de contato falhar, o número será prefixado com este código do país e a busca pelo contato será testada novamente. Se o número de telefone começar com um único dígito \'0\', o dígito \'0\' será removido antes que o número seja prefixado. Código do país inválido \'%1$s\' Apelido (se houver) é utilizado para identificar essa conta dentro do aplicativo baresip. Apelido inválido da conta \'%1$s\' O apelido \'%1$s\' já existe Apelido Multiplexagem RTCP Se marcada, os pacotes RTP e RTCP são multiplexados numa única porta (RFC 5761). Você não pode usar este aplicativo sem a permissão \"Notificações\". Foco de áudio negado! Justificativa das permissões O baresip precisa da permissão \"Microfone\" para realizar chamadas de voz, da permissão \"Dispositivos próximos\" para fazer a detecção do microfone/alto-falante Bluetooth e a permissão \"Notificações\" para postar notificações. O baresip+ precisa da permissão \"Microfone\" para realizar chamadas de voz, da permissão \"Câmera\" para realizar chamadas de vídeo. da permissão \"Dispositivos próximos\" para fazer a detecção do microfone/alto-falante Bluetooth caso queira usar tal dispositivo, assim como a permissão \"Notificações\" para postar notificações. Intervalo de registro Intervalo de registro inválido \'%1$s\' Informa com que frequência (em segundos) o baresip envia solicitações REGISTER. Os valores válidos são entre 60 a 3600. A conta \'%1$s\' não tem provedor telefonico Escolha a URI de destino A gravação só pode ser ativada ou desativada quando não estiver em chamada Família de endereço Escolhe quais endereços IP o baresip está usando. Se for escolhido IPv4 ou IPv6, o baresip usa apenas endereços IPv4 ou IPv6. Se nenhum for escolhido, o baresip usará os endereços IPv4 e IPv6. Atraso do áudio Tempo (em milissegundos) para esperar o áudio de quem chama quando a chamada for estabelecida. Defina um valor mais alto caso perca o áudio de quem chama no início da chamada. Atraso inválido de áudio \'%1$s\'. Os valores válidos ficam entre 100 a 3000. Chamada rejeitada automaticamente de %1$s Aplicativo de telefone padrão A função de discador não está disponível Se marcado, o baresip será o aplicativo de telefone padrão. Não marque caso o seu dispositivo também precise lidar com outras chamadas ou mensagens SIP. Modo de redirecionamento Solicitação de redirecionamento Redirecionamento automático para \'%1$s\'\\ Você aceita o redirecionamento de chamada para \'%1$s\'\? Seleciona caso a solicitação de redirecionamento da chamada seja seguida automaticamente ou se caso uma confirmação seja solicitada. Tom do país Toque da chamada do país, espera e tom de chamada ocupada Se marcado, indica suporte para respostas provisórias confiáveis (RFC 3262). Respostas provisórias confiáveis Favorito A inicialização automática precisa aparecer no top das permissões. Falha ao restaurar os dados do aplicativo. A versão 14 e superior do Android não permite a restauração de dados cujo backup foi feito %1$s antes da versão %2$s. INFO RTP ou SIP em banda Alto-falante do telefone Se marcada, o viva-voz do telefone é ligado automaticamente quando a chamada for iniciada. Quadros por segundo inválidos \'%1$d\' Quadros de vídeo por segundo Taxa de quadros de vídeo que será oferecida durante o handshake SDP. Os valores válidos são de 10 a 30. Pedido de chamada Aceita o pedido de chamada para \'%1$s\'? Agente do usuário Valor do campo de cabeçalho User-Agent personalizado da solicitação/resposta SIP Valor do campo do cabeçalho User-Agent inválido Valor inválido do ganho do microfone Ganho do microfone Multiplique o volume do microfone por esse número decimal. O valor mínimo é 1,0 (padrão de fábrica), que desativa o ganho do microfone. Valores maiores podem afetar negativamente a qualidade do áudio. Se marcada, o contato será exibido entre os favoritos na parte superior da lista. Verificar Origem
================================================ FILE: app/src/main/res/values-ro/strings.xml ================================================ Despre baresip Cont Nume afișat Nume (dacă este cazul) utilizat în adresa solicitărilor de conectare. Nume utilizator de autentificare Numele de utilizator dacă este necesar pentru proxy de ieșire. Parolă de autentificare Parola de autentificare dacă este necesar pentru proxy de ieșire. Proxy-uri de ieșire Adresă SIP server proxy Adresa SIP a altui server proxy Înregistrare Codec-uri audio Traversare NAT pentru media Server STUN Criptare media Adresă căsuță vocală Cont implicit Dacă este bifată, acest cont este selectat atunci când baresip este pornit. Conturi utilizator@domeniu[:port][;transport=udp|tcp|tls] \'%1$s\' este invalid Contul \'%1$s\' exista deja. Nu s-a putut aloca un cont nou. Parolă criptare Parolă decriptare Doriți să ștergeți contul \'%1$s\'\? Cerere transfer apel către Istoric apeluri Apel apeluri apel Doriți să adăugați \'%1$s\' la contacte sau să ștergeți %2$s din istoricul apelurilor\? Doriți să ștergeți \'%1$s\' %2$s din istoricul apelurilor\? Discuție cu %1$s Mesaj nou Doriți să ștergeți mesajul sau să adăugați \'%1$s\' la contacte\? Doriți să ștergeți mesajul\? Adaugă contact Trimiterea mesajului a eșuat Eșuat Istoric discuție Azi Tu Discuție cu un contact nou Doriți să ștergeți discuția cu \'%1$s\' sau să adăugați la contacte\? Doriți să ștergeți discuția cu \'%1$s\'\? Configurație Pornește automat Dacă este bifată, baresip pornește automat după (re)pornirea dispozitivului. Adresă de conectare Servere DNS Rată de eșantionare Opus Volum implicit de apel Dacă este setat, volumul audio de apel implicit între 1–10. Depanare Mesajele de depanarea și informare sunt scrise în jurnalul dispozitivului. Resetare la setările inițiale Dacă este bifată, configurația este resetară la valorile implicite. Contact nou Nume Nume de contact invalid \'%1$s\' Contactul \'%1$s\' deja există. Contacte Doriți să apelați sau să trimiteți un mesaj către \'%1$s\'\? Trimite mesaj Doriți să ștergeți contactul \'%1$s\'\? Alertă Informații Notificări Anulare Bine Da Nu Acceptă Refuză Adaugă Șterge Editare Stare Eroare Despre Ieșire Apel efectuat către … Apel primit de la … DTMF Informații apel Durată %1$d Codec-uri Rată: %1$s Mesaje căsuță vocală Aveți un mesaj nou mesaje noi un mesaj vechi mesaje vechi și Nu aveți mesaje Ascultă Aveți deja un apel activ. Înregistrarea %1$s a eșuat. Verificare Doriți să verificați SAS <%1$s>\? Acceptați să transferați apelul către \'%1$s\'\? Apelul a eșuat. Apel închis. Acest apel NU este securizat! Acest apel este SECURIZAT, dar contactul NU este verificat! Acest apel este SECURIZAT și contactul este VERIFICAT! Doriți să anulați verificarea\? Anulare verificare Se activeaza înregistrarea și cererile REGISTER vor fi trimise la fiecare 12 minute. Selectare protocol traversare NAT (dacă există). Posibile opțiuni: STUN (Session Traversal Utilities for NAT, RFC 5389) și ICE (Interactive Connectivity Establishment, RFC 5245). Un server STUN este de forma gazdă[:port]. Setarea implicită este \'stun.l.google.com:19302\', ce folosește un server public STUN găzduit de Google. Momentan nu se poate seta un nume de utilizator și parolă. Adresă SIP pentru verificarea mesajelor din căsuța vocală. Dacă nu este completată, nu se va face abonarea (Message Waiting Indications) la căsuța vocală. Baresip nu a putut porni! Aceasta se poate datora unei setări invalide. Verificați adresa de ascultare, fișierul certificat TLS și fișierul TLS al CA. Apoi reporniți baresip. Fișier certificat TLS Fișier CA TLS Nu s-a putut citi fișierul \'cert.pem\' din dosarul Download. Nu s-a putut citi fișierul \'ca_certs.crt\' din dosarul Download. Pachete pierdute Opus Adresă de ascultare invalidă Servere DNS invalide Nu s-au putut seta serverele DNS Rată de biți Opus invalidă Procentaj de pachete pierdute Opus invalid Mod de răspuns Selectează modul de răspuns al apelurilor primite. Manual Automat Repornire Rata medie maximă de biți utilizată de fluxul audio Opus. Valorile valide sunt 6000-510000. Valoarea implicită din fabrică este 28000. Dezactivează Activează Copie de rezervă Restaurare Codecurile audio furnizate de modulele bifate sunt disponibile pentru utilizare de către conturi. Nume afișat nevalid \"%1$s\' Despre baresip+ ================================================ FILE: app/src/main/res/values-ru/strings.xml ================================================ О программе Добавить Добавить контакт Внимание Аудио кодеки Пароль аутентификации Имя пользователя Авто Вызов У вас уже есть активный звонок. Вызов закончен Вызов не удался История звонков Информация о звонке Этот вызов БЕЗОПАСЕН, и собеседник ПОДТВЕРЖДЕН! Вы хотите отменить верификацию собеседника\? Вы хитите добавить \'%1$s\' в контакты или удалить %2$s из истории звинков? вызов Вызовы Хотите удалить \'%1$s\' %2$s из истории вызовов? Отмена Чат с %1$s История чата Кодеки Хотите перегрузить для активации новых установок? Настройки Хотите позвонить или послать сообщение \'%1$s\'? Контакт \'%1$s\' уже существует. Хотите удалить контакт \'%1$s\'? Имя Контакты Отладка Расшифровать пароль Аккаунт по умолчанию Если отмечено, эта учетная запись будет выбрана при запуске baresip. Громкость вызова по умолчанию Если установлено громкость вызова 1-10. Удалить Хотите удалить аккаунт \'%1$s\'? Хотите удалить историю чата с \'%1$s\'? Хотите удалить историю вызовов с \'%1$s\'? Запрет Отключить Отображаемое имя Имя используемое в From URI исходящих вызовов. ДНС сервера Список адресов DNS-серверов, разделенных запятыми. Если не указан, адреса DNS-серверов получаются из системы динамически. Каждый DNS-адрес имеет форму «ip:порт» или «ip». Если порт не указан, по умолчанию используется значение 53. Если ip является адресом IPv6 и также указан порт, ip должен быть записан в квадратных скобках []. Например, список «8.8.8.8:53,[2001:4860:4860::8888]:53» указывает на IPv4- и IPv6-адреса общедоступных DNS-серверов Google. Длительность: %1$d (с) Редактировать Включить Шифровать пароль Ошибка Не могу загрузить модуль. Не могу установиь ДНС сервера Вызов от … Информация Неверное имя контакта \'%1$s\' Неверный ДНС сервер Неверный адрес Неверный Opus bitrate Послать сообщение Не удалось отправить сообщение Хотите удалить чат с \'%1$s\'? Хотите удалить сообщение? Запускать автоматически Если выбрано, baresip запускается автоматически после (пере)запуска устройства. Статус Сообщения голосовой почты URI голововой почты Да Вы Вы имеете О baresip Аудио модули Пароль аутентификации до 64 символов. Если указано имя пользователя для аутентификации, но нет пароля, он будет запрошен при запуске baresip. Резервное копирование Шифрование медиа Выбирает протокол шифрования медиа-транспорта (если есть). \n • ZRTP (рекомендуется) означает, что согласование сквозного шифрования данных ZRTP предпринимается после установления соединения. \n • DTLS-SRTPF означает, что UDP/TLS/RTP/SAVPF предлагается в исходящем вызове и что RTP/SAVP, RTP/SAVPF, UDP/TLS/RTP/SAVP или UDP/TLS/RTP/SAVPF используется, если предлагается во входящем вызов. \n • SRTP-MANDF означает, что RTP/SAVPF предлагается при исходящем вызове и требуется при входящем вызове. \n • SRTP-MAND означает, что RTP/SAVP предлагается при исходящем вызове и требуется при входящем вызове. \n • SRTP означает, что RTP/AVP предлагается в исходящем вызове и что RTP/SAVP или RTP/SAVPF используется, если предлагается во входящем вызове. Неудачно SIP URI или Новый аккаунт Новый партнер чата Новый контакт Новое сообщение Новые сообщения Нет Вы не имеете сообщений Оповещение Ок старые сообщения одно новое сообщение одно старое сообщение Выход Текущая скорость: %1$s (Кбит/с) Регистрация Сброс к заводским настройкам Перезагрузка Восстановление Запрос на проверку baresip требуется разрешение доступа к микрофону для голосовых вызовов. Хотите удалить сообщения или добавить пользователя \'%1$s\' в контакты? Принять Профиль Не могу создать новый аккаунт. Профиль \'%1$s\' уже существует. Профили и Режим ответа Выберите как отвечать на входящие вызовы. Данные приложения (за исключением записей) сохранены в файле \'%1$s\'. В Android версии 9 этот файл находится в папке Download. Вызов не безопасен! Прослушивать адрес Исходящие прокси Вызов на … Регистрация `%1$s` не удалась. Данные приложения восстановлены. Нужна перезагрузка baresip. Перезагрузить\? Сегодня Принять перевод этого вызова на \'%1$s\'\? Запрос перевода на Отменить подтверждение Неверный процент потерь Opus Прохождение медиа NAT Выбирает протокол прохождения медиа NAT (если есть). Возможные варианты: STUN (Session Traversal Utilities for NAT, RFC 5389) и ICE (Interactive Connectivity Establishment, RFC 5245). У вас нет поддерживаемых камер! Выдайте разрешение доступа к камере для совершения видеозвонков или ответа на них. Не удалось восстановить данные приложения. Проверьте, что вы указали правильный пароль и что файл резервной копии принадлежит этому приложению. В Android версии 9 также проверьте Apps → baresip → Permissions → Storage и наличие файла \'%1$s\' в папке Download. Запрос на перезапуск Не удалось создать резервную копию данных приложения в файл \'%1$s\'. Проверьте Приложения → baresip → Разрешения → Хранилище. Этот вызов БЕЗОПАСНЫЙ, но собеседник НЕ проверен! Передача Вы хотите проверить SAS <%1$s>\? Baresip не запустился. Это может быть из-за неверного значения настроек. Проверьте адрес прослушивания, файл сертификата TLS и файл CA TLS. Затем перезапустите baresip. Слушать Нет информации DTMF (тональный) Разрешить передачу и приём видео с \'%1$s\'\? Видео запрос Видеозвонок Подтверждение Не удалось прочитать файл \'ca_certs.crt\'. Не удалось прочитать файл \'cert.pem\'. Если этот флажок установлен, настройки сбрасываются до значений по умолчанию. При отметке отправляет отладочные и информационные сообщения в Logcat. Размер передаваемых видеокадров (ширина x высота) Размер кадра видео Ожидаемый процент потери пакетов аудиопотока Opus, от 0 до 100. По умолчанию 1. Значение 0 также выключает Opus Forward Error Correction (FEC). Ожидаемая потеря пакетов Opus Средняя максимальная скорость передачи данных, используемая в аудиопотоке Opus. Допустимые значения: 6000-510000. Значение по умолчанию - 28000. Скорость передачи данных Opus Аудиокодеки, предоставленные выбранными модулями, доступны для использования в аккаунтах. Если флажок установлен, то был или будет загружен файл, содержащий TLS-сертификаты таких центров сертификации, которые не включены в ОС Android. В Android версии 9 файл под названием \'ca_certs.crt\' загружается из папки Download. Файл TLS CA Если флажок установлен, загружен или будет загружен файл, содержащий TLS-сертификат и закрытый ключ данного экземпляра baresip. В Android версии 9 файл под названием \'cert.pem\' загружается из папки Download. В целях безопасности удалите этот файл после загрузки. Файл сертификата TLS IP-адрес и порт вида \'адрес:порт\', по которым baresip прослушивает входящие SIP-запросы. Если IP-адрес является IPv6-адресом, он должен быть написан в скобках []. IPv4-адрес 0.0.0.0 или IPv6-адрес [::] заставляет baresip прослушивать все доступные адреса. Если оставить пустым (заводское значение по умолчанию), baresip будет прослушивать произвольный порт на всех доступных адресах. Вы хотите удалить чат с собеседником \'%1$s\' или добавить собеседника в контакты\? Недействительный user@domain[:port][;transport=udp|tcp|tls] \'%1$s\' SIP URI или новая учётная в форме: <пользователь>@<домен>[:<порт>][;transport=udp|tcp|tls]. Если порт указан, а транспортный протокол отсутствует, по умолчанию используется протокол UDP. Если же указан транспортный протокол и отсутствует порт, по умолчанию используется порт 5060 или 5061 (TLS). Если ни один из них не указан и не используется исходящий прокси-сервер, регистратор учётной записи (если есть) определяется исключительно на основе DNS-информации домена. SIP URI для проверки сообщений голосовой почты. Если оставить поле пустым, сообщения голосовой почты (индикаторы ожидающего сообщения) не задействуются. Вручную Неверный пароль \'%1$s\' Пароль, если требуется сервером STUN/TURN Пароль STUN/TURN Недействительное имя пользователя \'%1$s\' Имя пользователя, если требуется сервером STUN/TURN Имя пользователя STUN/TURN Недействительный URI сервера STUN/TURN \'%1$s\' URI сервера STUN/TURN в виде схема:хост[:порт][\?transport=udp|tcp], где схема — \'stun\', \'turn\' или \'turns\'. STUN-сервер по умолчанию для протоколов STUN и ICE — \'stun:stun.l.google.com:19302\' — общедоступный STUN-сервер Google. TURN-сервер по умолчанию не используется. STUN/TURN сервер Видео кодеки При отметке включается регистрация и REGISTER-запросы отправляются с указанным интервалом. Недействительный URI прокси-сервера \'%1$s\' SIP URI другого прокси-сервера SIP URI прокси-сервера <h1>SIP-клиент на основе библиотеки Baresip</h1>\n<p>Juha Heinanen &lt;jh@tutpro.com&gt;</p>\n<p>Версия %1$s</p>\n<h2>Подсказки по использованию</h2>\n<ul>\n<li>Убедитесь, что значения параметров по умолчанию соответствуют вашим потребностям (касайтесь их названий для получения справки).</li>\n<li>Затем создайте одну или несколько учётных записей (опять же касайтесь заголовков элементов для получения справки).</li>\n<li>Новая учётная запись может частично настраиваться автоматически. Смотрите подробности в <a href=https://github.com/juha-h/baresip-studio/wiki/Automatic-Account-Configuration>вики</a>.</li>\n<li>Состояние регистрации учётной записи отображается цветной точкой: зелёной (регистрация успешна), жёлтой (регистрация выполняется), красной (регистрация не удалась), белой (регистрация отключена).</li>\n<li>Касание точки состояния открывает параметры учётной записи.</li>\n<li>Долгое нажатие на значках панели baresip показывает информацию про значки.</li>\n<li>Жест смахивания вниз вызывает повторную регистрацию текущей учётной записи.</li>\n<li>Долгое нажатие на текущей учётной записи включает или отключет её регистрацию.</li>\n<li>Смахивание влево или вправо переключает между учётными записями.</li>\n<li>Значок сверху основного экрана переключает динамик.</li>\n<li>Значки снизу основного экрана открывают голосовую почту (если её URI указан в учётной записи), контакты, сообщения и историю вызовов, а также позволяют переключаться между цифровой и буквенно-цифровой клавиатурами.</li>\n<li>Предыдущего собеседника можно выбрать касанием значка вызова при пустом поле «Вызываемый».</li>\n<li>Собеседников в вызовах и сообщениях можно добавлять в контакты долгими нажатиями.</li>\n<li>Также долгие нажатия можно использовать для удаления вызовов, чатов, сообщений и контактов.</li>\n<li>Долгое нажатие на аудио кодеке позволяет включать или отключать его.</li>\n<li>Долгое нажатие значка контакта можно использовать для установки/удаления изображения аватара.</li>\n</ul>\n<h2>Политика конфиденциальности</h2>\n<ul>\n<li>Политика конфиденциальности доступна <a href=https://raw.githubusercontent.com/juha-h/baresip-studio/master/PrivacyPolicy.txt>здесь</a>.</li>\n</ul>\n<h2>Исходный код</h2> Исходный код доступен на <a href=https://github.com/juha-h/baresip-studio>GitHub</a>, там же можно сообщить о проблемах.\n<h2>Лицензии</h2>\n<ul>\n<li><b>BSD-3-Clause</b>, за исключением:</li>\n<li><b>Apache 2.0</b> Кодеки AMR и защита TLS</li>\n<li><b>AGPLv4</b> ZRTP-шифрофание</li>\n<li><b>LGPL 2.1</b> Кодеки G.722, G.726 и Codec2</li>\n<li><b>GNU GPLv3</b> Кодек G.729</li>\n</ul> SIP URI одного или двух прокси, которые необходимо использовать при отправке запросов. Если задано два, запросы РЕГИСТРАЦИИ отправляются обоим, а другие запросы отправляются тому, кто отвечает. Если исходящий прокси-сервер не указан, запросы отправляются на основе поиска записи DNS NAPTR/SRV/A для URI хоста вызываемого объекта. Если hostpart SIP URI является адресом IPv6, адрес должен быть записан в скобках []. \nПримеры: \n • sip:fooexample.com:50601;transport=tls \n • sip:[2001:67c:223:777::10]:5060;transport=tcp \n • sip:192.168.43.50:443;transport=wss Неверное имя пользователя для аутентификации \'%1$s\' Имя пользователя для аутентификации, если требуется аутентификация SIP-запросов. Значение по умолчанию - имя пользователя учетной записи. Неверное отображаемое имя \'%1$s\' Неверный пароль аутентификации \'%1$s\' Ошибка передачи Переадресовать на Переадресация вызова Переадресовать вызов SIP-клиент на основе библиотеки Baresip с видеозвонками

Juha Heinanen <jh@tutpro.com>

Version %1$s


Подсказки по использованию

  • Проверьте, что значения умолчальных параметров соответствуют вашим потребностям (касайтесь их названий для получения справки).
  • Затем создайте одну или несколько учётных записей (касайтесь заголовков элементов для получения справки).
  • Состояние регистрации учётной записи отображается цветной точкой: зелёная (регистрация успешна), жёлтая (регистрация выполняется), красная (регистрация не удалась), белая (регистрация отключена).
  • Касание точки открывает параметры учётной записи.
  • Долгое нажатие на текущей учётной записи включает или отключает её регистрацию.
  • Жест смахивания вниз вызывает повторную регистрацию текущей учётной записи.
  • Долгое нажатие на текущей учётной записи включает или отключает её регистрацию.
  • Смахивание влево или вправо переключает между учётными записями.
  • редыдущего собеседника можно выбрать касанием значка вызова при пустом поле «Вызываемый».
  • Собеседников в вызовах и сообщениях можно добавлять в контакты долгими нажатиями.
  • Также долгие нажатия можно использовать для удаления вызовов, чатов, сообщений и контактов.
  • Касание / долгое нажатие значка контакта можно использовать для установки/удаления изображения аватара.
  • См. Wiki для подробностей.

Известные проблемы

  • При видеозвонках устройство необходимо удерживать в альбомной ориентации. режим повернут на 90 градусов влево от книжной ориентации.
  • Selfview не отображается должным образом, когда видеопоток используется только для отправки.

Политика конфиденциальности

Политика конфиденциальности доступна здесь.


Исходный код

Исходный код доступен на GitHub, там же можно сообщать об ошибках.


Лицензии

  • BSD-3-Clause except the following:
  • Apache 2.0 AMR codecs and TLS security
  • AGPLv4 ZRTP media encryption
  • GNU LGPL 2.1 G.722, G.726, and Codec2 codecs
  • GNU GPLv3 G.729 codec
  • GNU GPLv2 H.264 and H.265 codecs
  • AOMedia AV1 codec
]]>
О baresip+ Темная тема Проверка сертификатов сервера Пир Направление Продолжительность Оптимизация работы батареи Режим DTMF Выбирает способ отправки сигналов DTMF 0-9, #, * и A-D. Внутриполосные события RTP Запросы SIP INFO Пропущенные вызовы %1$d пропущенных вызовов Сброс Трассировка SIP Пропущенный вызов от Детали вызовов Время Если флажок установлен, baresip проверяет TLS-сертификаты SIP User Agent и SIP Proxy Servers, когда используется транспорт TLS. Принудительное отображение темной темы Вы уверены, что хотите сбросить настройки до значений по умолчанию\? Если флажок установлен и если установлен флажок Отладка, сообщения Logcat также включают трассировку запросов и ответов SIP. Автоматически снимается при запуске baresip. Отключите оптимизацию работы батареи (рекомендуется), если вы хотите снизить вероятность того, что Android ограничит доступ baresip к сети или переведет baresip в режим ожидания. Аудионастройки Псевдоним (если есть), используемый для идентификации этой учетной записи в baresip app. Неверный никнейм учетной записи \'%1$s\' Никнейм \'%1$s\' уже существует Код страны Никнейм Интервал регистрации Неверный интервал регистрации \'%1$s\' Указывает baresip как часто (в секундах) отправлять REGISTER-запросы. Допустимы значения от 60 до 3600. Неверная хостовая часть SIP URI \'%1$s\' Разрешить приём видео с сайта \'%1$s\'\? Разрешить передачу видео на \'%1$s\'\? Мультиплексирование RTCP Запрос согласия SIP или tel URI Неверный SIP или tel URI \'%1$s\' Отклонено… Вызов на удержании С участием Потеряно Вы не можете получить доступ к контактам Android без разрешения «Контакты». Джиттер: %1$s (мс) Неверный код страны \'%1$s\' Выберите целевой URI Пакеты Средняя скорость: %1$s (кбит/с) Анонимно Неизвестно baresip необходимо разрешение \"Микрофон\" для голосовых вызовов, разрешение \"Близлежащие устройства\" для обнаружения микрофона/динамика Bluetooth, разрешение \"Уведомления\" для отправки уведомлений, а в Android 9 - разрешение \"Хранилище\" для операций резервного копирования/восстановления. Если выбраны контакты Android, они могут быть использованы в звонках и сообщениях как ссылки для SIP и tel URI. Приложение baresip не хранит контакты Android и никому их не передаёт. Чтобы контакты Android стали доступны в baresip, Google требует вашего согласия на их использование в соответствии с этим описанием и политикой конфиденциальности . Код страны в формате E.164 для данной учетной записи. Если пользовательская часть URI инициатора входящего вызова или отправителя сообщения содержит телефонный номер, не начинающийся со знака \'+\', и контакт не найден, к номеру добавляется префикс с этим кодом страны и повторяется поиск контакта. Если номер телефона начинается с одной цифры \'0\', она удаляется перед добавления номеру префикса. Семейство адресов Изображение профиля В учётной записи \'%1$s\' нет поставщика услуг телефонии Обоснование разрешений Аудиофокус запрещён! Оба Нет подключения к сети! пользователь@домен или номер телефона При отметке RTP- и RTCP-пакеты мультиплексируются на единственном порту (RFC 5761). Хостовая часть SIP URI используется при звонках на телефонные номера. По умолчанию это домен учётной записи. Если не указана, эта учётная запись не сможет использоваться для телефонных вызовов. Выбирает, будут ли использоваться контакты baresip, контакты Android или оба контакта. Если используется и то, и другое, и контакт с одинаковым именем существует в обоих контактах, будет выбран контакт baresip. Определяет, какие IP-адреса использует baresip. Если выбрано IPv4 или IPv6, baresip использует либо IPv4, либо IPv6 адреса. Если ничего не выбрано, baresip использует оба типа. baresip+ необходимо разрешение \"Микрофон\" для голосовых вызовов, разрешение \"Камера\" для видеозвонков, разрешение \"Близлежащие устройства\" для обнаружения микрофона/динамика Bluetooth, разрешение \"Уведомления\" для отправки уведомлений, а в Android 9 - разрешение \"Хранилище\" для операций резервного копирования/восстановления. Поставщик услуг телефонии При отметке этот контакт будет добавлен в контакты Android. Вслепую Вы не можете создавать резервные копии без разрешения доступа к хранилищу. Вы не можете восстановить резервную копию без разрешения доступа к хранилищу. Вы не можете использовать это приложение без разрешения «Уведомления». Запись может быть включена или отключена только при отсутствии соединения Задержка звука Время (в миллисекундах) ожидания звука вызываемой стороны при установлении соединения. Установите большее значение, если звук собеседника теряется в начале звонка. Некорректная Задержка звука \'%1$s\'. Допускаются значения от 100 до 3000. Автоматически отклонённый вызов от %1$s Автоматическая переадресация на \'%1$s\'\\ Роль номеронабирателя недоступна Предпочтительный Определяет, будет ли переадресация вызова выполняться автоматически или будет запрошено подтверждение. Согласны ли вы переадресовать вызов на \'%1$s\'? Режим переадресации Телефонное приложение по умолчанию Запрос переадресации Страна тонов При отметке показывается поддержка надёжных предварительных ответов (RFC 3262). Если отмечено, baresip является приложением по умолчанию. Не устанавливайте, если устройству может потребоваться обработка не только SIP-вызовов или сообщений. Страна для определения тонов вызова, ожидания и занятости Надёжные предварительные ответы Для автоматического запуска требуется разрешение отображения поверх других приложений. Кадровая частота Цифровая клавиатура" Юзер агент Громкоговоритель Если этот флажок установлен, громкоговоритель автоматически включается при начале вызова. Недопустимые кадры В секунду \'%1$d\' Недопустимое значение поля заголовка User-Agent Микрофон Частота кадров видео, которая будет предлагаться во время рукопожатия SDP. Допустимые значения - от 10 до 30. Если он активирован во время вызова, микрофон отключается. Громкоговоритель Если активирован, звук воспроизводится через громкоговоритель устройства. Запись звонков Если активирован, будут записываться новые входящие и исходящие вызовы. Записи можно воспроизвести на странице Сведений о вызове Если флажок установлен, цифровая клавиатура отображается, когда выделено поле \'Позвонить на ...\'." Умножить громкость микрофона на это десятичное число. Минимальное значение равно 1.0 (заводское значение по умолчанию), которое отключает усиление микрофона. Большие значения могут негативно повлиять на качество звука. Нет разрешения на чтение из внешнего хранилища Если этот флажок установлен, контакт отображается среди других избранных в верхней части списка контактов. Не удалось восстановить данные приложения. Android версии 14 и выше не позволяет восстанавливать данные, резервные копии которых были созданы ранее %1$s version %2$s. Усиление микрофона Рингтон Выберите рингтон Значение поля пользовательского заголовка User-Agent SIP-запроса / ответа Отсутствие аппаратного акустического эхоподавления! Запрос на вызов Принимаете ли вы запрос на звонок \'%1$s\'? In-band RTP или SIP INFO Воспроизведение записи … Недопустимое значение усиления микрофона Ответить Сохранить Звонок принят Ответ на звонок в другом месте Пропущенный звонок Звонок отклонен Цвет для слепых Используйте удобные для слепых значки состояния регистрации Датчик приближения"> Если флажок установлен, функция определения приближения активна во время вызовов. Блокировать неизвестных Блокировать звонки и сообщения от участников которые не найдены в контактах. звонит Сохранить запись Вы хотите сохранить эту запись? Запись сохранена Вы хотите удалить этот звонок из истории? Протоколы передачи Список поддерживаемых транспортных протоколов передачи SIP-запросов/ответов, разделенных запятыми. Если поле оставить пустым, по умолчанию будет использоваться значение \'udp,tcp,tls,ws,wss\', включающее все поддерживаемые протоколы передачи. Неправильный список протоколов передачи Динамические цвета Использовать динамические цвета, если они включены в настройках Android Стоп
================================================ FILE: app/src/main/res/values-sl/strings.xml ================================================ Uporabniško Ime Napačno ime za prikaz %1$s Ime (če je) uporabljen v polju From URI za odhodne akcije. Prikazano ime Račun O baresip+ O baresip-u ================================================ FILE: app/src/main/res/values-sv/strings.xml ================================================ OK Vill du ta bort kontakten \'%1$s\'\? Skicka meddelande Vill du ringa eller skicka meddelande till \'%1$s\'\? Kontakter Namn Ny kontakt Mörkt tema Ogiltiga DNS-servrar DNS-servrar Starta automatiskt Inställningar Du Idag Lägg till kontakt Vill du ta bort meddelandet\? Nytt meddelande Vill du ta bort samtalshistoriken för kontot \'%1$s\'\? Aktivera Inaktivera Vill du ta bort \'%1$s\' %2$s från samtalshistoriken\? Vill du lägga till \'%1$s\' to dina kontakter eller ta bort %2$s från samtalshistoriken\? samtal samtal Ring Samtalshistorik Missat samtal från Vill du ta bort kontot \'%1$s\'\? Kontot \'%1$s\' finns redan. Ogiltig användare@domän[:port][;transport=udp|tcp|tls] \'%1$s\' SIP URI för nytt konto Konton Förvalt konto SIP INFO-förfrågningar Inuti RTP Väljer hur DTMF-toner för 0-9, #, * och A-D skickas. DTMF-läge Mediakryptering Ogiltigt lösenord \'%1$s\' Lösenord om STUN/TURN-servern kräver det STUN/TURN-lösenord Ogiltigt användarnamn \'%1$s\' Användarnamn om STUN/TURN-servern kräver det Ogiltig STUN/TURN-server-URI \'%1$s\' En STUN/TURN-server-URI på formatet schema:värd[:port][\?transport=udp|tcp], där schema är \'stun\', \'stuns\', \'turn\', eller \'turns\'. Förvald STUN-server för STUN- och ICE-protokollen är \'stun:stun.l.google.com:19302\' som är Googles publika STUN-server. Det finns ingen förvald TURN-server. NAT-passering av media Väljer protokoll för NAT-passering av media. Möjliga val är STUN (Session Traversal Utilities for NAT, RFC 5389) och ICE (Interactive Connectivity Establishment, RFC 5245). Nej Ja Lägg till Ta bort Ändra Status Fel Säkerhetskopiera Återställ Om Starta om Avsluta Ring till … Samtal från … Koppla samtal Koppla till Koppling misslyckades Koppling DTMF Kodekar Röstmeddelanden ett nytt meddelande Du har nya meddelanden ett gammalt meddelande gamla meddelanden och Lyssna Du har inga meddelanden Registrering av %1$s misslyckades. Samtal misslyckades Samtal avslutat Detta samtal är INTE säkert! Ta bort verifiering Detta samtal är SÄKERT och motparten är VERIFIERAD! Vill du ta bort verifieringen av motparten\? Detta samtal är SÄKERT, men motparten är INTE verifierad! STUN/TURN-användarnamn STUN/TURN-server Videokodekar Ljudkodekar Kryssa i för att aktivera registrering och att REGISTER-förfrågningar skickas periodiskt enligt angett registreringsintervall. Registrera Ogiltig proxyserver-URI \'%1$s\' SIP-URI för en proxyserver SIP-URI för en annan proxyserver SIP-URI för en eller två proxyservrar som måste användas när SIP-förfrågningar skickas. Om två anges skickas REGISTER-förfrågningar till båda och andra förfrågningar skickas till den som svarar. Om ingen utgående proxyserver anges skickas förfrågningar genom att DNS NAPTR/SRV/A-poster slås upp från värdnamnet i mottagarens URI. Om värdnamnet är en IPv6-adress måste det skrivas inom hakparenteser []. \nExempel: \n • sip:example.com:5061;transport=tls \n • sip:[2001:67c:223:777::10];transport=tcp \n • sip:192.168.43.50:443;transport=wss Utgående proxyservrar Ogiltigt lösenord för autentisering \'%1$s\' Lösenord för autentisering, upp till 64 tecken. Om användarnamn för autentisering är ifyllt, men lösenordet lämnas tomt kommer baresip fråga om lösenord vid uppstart. Lösenord för autentisering Användarnamn som används om SIP-förfrågningar kräver autentisering. Förvalt värde är kontots användarnamn. Användarnamn för autentisering Ogiltigt användarnamn för autentisering \'%1$s\' Ogiltigt visat namn \'%1$s\' Namn som, om det anges, används i From-URI i utgående SIP-förfrågningar. Visat namn Konto Om baresip+ Om baresip SIP-användaragent baserad på baresip-biblioteket

Juha Heinanen <jh@tutpro.com>

Version %1$s


Användningstips

  • Kontrollera att standardvärdena i baresips inställningar uppfyller dina behov (tryck på objektets titel för hjälp).
  • Skapa sedan ett eller flera konton i Konton (tryck på objektets titel för hjälp).
  • Registreringsstatus för ett konto visas med en färgad punkt: grön (registrering lyckades), gul (registrering pågår), röd (registrering misslyckades), vit (registrering har inte aktiverats).
  • Tryck på statuspunkten för att komma direkt till kontoinställningarna.
  • Långt tryck på baresip-ikonerna visar information om ikonerna.
  • Svep nedåt för att omregistrera det aktuella kontot.
  • Långt tryck på det aktuella kontot aktiverar eller inaktiverar kontots registrering.
  • Svep åt vänster/höger för att växla mellan kontona.
  • Tidigare samtalspartner kan väljas igen genom att trycka på samtalsikonen när mottagaren är tom.
  • Samtal och meddelanden kan läggas till i kontakter genom att trycka länge på dem.
  • Långt tryck kan också användas för att ta bort samtal, chattar, meddelanden och kontakter.
  • Tryck/långtryck på kontaktikonen kan användas för att installera/ta bort bildavatar.
  • Långtryck på en ljudkodek kan användas för att aktivera/inaktivera kodeken.
  • Se Wiki för mer information.

Integritet

Integritetspolicyn finns tillgänglig här.


Källkod

Källkoden finns tillgänglig på GitHub, där även problem kan rapporteras.


Språköversättningar

Språköversättningar hanteras via baresip Weblate-projektet.


Licenser

  • BSD-3-klausul med undantag för följande:
  • Apache 2.0 AMR-kodekter och TLS-säkerhet
  • AGPLv4 ZRTP-mediekryptering
  • GNU LGPL 2.1 G.722-, G.726- och Codec2-kodeken
  • GNU GPLv3 G.729-kodeken
]]>
Accepterar du att koppla det här samtalet till \'%1$s\'\? Ljudmoduler Baresip kunde inte starta. Det kan bero på ett ogiltigt värde för en inställning. Kontrollera Adress att lyssna på, TLS certifikat-fil och TLS CA-fil. Starta sedan om baresip. Du har redan ett pågående samtal. Nuvarande hastighet: %1$s (kbits/s) Ingen information tillgänglig Samtalsinformation Längd: %1$d (sekunder) Acceptera att ta emot video från \'%1$s\'\? Acceptera att skicka video till \'%1$s\'\? Acceptera att skicka och ta emot video med \'%1$s\'\? Videoförfrågan Videosamtal Avvisa Avbryt Kontakten \'%1$s\' finns redan. Ogiltigt kontaktnamn \'%1$s\' Du måste starta om baresip för att aktivera de nya inställningarna. Starta om nu\? Kunde inte läsa filen \'ca_certs.crt\'. Kunde inte läsa filen \'cert.pem\'. Återställ Är du säker på att du vill återställa alla inställningar till deras ursprungsvärden\? Kryssa i för att återställa alla inställningar till deras ursprungsvärden. Återställ grundinställningarna Framtvinga mörkt skärmtema Ogiltig Opus-bitrate Förväntat Opus-pakettapp Opus-bitrate Kunde inte ladda modul. Ljudkodekar som tillhandahålls av de förkryssade modulerna är tillgängliga för konton. TLS CA-fil Verifiera servercertifikat Chatta med %1$s Begäran att koppla samtalet till Kunde inte skapa ett nytt konto. Kryssa i för att välja detta konto när baresip startar. SIP-URI för att kontrollera röstmeddelanden. Om fältet lämnas tomt kommer röstmeddelanden (Message Waiting Indications) inte kontrolleras. URI till röstbrevlåda Automatiskt Manuellt Anger hur inkommande samtal besvaras. Metod att svara Väljer eventuellt krypteringsprotokoll för media. \n • ZRTP (rekommenderad) innebär att ändpunktskryptering via ZRTP försöker förhandlas fram efter att samtalet kopplats upp. \n • DTLS-SRTPF innebär att UDP/TLS/RTP/SAVPF erbjuds i utgående samtal och att RTP/SAVP, RTP/SAVPF, UDP/TLS/RTP/SAVP eller UDP/TLS/RTP/SAVPF används om det erbjuds i inkommande samtal. \n • SRTP-MANDF innebär att RTP/SAVPF erbjuds i utgående samtal och är ett krav för inkommande samtal. \n • SRTP-MAND innebär att RTP/SAVP erbjuds i utgående samtal och är ett krav för inkommande samtal. \n • SRTP innebär att RTP/AVP erbjuds i utgående samtal och att RTP/SAVP eller RTP/SAVPF används om de erbjuds i inkommande samtal. TLS-certifikatfil Kunde inte ställa in DNS-servrar Kommaseparerad lista med adresser på DNS-servrar. Om listan lämnas tom hämtas DNS-serveradresser automatiskt från systemet. Varje DNS-adress skrivs på formatet \'ip:port\' eller \'ip\'. Om port utelämnas används port 53. Om ip är en IPv6-adress och en port anges då måste ip skrivas inom hakparenteser []. Som exempel, i listan \'8.8.8.8:53,[2001:4860:4860::8888]:53\' anges IPv4- och IPv6-adresser för Googles publika DNS-servrar. Ogiltig adress att lyssna på IP-adress och port i formatet ”adress:port” där baresip lyssnar efter inkommande SIP-förfrågningar. Om IP-adressen är en IPv6-adress måste den skrivas inom hakparenteser []. IPv4-adressen 0.0.0.0 eller IPv6-adressen [::] gör att baresip lyssnar på alla tillgängliga adresser. Om fältet lämnas tomt (fabriksinställning) lyssnar baresip på en godtycklig port på alla tillgängliga adresser. Adress att lyssna på Kryssa i för att starta baresip automatiskt när enheten startat. Vill du ta bort chatt-historiken för kontot \'%1$s\'\? Vill du ta bort chatten med \'%1$s\'\? Vill du ta bort chatten med motparten \'%1$s\' eller lägga till motparten till dina kontakter\? Ny chat-motpart Chat-historik Misslyckades Kunde inte skicka meddelande Vill du ta bort meddelandet eller lägga till motparten \'%1$s\' till dina kontakter\? Lösenord för avkryptering Lösenord för kryptering SIP URI för nytt konto i formatet <användare>@<domän>[:<port>][;transport=udp|tcp|tls]. Om <port> anges och transportprotokollet inte anges, är standardtransportprotokollet UDP. Om <port> inte anges och transportprotokollet anges, är standardporten 5060 eller 5061 (TLS). Om varken det ena eller det andra anges och ingen utgående proxy specificeras, bestäms kontots registrator (om sådan finns) enbart utifrån domänens DNS-information. SIP-användaragent med videosamtal baserat på baresip-biblioteket

Juha Heinanen <jh@tutpro.com>

Version %1$s


Användningstips

  • Kontrollera att standardvärdena i baresip+\'s Inställningar uppfyller dina behov (tryck på objektets titel för hjälp).
  • Skapa sedan ett eller flera konton i Konton (tryck på objektets titel för hjälp).
  • Registreringsstatus för ett konto visas med en färgad punkt: grön (registrering lyckades), gul (registrering pågår), röd (registrering misslyckades), vit (registrering har inte aktiverats).
  • Tryck på statuspunkten för att komma direkt till kontoinställningarna.
  • Långt tryck på baresip-ikonerna visar information om ikonerna.
  • Svep nedåt för att omregistrera det aktuella kontot.
  • Långt tryck på det aktuella kontot aktiverar eller inaktiverar kontots registrering.
  • Svep åt vänster/höger för att växla mellan kontona.
  • Tidigare samtalspartner kan väljas igen genom att trycka på samtalsikonen när mottagaren är tom.
  • Samtalspartner och meddelanden kan läggas till i kontakter genom att trycka länge på dem.
  • Långt tryck kan också användas för att ta bort samtal, chattar, meddelanden och kontakter.
  • Tryck/långt tryck på kontaktikonen kan användas för att installera/ta bort bildavatar.
  • Långt tryck på en ljud- eller videokodek kan användas för att aktivera/inaktivera kodeken.
  • Se Wiki för mer information.

Kända problem
  • Vid videosamtal måste enheten hållas i liggande läge, roterat 90 grader åt vänster från stående läge.
  • Självbilden visas inte korrekt när videoströmmen är sendonly.

Integritet

Integritet finns här.


Källkod

Källkoden finns tillgänglig på GitHub, där även problem kan rapporteras.


Språköversättningar

Språköversättningar hanteras via baresip Weblate-projektet.


Licenser

  • BSD-3-klausul med undantag för följande:
  • Apache 2.0 AMR-kodeken och TLS-säkerhet
  • AGPLv4 ZRTP-mediekryptering
  • GNU LGPL 2.1 G.722-, G.726- och Codec2-kodeken
  • GNU GPLv3 G.729-kodeken
  • GNU GPLv2 H.264- och H.265-kodeken
  • AOMedia AV1-kodeken
]]>
Förvald samtalsvolym Ogiltig procentuell paketförlust för Opus Förväntad procentuell förlust av Opus-ljudströmspaket, från 0 till 100. Fabriksinställt standardvärde är 1. Värdet 0 tur inaktiverar även Opus Forward Error Correction (FEC). Genomsnittlig maximal bitrate för Opus-ljudström. Giltiga värden är 6000-510000. Förvalt värde är 28000. Omstartsbegäran Verifikationsbegäran Vill du verifiera SAS <%1$s>\? Kopplingsbegäran Kryssa i för att använda en förvald ljudvolym, 1–10, vid samtal. Acceptera Kryssa i för att lägga till kontakten till dina Android-kontakter. Kryssa i för att inkludera SIP-meddelanden i Logcat-meddelanden när Felsökning är aktiverat. Funktionen inaktiveras automatiskt när baresip startar. Kryssa i för att skicka felsöknings- och info-meddelanden till Logcat. Om denna ruta är markerad har en fil läst in eller kommer att läst in som innehåller TLS-certifikat från certifikatutfärdare som inte ingår i Android OS. I Android version 9 läst in en fil med namnet ”ca_certs.crt” från mappen Hämtningar. Om denna ruta är markerad verifierar baresip TLS-certifikat för SIP-användaragenter och SIP-proxyservrar när TLS-transport används. Om denna ruta är markerad har en fil som innehåller TLS-certifikatet och den privata nyckeln för denna baresip-instans läsas in eller kommer att läsas in. I Android version 9 läses in en fil med namnet ”cert.pem” från mappen Download. Av säkerhetsskäl bör du radera filen efter läsin. Bekräftelse Notis Info Varning SIP-trafik Felsökning Storlek på bildrutorna i skickad video (bredd x höjd) Storlek på bildrutor Du har ingen videokamera som stöds! Aktivera behörigheten \"Kamera\" för att ringa eller ta emot samtal. baresip behöver behörigheten \"Mikrofon\" för röstsamtal. Det gick inte att återställa programdata. Kontrollera att du har angett rätt lösenord och att säkerhetskopian kommer från det här programmet. I Android version 9 ska du också kontrollera Appar → baresip → Behörighet → Lagring och att filen ’%1$s’ finns i mappen Nedladdningar. Programdata återställd. baresip behöver startas om. Starta om nu\? Kunde inte säkerhetskopiera programdata till filen \'%1$s\'. Kontrollera Appar → baresip → Behörigheter → Lagring. Applikationsdata (exklusive inspelningar) säkerhetskopierade till filen ’%1$s’. I Android version 9 finns filen i mappen Hämtningar. Inget nätverk tillgängligt! Missade samtal %1$d missade samtal Batterioptimering Profilbild Det går inte att skapa en backup utan behörigheten \"Lagring\". Det går inte att återställa en backup utan behörigheten \"Lagring\". Inaktivera batterioptimering (rekommenderas) om du vill minska risken att Android begränsar baresips åtkomst till nätverk eller inaktiverar baresip för att spara ström. Ljudinställningar Jitter: %1$s (ms) Samtalet är parkerat Medelhastighet: %1$s (kbits/s) Paket Samtalsdetaljer Tid Värdnamn att använda i SIP-URI:er vid samtal till telefonnummer. Förvalt är att använda kontots domän. Om fältet lämnas tomt kan inte detta konto användas för att ringa telefonnummer. Motpart Telefonileverantör Ogiltigt värdnamn i SIP-URI \'%1$s\' Riktning Längd SIP- eller tel-URI användare@domän eller telefonnummer Tappade Ogiltig SIP- eller tel-URI \'%1$s\' Väljer om baresip-kontakter, Android-kontakter eller båda ska användas. Om båda används och en kontakt med samma namn finns i båda kontakterna, väljs baresip-kontakten. Båda Det går inte att komma åt Androids kontakter utan behörigheten \"Kontakter\". Landsnummer Ogiltigt landsnummer \'%1$s\' Det här kontots landsnummer, enligt E.164. Om telefonnumret i From-URI för ett inkommande samtal, eller meddelande, inte börjar med en \'+\'-symbol och en matchande kontakt inte kan hittas, då läggs landsnumret till telefonnumret och en kontaktsökning görs igen. Om telefonnumret börjar med siffran \'0\' tas denna siffra bort innan landsnumret läggs till. Anonym Okänd Begäran om medgivande Omdirigerad av… Vägledd Blind Om Androids kontakter används då kan de användas som SIP- eller tel-URI-referenser för samtal och för att skicka meddelanden. baresip sparar inte Androids kontakter och delar dem inte heller med någon. För att Androids kontakter ska bli tillgängliga i baresip kräver Google att du accepterar användningen som beskrivs här och i appens integritetspolicy. Kryssa i för att låta RTP- och RTCP-paket använda samma port (RFC 5761). RTCP-multiplex Det går inte att använda den här appen utan behörigheten \"Aviseringar\". Namn (valfritt) som används för att identifiera det här kontot i baresip-appen. Smeknamn Ogiltigt kontonamn \'%1$s\' Namnet \'%1$s\' används redan Ljudfokus nekad! Registreringsintervall Ogiltigt registreringsintervall \'%1$s\' Kontot \'%1$s\' har ingen telefonileverantör Väljer vilken eller vilka IP-adress baresip använder. Om IPv4 eller IPv6 väljs använder baresip enbart vald adresstyp. Om ingen adresstyp väljs använder baresip både IPv4- och IPv6-adresser. Adresstyp Inspelning kan bara slås på/av när inget samtal pågår Välj destinations-URI baresip behöver behörighet för ”Mikrofon” för röstsamtal, behörighet för ”Närliggande enheter” för Bluetooth-mikrofon-/högtalardetektering, behörighet för ’Meddelanden’ för att publicera meddelanden och i Android 9 behörighet för ”Lagring” för säkerhetskopierings-/återställningsåtgärder. Fördröjt ljud Ogiltigt värde för fördröjt ljud \'%1$s\'. Giltiga värden är från 100 till 3000. Behörigheters motivering Avgör hur ofta (i sekunder) baresip skickar REGISTER-förfrågningar. Giltiga värden är från 60 till 3600. Tid (i millisekunder) att vänta på ljud från motparten när ett samtal upprättats. Sätt till ett högre värde om du saknar ljud från motparten i början av samtalet. baresip+ behöver behörighet för ”Mikrofon” för röstsamtal, behörighet för ”Kamera” för videosamtal, behörighet för ”Närliggande enheter” för Bluetooth-mikrofon-/högtalardetektering, behörighet för ’Meddelanden’ för att publicera meddelanden och i Android 9 behörighet för ”Lagring” för säkerhetskopierings-/återställningsåtgärder. Samtal besvarat Samtal besvarat någon annanstans Samtal missat Samtal nekat Samtalsinspelning Mikrofon Högtalartelefon Väljer om begäran om vidarekoppling av samtal ska följas automatiskt eller om bekräftelse ska begäras. Vidarekopplingsläge Numeriskt tangentbord" Om denna ruta är markerad visas det numeriska tangentbordet när fältet ”Ring till …” är markerat." Svara Spara Automatiskt avvisat samtal från %1$s Spelar upp inspelning … Automatisk start kräver behörighet för att visas överst. Telefonapp som standard Uppringningsrollen är inte tillgänglig Om denna ruta är markerad är baresip standardappen för telefoni. Markera inte denna ruta om din enhet kan behöva hantera andra samtal eller meddelanden än SIP-samtal. Ingen läsbehörighet för extern lagring Högtalartelefon Om denna ruta är markerad aktiveras högtalartelefonen automatiskt när samtalet inleds. Multiplicera mikrofonvolymen med detta decimaltal. Minsta värde är 1,0 (fabriksinställning) som inaktiverar mikrofonförstärkningen. Högre värden kan påverka ljudkvaliteten negativt. Mikrofonförstärkning Ogiltigt värde för mikrofonförstärkning Land för ringsignal, vänteläge och upptaget-signaler Färgblind Använd vänliga ikoner för registreringsstatus för färgblinda Närhetssensor"> Om denna ruta är markerad är närhetssensorn aktiv under samtal. Bildfrekvens för video Videobildfrekvens som kommer att erbjudas under SDP-handskakningen. Giltiga värden är mellan 10 och 30. Ogiltig bildfrekvens \'%1$d\' Ringsignal Välj ringsignal User Agent Anpassat värde för SIP-begäran/svar User-Agent-rubrikfält Ogiltigt värde i fältet User-Agent-header Favorit Om denna ruta är markerad visas kontakten bland andra favoriter högst upp i kontaktlistan. Samtalsbegäran Accepterar du begäran om att ringa ’%1$s’? Automatisk omkoppling till ’%1$s’\\ Omkopplingsbegäran Accepterar du vidarekoppling av samtal till ’%1$s’? Det gick inte att återställa applikationsdata. Android version 14 och senare tillåter inte återställning av data som säkerhetskopierats före %1$s version %2$s. Ingen hårdvara för akustisk ekokompensering! Om funktionen är aktiverad kommer nya inkommande och utgående samtal att spelas in. Inspelningarna kan spelas upp på sidan Samtalsdetaljer Om funktionen aktiveras under samtalet stängs mikrofonen av. Om funktionen är aktiverad spelas ljudet upp via enhetens högtalartelefon. Reliable Provisional Responses Om detta är markerat, ange stöd för tillförlitliga provisoriska svar (RFC 3262). In-band RTP eller SIP INFO Signalland Dynamiska färger Använd dynamiska färger om det är aktiverat i Android-inställningarna
================================================ FILE: app/src/main/res/values-ta/strings.xml ================================================ பரேசிப் பற்றி Paresip+ பற்றி கணக்கு இந்த கணக்கை PARESIP பயன்பாட்டில் அடையாளம் காண புனைப்பெயர் (ஏதேனும் இருந்தால்) பயன்படுத்தப்படுகிறது. தவறான அங்கீகார கடவுச்சொல் \'%1$s\' வெளிச்செல்லும் ப்ராக்சிகள் பதிலாள் சேவையகத்தின் SIP யூரி மற்றொரு பதிலாள் சேவையகத்தின் யூரி பதிவு செய்யுங்கள் வீடியோ கோடெக்குகள் தானியங்கி தொடக்க தேவைகள் சிறந்த அனுமதியில் தோன்றும். பேட்டரி மேம்படுத்தல்கள் பேட்டரி மேம்படுத்தல்களை முடக்கு (பரிந்துரைக்கப்படுகிறது) ஆண்ட்ராய்டு ப்ரெசிப்பின் நெட்வொர்க்கிற்கான அணுகலை கட்டுப்படுத்தும் அல்லது காத்திருப்பு நிலைக்கு பெரெசிப்பில் நுழைகிறது என்ற வாய்ப்பைக் குறைக்க விரும்பினால். இயல்புநிலை தொலைபேசி பயன்பாடு டயலர் பங்கு கிடைக்கவில்லை சரிபார்க்கப்பட்டால், PARESIP இயல்புநிலை தொலைபேசி பயன்பாடாகும். SIP அழைப்புகள் அல்லது செய்திகளைத் தவிர உங்கள் சாதனம் கையாள வேண்டுமா என்று சரிபார்க்க வேண்டாம். முகவரியைக் கேளுங்கள் தவறான கேளுங்கள் முகவரி முகவரி குடும்பம் மைக்ரோஃபோன் ஆதாயம் தவறான ஓபச் பிட்ரேட் தவறான ஓபச் பாக்கெட் இழப்பு விழுக்காடு ஒரு நொடிக்கு தவறான பிரேம்கள் \'%1$d\' பயனர் முகவர் ஏற்றுக்கொள் மறுக்கவும் கூட்டு நீக்கு தொகு நிலை பிழை கோரிக்கையை மறுதொடக்கம் செய்யுங்கள் தவறான அங்கீகார பயனர்பெயர் \'%1$s\' புனைப்பெயர் தவறான கணக்கு புனைப்பெயர் \'%1$s\' \'%1$s\' என்ற புனைப்பெயர் ஏற்கனவே உள்ளது காட்சி பெயர் வெளிச்செல்லும் கோரிக்கைகளின் யூரி இலிருந்து பெயர் (ஏதேனும் இருந்தால்) பயன்படுத்தப்படுகிறது. தவறான காட்சி பெயர் \'%1$s\' அங்கீகார பயனர்பெயர் அங்கீகார பயனர்பெயர் SIP கோரிக்கைகளின் ஏற்பு தேவைப்பட்டால். இயல்புநிலை மதிப்பு கணக்கின் பயனர்பெயர். அங்கீகார கடவுச்சொல் 64 எழுத்துக்கள் வரை அங்கீகார கடவுச்சொல். அங்கீகார பயனர்பெயர் வழங்கப்பட்டால், ஆனால் கடவுச்சொல் வழங்கப்படவில்லை என்றால், பெரெசிப் எப்போது தொடங்கப்படுகிறது என்று கேட்கப்படும். ச்டன்/டர்ன் சேவையகம் படிவத் திட்டத்தின் ச்டன்/டர்ன் சர்வர் யூரி: புரவலன் [: போர்ட்] [? போக்குவரத்து = யுடிபி | டி.சி.பி], அங்கு திட்டம் \'ச்டன்\', \'ச்டன்ச்\', \'டர்ன்\' அல்லது \'திருப்பங்கள்\'. ச்டன் மற்றும் பனி நெறிமுறைகளுக்கான தொழிற்சாலை இயல்புநிலை ச்டன் சேவையகம் \'ச்டன்: stun.l.google.com: 19302\' பொது கூகிள் ச்டன் சேவையகத்தை சுட்டிக்காட்டுகிறது. தொழிற்சாலை இயல்புநிலை திருப்ப சேவையகம் இல்லை. கோரிக்கைகளை அனுப்பும்போது பயன்படுத்தப்பட வேண்டிய ஒன்று அல்லது இரண்டு ப்ராக்சிகளின் சிப் யூரி. இரண்டு வழங்கப்பட்டால், பதிவு கோரிக்கைகள் இரண்டிற்கும் அனுப்பப்படும் மற்றும் பிற கோரிக்கைகள் பதிலளிக்கும் ஒன்றுக்கு அனுப்பப்படும். வெளிச்செல்லும் பதிலாள் எதுவும் வழங்கப்படாவிட்டால், டி.என்.எச் நாப்டிஆர்/எச்.ஆர்.வி/காலீ யூரி ஓச்ட்பார்ட்டின் பதிவு தேடல் ஆகியவற்றின் அடிப்படையில் கோரிக்கைகள் அனுப்பப்படுகின்றன. SIP யூரி இன் ஓச்ட்பார்ட் ஒரு IPv6 முகவரி என்றால், முகவரி அடைப்புக்குறிக்குள் எழுதப்பட வேண்டும் [].\n எடுத்துக்காட்டுகள்:\n • SIP: எடுத்துக்காட்டு.காம்: 5061; போக்குவரத்து = TLS\n • சிப்: [2001: 67 சி: 223: 777 :: 10]; போக்குவரத்து = டி.சி.பி.\n • SIP: 192.168.43.50: 443; போக்குவரத்து = WSS தவறான பதிலாள் சர்வர் யூரி \'%1$s\' சரிபார்க்கப்பட்டால், பதிவு இயக்கப்பட்டால் மற்றும் பதிவு வேண்டுகோள்களால் குறிப்பிடப்பட்ட இடைவெளியில் பதிவு கோரிக்கைகள் அனுப்பப்படுகின்றன. பதிவு இடைவெளி எவ்வளவு அடிக்கடி (நொடிகளில்) பரேசிப் பதிவு கோரிக்கைகளை அனுப்புகிறார் என்று சொல்கிறது. செல்லுபடியாகும் மதிப்புகள் 60 முதல் 3600 வரை. தவறான பதிவு இடைவெளி \'%1$s\' மீடியா நாட் டிராவர்சல் மீடியா நாட் டிராவர்சல் நெறிமுறையைத் தேர்ந்தெடுக்கிறது (ஏதேனும் இருந்தால்). சாத்தியமான தேர்வுகள் ச்டன் (NAT, RFC 5389 க்கான அமர்வு பயண பயன்பாடுகள்) மற்றும் ICE (ஊடாடும் இணைப்பு நிறுவனம், RFC 5245). தவறான ச்டன்/டர்ன் சர்வர் யூரி \'%1$s\' \' ச்டன்/டர்ன் பயனர்பெயர் ச்டன்/டர்ன் சேவையகத்தால் தேவைப்பட்டால் பயனர்பெயர் தவறான பயனர்பெயர் \'%1$s\' கடவுச்சொல்லை ச்டன்/திருப்புங்கள் கடவுச்சொல் ச்டன்/டர்ன் சேவையகம் தேவைப்பட்டால் தவறான கடவுச்சொல் \'%1$s\' ஊடக குறியாக்கம் மீடியா போக்குவரத்து குறியாக்க நெறிமுறையைத் தேர்ந்தெடுக்கிறது (ஏதேனும் இருந்தால்).\n • ZRTP (பரிந்துரைக்கப்படுகிறது) என்பது அழைப்பு நிறுவப்பட்ட பின்னர் ZRTP இறுதி முதல் இறுதி ஊடக குறியாக்க பேச்சுவார்த்தை முயற்சிக்கப்படுகிறது.\n • டி.டி.எல்.எச்-எச்.ஆர்.டி.பி.எஃப் என்பது வெளிச்செல்லும் அழைப்பில் யுடிபி/டி.எல்.எச்/ஆர்.டி.பி/எச்.ஏ.வி.பி.எஃப் வழங்கப்படுகிறது, மேலும் RTP/SAVP, RTP/SAVPF, UDP/TLS/RTP/SAVP, அல்லது UDP/TLS/RTP/SAVPF ஆகியவை உள்நுழைவுக்கு வழங்கப்பட்டால் பயன்படுத்தப்பட்டால் பயன்படுத்தப்படுகின்றன அழைப்பு.\n • SRTP-MANDF என்றால், RTP/SAVPF வெளிச்செல்லும் அழைப்பில் வழங்கப்படுகிறது மற்றும் உள்வரும் அழைப்பில் தேவைப்படுகிறது.\n • SRTP-MAND என்பது RTP/SAVP வெளிச்செல்லும் அழைப்பில் வழங்கப்படுகிறது மற்றும் உள்வரும் அழைப்பில் தேவைப்படுகிறது.\n • SRTP என்றால், வெளிச்செல்லும் அழைப்பில் RTP/AVP வழங்கப்படுகிறது மற்றும் உள்வரும் அழைப்பில் வழங்கப்பட்டால் RTP/SAVP அல்லது RTP/SAVPF பயன்படுத்தப்படுகிறது. RTCP மல்டிபிளெக்சிங் சரிபார்க்கப்பட்டால், RTP மற்றும் RTCP பாக்கெட்டுகள் ஒரு துறைமுகத்தில் (RFC 5761) மல்டிபிளெக்ச் செய்யப்படுகின்றன. நம்பகமான தற்காலிக பதில்கள் சரிபார்க்கப்பட்டால், நம்பகமான தற்காலிக பதில்களுக்கான ஆதரவைக் குறிக்கவும் (RFC 3262). டிடிஎம்எஃப் பயன்முறை டி.டி.எம்.எஃப் டோன்கள் 0–9, #, *மற்றும் ஏ-டி எவ்வாறு அனுப்பப்படுகின்றன என்பதைத் தேர்ந்தெடுக்கிறது. இன்-பேண்ட் ஆர்டிபி நிகழ்வுகள் SIP செய்தி கோரிக்கைகள் இன்-பேண்ட் ஆர்டிபி அல்லது எச்ஐபி செய்தி பதில் பயன்முறை உள்வரும் அழைப்புகளுக்கு எவ்வாறு பதிலளிக்கப்படுகிறது என்பதைத் தேர்ந்தெடுக்கிறது. திருப்பி பயன்முறை அழைப்பு திருப்பி கோரிக்கை தானாகவே பின்பற்றப்பட்டால் அல்லது உறுதிப்படுத்தல் கோரப்பட்டால் தேர்ந்தெடுக்கிறது. கையேடு தானியங்கி குரல் அஞ்சல் யூரி குரல் அஞ்சல் செய்திகளைச் சரிபார்க்க யூரி ஐ சிப் செய்யுங்கள். காலியாக இருந்தால், குரல் அஞ்சல் செய்திகள் (செய்தி காத்திருப்பு அறிகுறிகள்) சந்தா செலுத்தப்படவில்லை. நாட்டின் குறியீடு E.164 இந்த கணக்கின் நாடு குறியீடு. உள்வரும் அழைப்பு அல்லது செய்தியின் யூரி USERPART இலிருந்து \'+\' அடையாளத்துடன் தொடங்காத ஒரு தொலைபேசி எண்ணைக் கொண்டிருந்தால், தொடர்பு தேடல் தோல்வியுற்றால், இந்த நாட்டின் குறியீட்டில் எண் முன்னொட்டு மற்றும் தொடர்பு தேடல் மீண்டும் முயற்சிக்கப்படுகிறது. தொலைபேசி எண் \'0\' ஒற்றை இலக்கத்துடன் தொடங்கினால், எண் முன்னொட்டப்படுவதற்கு முன்பு \'0\' இலக்கத்தை அகற்றும். தவறான நாட்டு குறியீடு \'%1$s\' தொலைபேசி வழங்குநர் தொலைபேசி எண்களுக்கான அழைப்புகளில் பயன்படுத்தப்படும் SIP யூரி புரவலன் பகுதி. தொழிற்சாலை இயல்புநிலை என்பது கணக்கின் களமாகும். வழங்கப்படாவிட்டால், தொலைபேசி எண்களை அழைக்க இந்த கணக்கைப் பயன்படுத்த முடியாது. தவறான SIP யூரி புரவலன் பகுதி \'%1$s\' இயல்புநிலை கணக்கு சரிபார்க்கப்பட்டால், பரெசிப் தொடங்கும்போது இந்த கணக்கு தேர்ந்தெடுக்கப்படுகிறது. கணக்குகள் சிப் புதிய கணக்கு முகவரி சிப் புதிய கணக்கு முகவரி வடிவம்: <பயனர்>@<டொமைன்> [: <port>] [; போக்குவரத்து = யுடிபி | டி.சி.பி | டி.எல்.எச்]. <Port> வழங்கப்பட்டு போக்குவரத்து நெறிமுறை வழங்கப்படாவிட்டால், போக்குவரத்து நெறிமுறை இயல்புநிலையாக UDP க்கு. <port> வழங்கப்படாவிட்டால் மற்றும் போக்குவரத்து நெறிமுறை வழங்கப்பட்டால், <PORT> இயல்புநிலை 5060 அல்லது 5061 (TLS) க்கு. எதுவும் வழங்கப்படவில்லை மற்றும் வெளிச்செல்லும் பதிலாள் குறிப்பிடப்படவில்லை என்றால், கணக்கின் பதிவாளர் (ஏதேனும் இருந்தால்) டொமைனின் டிஎன்எச் தகவல்களை மட்டுமே அடிப்படையாகக் கொண்டது. தவறான பயனர்@டொமைன் [: போர்ட்] [; போக்குவரத்து = யுடிபி | டி.சி.பி | டி.எல்.எச்] \'%1$s\' \' கணக்கு \'%1$s\' ஏற்கனவே உள்ளது. புதிய கணக்கை ஒதுக்கத் தவறிவிட்டது. கடவுச்சொல்லை குறியாக்கவும் கடவுச்சொல்லை டிக்ரிப்ட் செய்யுங்கள் \'%1$s\' கணக்கை நீக்க விரும்புகிறீர்களா? இருந்து தவறவிட்ட அழைப்பு தவறவிட்ட அழைப்புகள் %1$d தவறவிட்ட அழைப்புகள் அழைப்பு பரிமாற்ற கோரிக்கை %1$s இலிருந்து தானாக நிராகரிக்கப்பட்ட அழைப்பு வரலாற்றை அழைக்கவும் அழைப்பு விவரங்கள் அழைப்பு அழைப்புகள் அழைப்பு ஒப்பி திசை நேரம் காலம் அழைப்பு வரலாற்றிலிருந்து \' %1$s\' %2$s ஐ நீக்க விரும்புகிறீர்களா? முடக்கு இயக்கு கணக்கின் அழைப்பு வரலாற்றை \'%1$s\' ஐ நீக்க விரும்புகிறீர்களா? %1$s உடன் அரட்டையடிக்கவும் புதிய செய்தி செய்திகளை நீக்க விரும்புகிறீர்களா அல்லது தொடர்புகளில் \'%1$s\' ஐ சேர்க்க விரும்புகிறீர்களா? செய்தியை நீக்க விரும்புகிறீர்களா? தொடர்பைச் சேர்க்கவும் செய்தியை அனுப்புவது தோல்வியடைந்தது தோல்வியுற்றது அரட்டை வரலாறு இன்று நீங்கள் புதிய அரட்டை பியர் பியர் \'%1$s\' உடன் அரட்டையை நீக்க விரும்புகிறீர்களா அல்லது தொடர்புகளுக்கு சகாக்களைச் சேர்க்க விரும்புகிறீர்களா? \'%1$s\' உடன் அரட்டையை நீக்க விரும்புகிறீர்களா? கணக்கின் அரட்டை வரலாற்றை \'%1$s\' ஐ நீக்க விரும்புகிறீர்களா? ஆடியோ கோடெக்குகள் அமைப்புகள் தானாகத் தொடங்குங்கள் சரிபார்க்கப்பட்டால், சாதனம் (மறு) தொடங்கிய பின் PARESIP தானாகத் தொடங்குகிறது. மீட்டெடு ஐபி முகவரி மற்றும் படிவத்தின் துறை \'முகவரி: துறைமுகம்\' உள்வரும் எச்ஐபி கோரிக்கைகளுக்குப் பெரெசிப் கேட்கிறது. ஐபி முகவரி ஒரு ஐபிவி 6 முகவரி என்றால், அது அடைப்புக்குறிக்குள் எழுதப்பட வேண்டும் []. ஐபிவி 4 முகவரி 0.0.0.0 அல்லது ஐபிவி 6 முகவரி [::] கிடைக்கக்கூடிய எல்லா முகவரிகளிலும் பரேசிப் கேட்க வைக்கிறது. காலியாக இருந்தால் (தொழிற்சாலை இயல்புநிலை), கிடைக்கக்கூடிய அனைத்து முகவரிகளிலும் ஏதேனும் துறைமுகத்தில் பரெசிப் கேட்கிறது. பரேசிப் எந்த ஐபி முகவரிகளைப் பயன்படுத்துகிறது என்பதைத் தேர்வுசெய்கிறது. IPv4 அல்லது IPv6 தேர்ந்தெடுக்கப்பட்டால், PARESIP IPv4 அல்லது IPv6 முகவரிகளை மட்டுமே பயன்படுத்துகிறது. இரண்டுமே தேர்வு செய்யப்படாவிட்டால், பரெசிப் ஐபிவி 4 மற்றும் ஐபிவி 6 முகவரிகளைப் பயன்படுத்துகிறது. டிஎன்எச் சேவையகங்கள் கமா டிஎன்எச் சேவையகங்களின் முகவரிகளின் பட்டியல். வழங்கப்படாவிட்டால், டிஎன்எச் சேவையக முகவரிகள் கணினியிலிருந்து மாறும் வகையில் பெறப்படுகின்றன. ஒவ்வொரு டிஎன்எச் முகவரியும் \'ஐபி: துறைமுகம்\' அல்லது \'ஐபி\' வடிவத்தில் இருக்கும். துறைமுகம் தவிர்க்கப்பட்டால், அது இயல்புநிலையாக 53 ஆக இருக்கும். ஐபி ஒரு ஐபிவி 6 முகவரி மற்றும் துறைமுகம் வழங்கப்பட்டால், ஐபி அடைப்புக்குறிக்குள் எழுதப்பட வேண்டும் []. உதாரணமாக, பட்டியல் \'8.8.8.8:53, LEISER2001:4860:4860:8888]: :53\' ஐ ஐபிவி 4 மற்றும் பொது கூகிள் டிஎன்எச் சேவையகங்களின் ஐபிவி 6 முகவரிகளுக்கு புள்ளிகள். தவறான டிஎன்எச் சேவையகங்கள் டிஎன்எச் சேவையகங்களை அமைப்பதில் தோல்வி டி.எல்.எச் சான்றிதழ் கோப்பு சரிபார்க்கப்பட்டால், TLS சான்றிதழ் கொண்ட ஒரு கோப்பு மற்றும் இந்த PARESIP நிகழ்வின் தனிப்பட்ட விசை ஆகியவை ஏற்றப்பட்டுள்ளன அல்லது ஏற்றப்படும். ஆண்ட்ராய்டு பதிப்பு 9 இல், \'cert.pem\' எனப்படும் கோப்பு பதிவிறக்க கோப்புறையிலிருந்து ஏற்றப்படுகிறது. பாதுகாப்பு காரணங்களுக்காக, ஏற்றப்பட்ட பிறகு கோப்பை நீக்கவும். சேவையக சான்றிதழ்களை சரிபார்க்கவும் சரிபார்க்கப்பட்டால், டி.எல்.எச் போக்குவரத்து பயன்படுத்தப்படும்போது எச்ஐபி பயனர் முகவர் மற்றும் எச்ஐபி பதிலாள் சேவையகங்களின் டிஎல்எச் சான்றிதழ்களை பாரெசிப் சரிபார்க்கிறது. TLS CA கோப்பு ஆடியோ அமைப்புகள் சரிபார்க்கப்பட்டால், ஆண்ட்ராய்டு OS இல் சேர்க்கப்படாத அத்தகைய சான்றிதழ் அதிகாரிகளின் TLS சான்றிதழ்களைக் கொண்ட ஒரு கோப்பு அல்லது ஏற்றப்படும். ஆண்ட்ராய்டு பதிப்பு 9 இல், \'Ca_certs.crt\' என்ற கோப்பு பதிவிறக்க கோப்புறையிலிருந்து ஏற்றப்படுகிறது. அவைத்தலைவர் தொலைபேசி இந்த தசம எண்ணால் மைக்ரோஃபோன் அளவைப் பெருக்கவும். மைக்ரோஃபோன் ஆதாயத்தை முடக்கும் குறைந்தபட்ச மதிப்பு 1.0 (தொழிற்சாலை இயல்புநிலை) ஆகும். பெரிய மதிப்புகள் ஆடியோ தரத்தை எதிர்மறையாக பாதிக்கலாம். சரிபார்க்கப்பட்டால், அழைப்பு தொடங்கும் போது ச்பீக்கர் தொலைபேசி தானாக இயக்கப்படும். ஆடியோ தொகுதிகள் சரிபார்க்கப்பட்ட தொகுதிகள் வழங்கிய ஆடியோ கோடெக்குகள் கணக்குகளால் பயன்படுத்தப்படுகின்றன. தொகுதி ஏற்றுவதில் தோல்வி. தவறான மைக்ரோஃபோன் ஆதாய மதிப்பு ஓபச் பிட் வீதம் ஓபச் ஆடியோ ச்ட்ரீம் பயன்படுத்தும் சராசரி அதிகபட்ச பிட் வீதம். செல்லுபடியாகும் மதிப்புகள் 6000-510000. தொழிற்சாலை இயல்புநிலை 28000 ஆகும். எதிர்பார்க்கப்படும் ஓபச் பாக்கெட் இழப்பு எதிர்பார்க்கப்படும் ஓபச் ஆடியோ ச்ட்ரீம் பாக்கெட் இழப்பு விழுக்காடு, 0–100 முதல். தொழிற்சாலை இயல்புநிலை மதிப்பு 1. மதிப்பு 0 மேலும் ஓபச் முன்னோக்கி பிழை திருத்தம் (FEC) ஐ முடக்குகிறது. ஆடியோ நேரந்தவறுகை தவறான ஆடியோ நேரந்தவறுகை \'%1$s\'. செல்லுபடியாகும் மதிப்புகள் 100 முதல் 3000 வரை. இயல்புநிலை அழைப்பு தொகுதி அமைக்கப்பட்டால், இயல்புநிலை அழைப்பு ஆடியோ தொகுதி 1–10. தொனி நாடு அழைப்பு ரிங்கிங், காத்திருப்பு மற்றும் காலீ பிசியான டோன்களின் நாடு அழைப்பு நிறுவப்படும்போது காலியில் இருந்து ஆடியோவைக் காத்திருக்க நேரம் (மில்லி விநாடிகளில்). அழைப்பின் தொடக்கத்தில் காலியிலிருந்து ஆடியோவைத் தவறவிட்டால் அதிக மதிப்புக்கு அமைக்கவும். இருண்ட கருப்பொருள் இருண்ட காட்சி கருப்பொருள் கட்டாயப்படுத்துங்கள் வீடியோ பிரேம் அளவு கடத்தப்பட்ட வீடியோ பிரேம்களின் அளவு (அகலம் ஃச் உயரம்) நொடிக்கு வீடியோ பிரேம்கள் தொழிற்சாலை இயல்புநிலை மதிப்புகளுக்கு அமைப்புகளை மீட்டமைக்க விரும்புகிறீர்களா? எச்.டி.பி ஏண்ட்சேக்கின் போது வழங்கப்படும் வீடியோ பிரேம் வீதம். செல்லுபடியாகும் மதிப்புகள் 10 முதல் 30 வரை. தனிப்பயன் SIP கோரிக்கை/பதில் பயனர்-முகவர் தலைப்பு புல மதிப்பு தவறான பயனர்-முகவர் தலைப்பு புல மதிப்பு பேராசிப் தொடர்புகள், ஆண்ட்ராய்டு தொடர்புகள் அல்லது இரண்டும் பயன்படுத்தப்பட்டால் தேர்வுசெய்கின்றன. இரண்டும் பயன்படுத்தப்பட்டு, ஒரே பெயருடன் ஒரு தொடர்பு இரு தொடர்புகளிலும் இருந்தால், பெரெசிப் தொடர்பு தேர்ந்தெடுக்கப்படும். இரண்டும் பிழைத்திருத்தம் சரிபார்க்கப்பட்டால், பிழைத்திருத்த மற்றும் செய்தி நிலை பதிவு செய்திகளை Logcat க்கு வழங்குகிறது. சிப் சுவடு சரிபார்க்கப்பட்டால், பிழைத்திருத்தத்தை சரிபார்க்கினால், லோகாட் செய்திகளில் SIP கோரிக்கை மற்றும் மறுமொழி சுவடு ஆகியவை அடங்கும். பரேசிப் தொடக்கத்தில் தானாக சரிபார்க்கப்படாமல். தொழிற்சாலை இயல்புநிலைகளுக்கு மீட்டமைக்கவும் சரிபார்க்கப்பட்டால், அமைப்புகள் தொழிற்சாலை இயல்புநிலை மதிப்புகளுக்கு மீட்டமைக்கப்படும். மீட்டமை \'Cert.pem\' கோப்பைப் படிக்கத் தவறிவிட்டது. புதிய அமைப்புகளை செயல்படுத்த நீங்கள் பேராசிப்பை மறுதொடக்கம் செய்ய வேண்டும். இப்போது மறுதொடக்கம் செய்யவா? ஒப்புதல் கோரிக்கை \'Ca_certs.crt\' கோப்பைப் படிக்கத் தவறிவிட்டது. ஆண்ட்ராய்டு தொடர்புகள் தேர்ந்தெடுக்கப்பட்டால், அவை SIP மற்றும் TEL URIS பற்றிய குறிப்புகளாக அழைப்பு மற்றும் செய்தியிடலில் பயன்படுத்தப்படலாம். பரேசிப் பயன்பாடு ஆண்ட்ராய்டு தொடர்புகளை சேமிக்காது அல்லது அவற்றை யாருடனும் பகிராது. ஆண்ட்ராய்டு தொடர்புகளை Paresip இல் கிடைக்கச் செய்வதற்கு, இங்கே விவரிக்கப்பட்டுள்ளபடி அவற்றின் பயன்பாட்டை நீங்கள் ஏற்றுக்கொள்ள வேண்டும் மற்றும் பயன்பாட்டின் தனியுரிமைக் கொள்கை . புதிய தொடர்பு பெயர் சிப் அல்லது டெல் யூரி பயனர்@டொமைன் அல்லது தொலைபேசி எண் பிடித்த தவறான தொடர்பு பெயர் \'%1$s\' \'%1$s\' ஐ தொடர்பு கொள்ளுங்கள். சரிபார்க்கப்பட்டால், இந்த தொடர்பு ஆண்ட்ராய்டு தொடர்புகளில் சேர்க்கப்படும். சுயவிவர படம் தொடர்புகள் \'%1$s\' க்கு அழைக்க அல்லது செய்தியை அனுப்ப விரும்புகிறீர்களா? செய்தி அனுப்பவும் \'%1$s\' என்ற தொடர்பை நீக்க விரும்புகிறீர்களா? விழிப்புணர்வு தகவல் அறிவிப்பு ரத்துசெய் சரி ஆம் இல்லை உறுதிப்படுத்தல் அநாமதேய தெரியவில்லை தவறான SIP அல்லது TEL யூரி \'%1$s\' காப்புப்பிரதி பற்றி மறுதொடக்கம் வெளியேறு அழைக்கவும்… இருந்து அழைக்கவும்… \'%1$s\' உடன் வீடியோவை அனுப்புவதையும் பெறுவதையும் ஏற்றுக்கொள்கிறீர்களா? \'%1$s\' க்கு வீடியோ அனுப்புவதை ஏற்றுக்கொள்கிறீர்களா? \'%1$s\' இலிருந்து வீடியோவைப் பெறுவதை ஏற்றுக்கொள்கிறீர்களா? திருப்பி விடப்பட்டது… கணக்கு \'%1$s\' தொலைபேசி வழங்குநர் இல்லை வீடியோ அழைப்பு வீடியோ கோரிக்கை அழைப்பு நிறுத்தி வைக்கப்பட்டுள்ளது அழைப்பு இணைக்கப்படாதபோது மட்டுமே பதிவை இயக்கலாம் அல்லது முடக்கலாம் அழைப்பு பரிமாற்றம் குருட்டு கலந்து கொண்டார் இடத்தை மாற்றவும் இலக்கு யூரி ஐத் தேர்வுசெய்க இடமாற்றம் டி.டி.எம்.எஃப் அழைப்பு செய்தி இடமாற்றம் தோல்வியுற்றது செய்தி எதுவும் கிடைக்கவில்லை காலம்: %1$d (நொடி) கோடெக்குகள் தற்போதைய வீதம்: %1$s (kbits/s) சராசரி வீதம்: %1$s (kbits/s) பாக்கெட்டுகள் இழந்தது நடுக்கம்: %1$s (எம்.எச்) குரல் அஞ்சல் செய்திகள் உங்களிடம் உள்ளது ஒரு புதிய செய்தி கேளுங்கள் புதிய செய்திகள் ஒரு பழைய செய்தி பழைய செய்திகள் மற்றும் உங்களிடம் செய்திகள் இல்லை உங்களிடம் ஏற்கனவே செயலில் அழைப்பு உள்ளது. பரேசிப் தொடங்கத் தவறிவிட்டார். இது தவறான அமைப்புகளின் மதிப்பு காரணமாக இருக்கலாம். கேளுங்கள் முகவரி, TLS சான்றிதழ் கோப்பு மற்றும் TLS CA கோப்பை சரிபார்க்கவும். பின்னர் பரேசிப்பை மறுதொடக்கம் செய்யுங்கள். %1$s இன் பதிவு தோல்வியுற்றது. கோரிக்கையை சரிபார்க்கவும் SAS <%1$s> ஐ சரிபார்க்க விரும்புகிறீர்களா? பரிமாற்ற கோரிக்கை இந்த அழைப்பை \'%1$s\' க்கு மாற்ற ஏற்றுக்கொள்கிறீர்களா? அழைப்பு கோரிக்கை \'%1$s\' என்று அழைப்பதற்கான கோரிக்கையை நீங்கள் ஏற்றுக்கொள்கிறீர்களா? \'%1$s\' \\ க்கு தானியங்கி திருப்பிவிடுதல் \\ கோரிக்கையை திருப்பி விடுங்கள் \'%1$s\' க்கு அழைப்பு திருப்பிவிடுவதை நீங்கள் ஏற்றுக்கொள்கிறீர்களா? அழைப்பு தோல்வியடைந்தது அழைப்பு மூடப்பட்டுள்ளது இந்த அழைப்பு பாதுகாப்பாக இல்லை! இந்த அழைப்பு பாதுகாப்பானது, ஆனால் பியர் சரிபார்க்கப்படவில்லை! இந்த அழைப்பு பாதுகாப்பானது மற்றும் பியர் சரிபார்க்கப்பட்டது! சகாக்களை அவிழ்க்க விரும்புகிறீர்களா? திறக்கவும் பயன்பாட்டு தரவு (பதிவுகள் தவிர) \'%1$s\' கோப்பில் காப்பக்கப்பட்டுள்ளது. ஆண்ட்ராய்டு பதிப்பு 9 இல், கோப்பு பதிவிறக்க கோப்புறையில் உள்ளது. \'%1$s\' ஐ தாக்கல் செய்ய பயன்பாட்டு தரவை காப்புப் பிரதி எடுக்கத் தவறிவிட்டது. பயன்பாடுகளை சரிபார்க்கவும் → Paresip → அனுமதிகள் → சேமிப்பிடம். பயன்பாட்டு தரவு மீட்டெடுக்கப்பட்டது. பரேசிப் மறுதொடக்கம் செய்யப்பட வேண்டும். இப்போது மறுதொடக்கம் செய்யவா? பயன்பாட்டு தரவை மீட்டெடுப்பதில் தோல்வி. நீங்கள் சரியான கடவுச்சொல்லை வழங்கியிருக்கிறீர்களா என்பதையும், காப்புப்பிரதி கோப்பு இந்த பயன்பாட்டிலிருந்து வந்ததா என்பதையும் சரிபார்க்கவும். ஆண்ட்ராய்டு பதிப்புகள் 9 இல், பயன்பாடுகளையும் சரிபார்க்கவும் → Paresip → அனுமதிகள் → சேமிப்பிடம் மற்றும் அந்த கோப்பு \'%1$s\' பதிவிறக்க கோப்புறையில் உள்ளது. பயன்பாட்டு தரவை மீட்டெடுப்பதில் தோல்வி. ஆண்ட்ராய்டு பதிப்பு 14 மற்றும் அதற்கு மேற்பட்டவை %1$s பதிப்பு %2$s க்கு முன் காப்புப் பிரதி எடுக்கப்பட்ட தரவை மீட்டமைக்க அனுமதிக்காது. \"அறிவிப்புகள்\" இசைவு இல்லாமல் இந்த பயன்பாட்டை நீங்கள் பயன்படுத்த முடியாது. குரல் அழைப்புகளுக்கு BARESIP க்கு \"மைக்ரோஃபோன்\" இசைவு தேவை. வீடியோ அழைப்புகளைச் செய்ய அல்லது பதிலளிக்க \"கேமரா\" இசைவு வழங்கவும். \"சேமிப்பு\" இசைவு இல்லாமல் காப்புப்பிரதியை உருவாக்க முடியாது. \"சேமிப்பு\" அனுமதியின்றி காப்புப்பிரதியை மீட்டெடுக்க முடியாது. \"தொடர்புகள்\" அனுமதியின்றி நீங்கள் ஆண்ட்ராய்டு தொடர்புகளை அணுக முடியாது. காணொளி நிழற்படபதிப்பான்கள் எதுவும் இல்லை! பிணைய இணைப்பு இல்லை! ஆடியோ கவனம் மறுக்கப்பட்டது! அனுமதிகள் பகுத்தறிவு குரல் அழைப்புகளுக்கான \"மைக்ரோஃபோன்\" இசைவு தேவை, ஊடலை மைக்ரோஃபோன்/ச்பீக்கர் கண்டறிதலுக்கான \"அருகிலுள்ள சாதனங்கள்\" இசைவு, அறிவிப்புகளை இடுகையிடுவதற்கான \"அறிவிப்புகள்\" இசைவு, மற்றும் காப்புப்பிரதி/மீட்டெடுப்பு செயல்பாடுகளுக்கான ஆண்ட்ராய்டு 9 \"சேமிப்பிடம்\" இசைவு. PARESIP+ தேவை குரல் அழைப்புகளுக்கான \"மைக்ரோஃபோன்\" இசைவு, வீடியோ அழைப்புகளுக்கான \"கேமரா\" இசைவு, \"அருகிலுள்ள சாதனங்கள்\" ஊடலை மைக்ரோஃபோன்/ச்பீக்கர் கண்டறிதலுக்கான இசைவு, அறிவிப்புகளை இடுகையிடுவதற்கான \"அறிவிப்புகள்\" இசைவு, மற்றும் ஆண்ட்ராய்டு 9 \"சேமிப்பிடம்\" காப்புப்பிரதி/மீட்டெடுப்பு நடவடிக்கைகளுக்கான அனுமதிகள். நீங்கள் தொடர்புகளுக்கு \'%1$s\' ஐ சேர்க்க விரும்புகிறீர்களா அல்லது அழைப்பு வரலாற்றிலிருந்து %2$s ஐ நீக்க விரும்புகிறீர்களா? எண் விசைப்பலகை" சரிபார்க்கப்பட்டால், \"அழைப்பு…\" புலம் கவனம் செலுத்தும்போது எண் விசைப்பலகை காண்பிக்கப்படும்." ரிங்டோன் பதில் சேமி அழைப்பு பதிவு ஒலிவாங்கி அழைப்பின் போது செயல்படுத்தப்பட்டால், மைக்ரோஃபோன் முடக்கப்பட்டுள்ளது. செயல்படுத்தப்பட்டால், சாதன ச்பீக்கர்ஃபோன் வழியாக ஆடியோ இயக்கப்படுகிறது. வெளிப்புற சேமிப்பு வாசிப்பு இசைவு இல்லை ரிங்டோனைத் தேர்ந்தெடுக்கவும் செயல்படுத்தப்பட்டால், புதிய உள்வரும் மற்றும் வெளிச்செல்லும் அழைப்புகள் பதிவு செய்யப்படும். அழைப்பு விவரங்கள் பக்கத்தில் பதிவுகளை இயக்கலாம் ச்பீக்கர்ஃபோன் PARESIP நூலக அடிப்படையிலான SIP பயனர் முகவர்

சுஆ எய்னனென் & lt; jh@tutpro.com>

பதிப்பு %1$s


பயன்பாட்டு குறிப்புகள்

  • பேராசிப்பின் அமைப்புகளில் இயல்புநிலை மதிப்புகள் உங்கள் தேவைகளைப் நிறைவு செய்யுங்கள் (உதவிக்கு உருப்படி தலைப்புகளைத் தொடவும்).
  • பின்னர் கணக்குகளில், ஒன்று அல்லது அதற்கு மேற்பட்ட கணக்குகளை உருவாக்கவும் (மீண்டும் உதவிக்கு உருப்படி தலைப்புகளைத் தொடவும்).
  • ஒரு கணக்கின் பதிவு நிலை வண்ண புள்ளியுடன் காட்டப்பட்டுள்ளது: பச்சை (பதிவு வெற்றி பெற்றது), மஞ்சள் (பதிவு நடந்து கொண்டிருக்கிறது), சிவப்பு (பதிவு தோல்வியுற்றது), வெள்ளை (பதிவு செயல்படுத்தப்படவில்லை).
  • நிலை புள்ளியைத் தொடுவது நேரடியாக கணக்கு உள்ளமைவுக்கு வழிவகுக்கிறது.
  • பரேசிப் பார் ஐகான்களில் நீண்ட தொடுதல் ஐகான்களைப் பற்றிய தகவல்களைக் காட்டுகிறது.
  • சைகை ச்வைப் கீழே காட்டப்பட்ட கணக்கை மீண்டும் பதிவு செய்கிறது.
  • தற்போது காட்டப்பட்ட கணக்கில் நீண்ட தொடுதல் கணக்கின் பதிவை இயக்குகிறது அல்லது முடக்குகிறது.
  • இடது/வலது சைகை கணக்குகளுக்கு இடையில் மாற்றுகிறது.
  • காலீ காலியாக இருக்கும்போது அழைப்பு ஐகானைத் தொடுவதன் மூலம் முந்தைய அழைப்பு விருந்தை மீண்டும் காணலாம்.
  • அழைப்புகள் மற்றும் செய்திகளின் சகாக்கள் நீண்ட தொடுதல்களால் தொடர்புகளில் சேர்க்கப்படலாம்.
  • அழைப்புகள், அரட்டைகள், செய்திகள் மற்றும் தொடர்புகளை அகற்ற நீண்ட தொடுதல்களைப் பயன்படுத்தலாம்.
  • தொடர்பு ஐகானில் தொடுதல்/நீண்ட தொடுதல் பட அவதாரத்தை நிறுவ/அகற்ற பயன்படுத்தலாம்.
  • கோடெக்கை இயக்க/முடக்க ஆடியோ கோடெக்கில் நீண்ட தொடுதல் பயன்படுத்தப்படலாம்.
  • விக்கி ஐப் பார்க்கவும்.

தனியுரிமைக் கொள்கை

தனியுரிமைக் கொள்கை இங்கே .


மூலக் குறியீடு

மூலக் குறியீடு github , சிக்கல்களைப் புகாரளிக்கலாம்.


உரிமங்கள்

  • BSD-3- அடைப்பு பின்வருவதைத் தவிர:
  • அப்பாச்சி 2.0 AMR கோடெக்குகள் மற்றும் TLS பாதுகாப்பு
  • agplv4 ZRTP மீடியா குறியாக்க
  • குனு எல்சிபிஎல் 2.1 ஐயா .722, ஐயா .726, மற்றும் கோடெக் 2 கோடெக்ச்
  • gnu gplv3 G.729 கோடெக்
]]>
சரிபார்க்கப்பட்டால், தொடர்புகள் பட்டியலில் முதலிடத்தில் உள்ள பிற பிடித்தவைகளில் தொடர்பு காட்டப்படும். வன்பொருள் ஒலி எதிரொலி ரத்து செய்யப்படவில்லை! வீடியோ அழைப்புகள் உடன் PARESIP நூலக அடிப்படையிலான SIP பயனர் முகவர்

சுஆ எய்னனென் & lt; jh@tutpro.com>

பதிப்பு %1$s


பயன்பாட்டு குறிப்புகள்

  • paresip+\ இன் அமைப்புகளில் இயல்புநிலை மதிப்புகள் உங்கள் தேவைகளைப் நிறைவு செய்யுங்கள் (உதவிக்கு உருப்படி தலைப்புகளைத் தொடவும்).
  • பின்னர் கணக்குகளில், ஒன்று அல்லது அதற்கு மேற்பட்ட கணக்குகளை உருவாக்கவும் (மீண்டும் உதவிக்கு உருப்படி தலைப்புகளைத் தொடவும்).
  • ஒரு கணக்கின் பதிவு நிலை வண்ண புள்ளியுடன் காட்டப்பட்டுள்ளது: பச்சை (பதிவு வெற்றி பெற்றது), மஞ்சள் (பதிவு நடந்து கொண்டிருக்கிறது), சிவப்பு (பதிவு தோல்வியுற்றது), வெள்ளை (பதிவு செயல்படுத்தப்படவில்லை).
  • நிலை புள்ளியைத் தொடுவது நேரடியாக கணக்கு உள்ளமைவுக்கு வழிவகுக்கிறது.
  • பரேசிப் பார் ஐகான்களில் நீண்ட தொடுதல் ஐகான்களைப் பற்றிய தகவல்களைக் காட்டுகிறது.
  • சைகை ச்வைப் கீழே காட்டப்பட்ட கணக்கை மீண்டும் பதிவு செய்கிறது.
  • தற்போது காட்டப்பட்ட கணக்கில் நீண்ட தொடுதல் கணக்கின் பதிவை இயக்குகிறது அல்லது முடக்குகிறது.
  • இடது/வலது சைகை கணக்குகளுக்கு இடையில் மாற்றுகிறது.
  • காலீ காலியாக இருக்கும்போது அழைப்பு ஐகானைத் தொடுவதன் மூலம் முந்தைய அழைப்பு விருந்தை மீண்டும் காணலாம்.
  • அழைப்புகள் மற்றும் செய்திகளின் சகாக்கள் நீண்ட தொடுதல்களால் தொடர்புகளில் சேர்க்கப்படலாம்.
  • அழைப்புகள், அரட்டைகள், செய்திகள் மற்றும் தொடர்புகளை அகற்ற நீண்ட தொடுதல்களைப் பயன்படுத்தலாம்.
  • தொடர்பு ஐகானின் தொடுதல்/நீண்ட தொடுதல் பட அவதாரத்தை நிறுவ/அகற்ற பயன்படுத்தலாம்.
  • கோடெக்கை இயக்க/முடக்க ஆடியோ அல்லது வீடியோ கோடெக்கில் நீண்ட தொடுதல் பயன்படுத்தப்படலாம்.
  • விக்கி ஐக் காண்க தகவல்.

அறியப்பட்ட சிக்கல்கள்

  • வீடியோ அழைப்புகளில், சாதனம் நிலப்பரப்பில் நடத்தப்பட வேண்டும் உருவப்படம் நோக்குநிலையிலிருந்து 90 டிகிரி மீதமுள்ள பயன்முறை சுழற்றப்பட்டது.
  • வீடியோ ச்ட்ரீம் அனுப்பும்போது தன்வய பார்வை சரியாகக் காட்டப்படவில்லை.

தனியுரிமைக் கொள்கை

தனியுரிமைக் கொள்கை இங்கே .


மூலக் குறியீடு

மூலக் குறியீடு github , சிக்கல்களைப் புகாரளிக்கலாம்.


உரிமங்கள்

  • BSD-3- அடைப்பு பின்வருவதைத் தவிர:
  • அப்பாச்சி 2.0 AMR கோடெக்குகள் மற்றும் TLS பாதுகாப்பு
  • agplv4 ZRTP மீடியா குறியாக்க
  • குனு எல்சிபிஎல் 2.1 ஐயா .722, ஐயா .726, மற்றும் கோடெக் 2 கோடெக்ச்
  • gnu gplv3 G.729 கோடெக்
  • குனு சி.பி.எல்.வி 2 எச் .264 மற்றும் எச் .265 கோடெக்ச்
  • aomedia av1 கோடெக்
]]>
பதிவு விளையாடுவது…
================================================ FILE: app/src/main/res/values-uk/strings.xml ================================================ ================================================ FILE: app/src/main/res/values-zh-rCN/strings.xml ================================================ 关于 baresip 关于 baresip+ 账号 昵称 账号昵称“%1$s”无效 昵称“%1$s”已存在 昵称(如果有)用于在 baresip 应用中识别此账号。 显示名称 名称(如果有)用于在出站请求中的 From URI。 显示名称“%1$s”无效 身份验证用户名 如果需要对 SIP 请求进行身份验证,则输入身份验证用户名。默认值是账号的用户名。 身份验证用户名“%1$s”无效 身份验证密码 身份验证密码最多 64 个字符。如果提供了身份验证用户名,但未提供密码,则在启动 baresip 时会询问。 身份验证密码“%1$s”无效 出站代理 当发送请求时,必须使用一个或两个代理的 SIP URI。如果指定两个,则 REGISTER 请求将同时发送给二者,其他请求则发送给最先响应的一方。如果没有指定出站代理,则根据被叫 URI 主机部分的 DNS NAPTR/SRV/A 记录查找发送请求。如果 SIP URI 的主机部分是 IPv6 地址,则该地址必须写在方括号 [] 内。\n示例:\n • sip:example.com:5061;transport=tls \n • sip:[2001:67c:223:777::10];transport=tcp \n • sip:192.168.43.50:443;transport=wss 代理服务器的 SIP URI 另一个代理服务器的 SIP URI 代理服务器 URI“%1$s”无效 注册 如果选中,则启用注册,并按照注册间隔指定的间隔发送 REGISTER 请求。 注册间隔 告诉 baresip 发送 REGISTER 请求的频率(以秒为单位)。有效值在 60 到 3600 之间。 注册间隔“%1$s”无效 媒体 NAT 穿越 选择媒体 NAT 穿越协议(如果有)。可能的选择是 STUN( NAT 会话穿越实用程序,RFC 5389)和 ICE(交互式连接建立,RFC 5245)。 STUN/TURN 服务器 STUN/TURN 服务器 URI 的格式为 scheme:host[:port][?transport=udp|tcp],其中 scheme 为“stun”、“stuns”、“turn”或“turns”。STUN 和 ICE 协议的出厂默认 STUN 服务器是指向公共 Google STUN 服务器的“stun:stun.l.google.com:19302”。没有出厂默认的 TURN 服务器。 STUN/TURN 服务器 URI“%1$s”无效 STUN/TURN 用户名 用户名(如果 STUN/TURN 服务器需要) 用户名“%1$s”无效 STUN/TURN 密码 密码(如果 STUN/TURN 服务器需要) 密码“%1$s”无效 媒体加密 选择媒体传输加密协议(如果有)。\n • ZRTP(推荐)意味着在建立呼叫后尝试通过 ZRTP 进行端到端媒体加密协商。\n • DTLS-SRTPF 意味着在传出呼叫中提供 UDP/TLS/RTP/SAVPF,如果在传入呼叫中提供 RTP/SAVP、RTP/SAVPF、UDP/TLS/RTP/SAVP 或 UDP/TLS/RTP/SAVPF 则使用。\n • SRTP-MANDF 意味着在传出呼叫中提供 RTP/SAVPF,在传入呼叫中也必须提供。\n • SRTP-MAND 意味着在传出呼叫中提供 RTP/SAVP,在传入呼叫中也必须提供。\n • SRTP 意味着在传出呼叫中提供 RTP/AVP,如果在传入呼叫中提供 RTP/SAVP 或 RTP/SAVPF 则使用。 RTCP 多路复用 如果选中,RTP 和 RTCP 数据包会在单个端口上多路复用(RFC 5761)。 可靠临时响应 如果选中,则表示支持可靠临时响应(RFC 3262)。 DTMF 模式 选择 DTMF 音的发送方式(0–9、#、* 和 A-D)。 带内 RTP 事件 SIP INFO 请求 带内 RTP 或 SIP INFO 接听模式 选择接听来电的方式。 重定向模式 选择是否会自动遵循呼叫重定向请求,或者是否请求确认。 手动 自动 语音信箱 URI 用于检查语音信箱消息的 SIP URI。如果留空,则不订阅语音信箱消息(消息等待指示)。 国家/地区代码 此账号的 E.164 国家/地区代码。如果来电或消息的 From URI 用户部分包含不以“+”符号开头的电话号码,并且如果联系人查找失败,则该号码将以该国家/地区代码作为前缀,并再次尝试联系人查找。如果电话号码以单数字“0”开头,则在号码前缀之前移除数字“0”。 国家/地区代码“%1$s”无效 电话服务提供者 用于拨打电话号码的 SIP URI 主机部分。出厂默认为账号的域名。如果未提供,则此账号不能用于拨打电话号码。 数字键盘" 如果选中,当“呼叫…”字段聚焦时,将显示数字键盘。" SIP URI 主机部分“%1$s”无效 默认账号 如果选中,则在启动 baresip 时选择此账号。 账号 新账号的 SIP URI 新账号的 SIP URI 格式为 <user>@<domain>[:<port>][;transport=udp|tcp|tls]。如果指定了 <port>,但未指定传输协议,则传输协议默认为 UDP。如果未指定 <port>,但指定了传输协议,则 <port> 默认为 5060 或 5061(TLS)。如果二者均未指定,也没有指定出站代理,则将仅根据域名的 DNS 信息确定账号的注册服务器(如果有)。 user@domain[:port][;transport=udp|tcp|tls]“%1$s”无效 账号“%1$s”已存在。 无法分配新账号。 加密密码 解密密码 是否要删除账号“%1$s”? 回复 保存 未接来电来自 未接来电 %1$d 个未接来电 通话转接请求至 已自动拒绝来自 %1$s 的通话 通话记录 通话详情 呼叫 通话 通话 对方 方向 时间 时长 正在播放录音… 您要将“%1$s”添加到联系人中,还是从通话记录中删除 %2$s? 是否要从通话记录中删除“%1$s”%2$s? 禁用 启用 是否要删除账号“%1$s”的通话记录? 通话已接听 通话已在别处接听 通话未接 通话遭拒 与 %1$s 聊天 新消息 您要删除消息还是将对方“%1$s”添加到联系人中? 是否要删除消息? 添加联系人 消息发送失败 失败 聊天记录 今天 新的聊天伙伴 您要删除与对方“%1$s”的聊天,还是将对方添加到联系人中? 是否要删除与“%1$s”的聊天? 是否要删除账号“%1$s”的聊天记录? 音频编解码器 视频编解码器 设置 自动启动 如果选中,在设备启动后或安装新版本 baresip 后,baresip 将自动启动。启动会延迟至设备解锁后执行。 自动启动需要“悬浮窗”权限。 电池优化 如果您想降低 Android 限制 baresip 访问网络或让 baresip 进入待机状态的概率,请禁用电池优化(推荐)。 默认电话应用 拨号器角色不可用 如果选中,则 baresip 是默认电话应用。如果您的设备还需要处理除 SIP 通话或消息之外的其他通话或消息,请勿选中。 监听地址 baresip 监听传入 SIP 请求的 IP 地址和端口,格式为“地址:端口”。如果 IP 地址是 IPv6 地址,则必须将其写在方括号 [] 内。IPv4 地址 0.0.0.0 或 IPv6 地址 [::] 使 baresip 监听所有可用地址。如果留空(出厂默认),baresip 将在所有可用地址的任意端口上监听。 监听地址无效 地址族 选择 baresip 正在使用的 IP 地址。如果选择 IPv4 或 IPv6,则 baresip 仅使用 IPv4 或 IPv6 地址。如果两者都没有选择,baresip 将同时使用 IPv4 和 IPv6 地址。 DNS 服务器 以英文逗号分隔的 DNS 服务器地址列表。如果未指定,则从系统中动态获取 DNS 服务器地址。每个 DNS 地址的格式为“ip:端口”或“ip”。如果省略端口,则默认为 53。如果 ip 是 IPv6 地址,并且指定了端口,则 ip 必须写在方括号 [] 内。例如,列表“8.8.8.8:53,[2001:4860:4860::8888]:53”指向公共 Google DNS 服务器的 IPv4 和 IPv6 地址。 DNS 服务器无效 无法设置 DNS 服务器 TLS 证书文件 如果选中,则已加载或将加载包含此 baresip 实例的 TLS 证书和私钥的文件。在 Android 9 中,会从下载文件夹加载名为“cert.pem”的文件。出于安全原因,请在加载后删除该文件。 验证服务器证书 如果选中,则在使用 TLS 传输时,baresip 会验证 SIP 用户代理和 SIP 代理服务器的 TLS 证书。 TLS CA 文件 如果选中,则已加载或将加载包含 Android 操作系统中未包含的证书颁发机构的 TLS 证书的文件。在 Android 9 中,会从下载文件夹加载名为“ca_certs.crt”的文件。 无外部存储读取权限 音频设置 如果选中,通话开始时扬声器会自动打开。 扬声器 音频模块 已选中的模块提供的音频编解码器可供账号使用。 无法加载模块。 麦克风增益 将麦克风音量乘以此十进制数。最小值为 1.0(出厂默认),禁用麦克风增益。较大的值可能会对音频质量产生负面影响。 麦克风增益值无效 Opus 比特率 Opus 音频流使用的平均最大比特率。有效值为 6000-510000。出厂默认为 28000。 预期 Opus 丢包率 预期 Opus 音频流丢包率,范围为 0–100。出厂默认值为 1。值为 0 时也会关闭 Opus 前向纠错(FEC)。 Opus 比特率无效 Opus 丢包率无效 音频延迟 通话建立后等待被叫方音频的时间(以毫秒为单位)。如果您在通话开始时未收到被叫方的音频,请设置为更高的值。 音频延迟“%1$s”无效。有效值为 100 至 3000。 默认通话音量 如果设置,则默认通话音量为 1–10 级。 来电铃声、等待铃声和被叫忙音的国家/地区 提示音国家/地区 深色主题 强制启用深色显示主题 色盲 使用色盲友好型注册状态图标 近距离感应"> 如果选中,则在通话期间将启用近距离感应功能。 视频帧大小 传输的视频帧大小(宽 × 高) 每秒视频帧数 SDP 握手期间提供的视频帧速率。有效值为 10 至 30。 每秒帧数“%1$d”无效 铃声 选择铃声 用户代理 自定义 SIP 请求/响应 User-Agent 标头字段值 User-Agent 标头字段值无效 选择是否使用 baresip 联系人、Android 联系人或两者都使用。如果两者都使用,并且两个联系人中都存在同名联系人,则将选择 baresip 联系人。 两者 调试 如果选中,则向 Logcat 提供调试和信息级别的日志消息。 SIP 跟踪 如果选中且调试也已选中,则 Logcat 消息还包括 SIP 请求和响应跟踪。在 baresip 启动时自动取消选中。 重置为出厂默认 如果选中,设置将重置为出厂默认值。 是否确定要将设置重置为出厂默认值? 重置 无法读取“cert.pem”文件。 无法读取“ca_certs.crt”文件。 您需要重启 baresip 才能激活新设置。是否立即重启? 同意请求 如果选择了 Android 联系人,可将其作为 SIP 和 tel URI 的引用用于通话和消息。baresip 应用不会存储 Android 联系人,也不会与任何人共享。为了使 Android 联系人在 baresip 中可用,Google 要求您接受此处和应用隐私政策中所述的使用。 新联系人 名称 SIP 或 tel URI user@domain 或电话号码 收藏 如果选中,联系人将显示在联系人列表顶部的其他收藏中。 联系人名称“%1$s”无效 联系人“%1$s”已存在。 如果选中,此联系人将添加到 Android 联系人中。 头像 联系人 您要呼叫还是发送消息至“%1$s”? 发送消息 是否要删除联系人“%1$s”? 提醒 信息 通知 取消 确定 接受 拒绝 添加 删除 编辑 状态 错误 确认 匿名 未知 SIP 或 tel URI“%1$s”无效 备份 恢复 关于 重启 退出 呼叫… 通话来自… 已转接… 账号“%1$s”没有电话服务提供者 视频通话 视频请求 是否接受向“%1$s”发送视频? 是否接受“%1$s”发送和接收视频? 是否接受从“%1$s”接收视频? 通话处于保持状态 仅在未连接通话时才可以打开或关闭录音 通话转接 盲转 协商转接 转接目标 选择目标 URI 转接 转接失败 DTMF 通话信息 无可用信息 时长:%1$d(秒) 编解码器 当前速率:%1$s(Kbits/s) 平均速率:%1$s(Kbits/s) 数据包 丢包数 抖动:%1$s(毫秒) 语音信箱消息 您有 一条新消息 新消息 一条旧消息 旧消息 以及 您没有消息 您已有正在进行的通话。 baresip 无法启动。这可能是由于设置值无效。请检查监听地址、TLS 证书文件和 TLS CA 文件。然后重启 baresip。 注册 %1$s 失败。 验证请求 是否要验证 SAS <%1$s>? 转接请求 是否接受将此通话转接到“%1$s”? 呼叫请求 是否接受呼叫“%1$s”的请求? 自动重定向至“%1$s” 重定向请求 是否接受将通话重定向至“%1$s”? 通话失败 通话已关闭 此通话不安全! 此通话安全,但对方未验证! 此通话安全且对方已验证!是否要取消验证对方? 取消验证 应用程序数据(不包括录音)已备份到文件“%1$s”。在 Android 9 中,该文件位于下载文件夹中。 无法将应用程序数据备份至文件“%1$s”。请检查“应用”→“baresip”→“权限”→“存储”。 重启请求 应用程序数据已恢复。需要重启 baresip。是否立即重启? 无法恢复应用程序数据。请检查您输入的密码是否正确,备份文件是否来自此应用程序。在 Android 9 中,请检查“应用”→“baresip”→“权限”→“存储”,并确保文件“%1$s”存在于下载文件夹中。 无法恢复应用程序数据。Android 14 及更高版本不允许恢复在 %1$s %2$s 版本之前备份的数据。 没有“通知”权限,您无法使用此应用程序。 baresip 需要“麦克风”权限才能进行语音通话。 授予“相机”权限即可拨打或接听视频通话。 没有“存储”权限,您无法创建备份。 没有“存储”权限,您无法恢复备份。 没有“联系人”权限,您无法访问 Android 联系人。 没有支持的视频相机! 无网络连接! 无硬件声学回声消除! 音频焦点被拒绝! baresip 需要“麦克风”权限才能进行语音通话,需要“附近的设备”权限才能检测蓝牙麦克风/扬声器,需要“通知”权限才能发布通知,并且在 Android 9 中需要“存储”权限才能进行备份/恢复操作。 baresip+ 需要“麦克风”权限才能进行语音通话,需要“相机”权限才能进行视频通话,需要“附近的设备”权限才能检测蓝牙麦克风/扬声器,需要“通知”权限才能发布通知,并且在 Android 9 中需要“存储”权限才能进行备份/恢复操作。 权限理由 通话录音 如果激活,新的来电和去电都会被录音。可以在通话详情页面播放录音。 麦克风 如果在通话期间激活,麦克风将静音。 扬声器 如果激活,音频将通过设备扬声器播放。 基于 baresip 库的带视频通话功能的 SIP 用户代理

Juha Heinanen <jh@tutpro.com>

版本 %1$s


使用提示

  • 检查 baresip+ 设置中的默认值是否满足您的需求(触摸项目标题获取帮助)。
  • 然后在“账号”中,创建一个或多个账号(再次触摸项目标题以获取帮助)。
  • 账号的注册状态用彩色圆点显示:绿色(注册成功)、黄色(注册正在进行)、红色(注册失败)、白色(注册尚未激活)。
  • 触摸状态点可直接进入账号配置。
  • 长按 baresip 栏图标可显示有关图标的信息。
  • 向下滑动手势会导致当前显示的账号重新注册。
  • 长按当前显示的账号可启用或禁用账号的注册。
  • 向左/向右滑动手势可在账号之间切换。
  • 当被叫方为空时,可以通过触摸通话图标来重新选择先前的呼叫方。
  • 长按即可将通话和消息联系人添加到联系人中。
  • 长按也可用于移除通话、聊天、消息和联系人。
  • 触摸/长按联系人图标可用于设置/移除头像。
  • 长按音频或视频编解码器可以启用/禁用该编解码器。
  • 如需更多信息,请参阅 Wiki

已知问题

  • 当视频流处于仅发送模式时,自显画面无法正常显示。

隐私政策

隐私政策可在此处获取。


源代码

源代码可在 GitHub 上找到,您也可以在此报告问题。


语言翻译

语言翻译通过 baresip Weblate 项目进行管理。


许可

  • BSD-3-Clause,但以下内容除外:
  • Apache 2.0 AMR 编解码器和 TLS 安全
  • AGPLv4 ZRTP 媒体加密
  • GNU LGPL 2.1 Codec2 编解码器
  • Free G.722 编解码器
  • GNU GPLv3 G.729 编解码器
  • GNU GPLv2 H.264 和 H.265 编解码器
  • AOMedia AV1 编解码器
]]>
基于 baresip 库的 SIP 用户代理

Juha Heinanen <jh@tutpro.com>

版本 %1$s


使用提示

  • 检查 baresip 设置中的默认值是否满足您的需求(触摸项目标题获取帮助)。
  • 然后在“账号”中,创建一个或多个账号(再次触摸项目标题以获取帮助)。
  • 账号的注册状态用彩色圆点显示:绿色(注册成功)、黄色(注册正在进行)、红色(注册失败)、白色(注册尚未激活)。
  • 触摸状态点可直接进入账号配置。
  • 长按 baresip 栏图标可显示有关图标的信息。
  • 向下滑动手势会导致当前显示的账号重新注册。
  • 长按当前显示的账号可启用或禁用账号的注册。
  • 向左/向右滑动手势可在账号之间切换。
  • 当被叫方为空时,可以通过触摸通话图标来重新选择先前的呼叫方。
  • 长按即可将通话和消息联系人添加到联系人中。
  • 长按也可用于移除通话、聊天、消息和联系人。
  • 触摸/长按联系人图标可用于设置/移除头像。
  • 长按音频编解码器可以启用/禁用该编解码器。
  • 如需更多信息,请参阅 Wiki

隐私政策

隐私政策可在此处获取。


源代码

源代码可在 GitHub 上找到,您也可以在此报告问题。


语言翻译

语言翻译通过 baresip Weblate 项目进行管理。


许可

  • BSD-3-Clause,但以下内容除外:
  • Apache 2.0 AMR 编解码器和 TLS 安全
  • AGPLv4 ZRTP 媒体加密
  • GNU LGPL 2.1 Codec2 编解码器
  • Free G.722 编解码器
  • GNU GPLv3 G.729 编解码器
]]>
动态配色 如果在 Android 设置中启用,则使用动态配色 正在呼叫 保存录音 是否要保存此录音? 录音已保存 是否要从通话记录中删除此通话? 停止 屏蔽非联系人列表中的联系人的通话和消息。 屏蔽未知联系人 传输协议 以英文逗号分隔的受支持的 SIP 请求/响应传输协议列表。如果留空,则默认值为“udp,tcp,tls,ws,wss”,其中包含所有受支持的传输协议。仅列出您需要的。出于安全和其他原因,不推荐使用 UDP。 传输协议列表无效 检查来源 如果选中,则仅允许来自发送注册请求的 IP 地址的入站请求。 已屏蔽 已屏蔽的通话 已屏蔽的消息 是否要删除“%1$s”的屏蔽请求? 是否要将对方“%1$s”添加到联系人? 已自动拒绝来自 %1$s 的已屏蔽通话 已自动拒绝来自 %1$s 的已屏蔽消息 搜索 唯一联系人 URI 如果选中,则保证联系人 URI 是唯一的。如果存在多个账号具有相同的 SIP URI 用户部分时需要选中,同时还能保护账号免受攻击。 通话已连接 对方不支持 REPLACES 功能 通话正在响铃 自定义参数 以英文分号分隔的自定义账号参数列表
================================================ FILE: build.gradle.kts ================================================ buildscript { repositories { google() mavenCentral() } dependencies { classpath(libs.gradle) classpath(libs.kotlin.gradle.plugin) } } allprojects { repositories { google() mavenCentral() } } tasks.register("clean", Delete::class) { description = "Deletes the build directory" group = "cleanup" delete(layout.buildDirectory) } ================================================ FILE: fastlane/metadata/android/de-DE/changelogs/10.1.0.txt ================================================ -Kompatibilität wurde für den G722 codec hinzugefügt. - Das Speichern von Accounts wurde repariert. ================================================ FILE: fastlane/metadata/android/de-DE/changelogs/10.2.0.txt ================================================ -Kompatibilität wurde für den G.726 codec hinzugefügt. - Die Grund-Rangliste der codecs wurde verbessert. ================================================ FILE: fastlane/metadata/android/de-DE/changelogs/10.3.0.txt ================================================ - Ein Anruflisten-menü wurde hinzugefügt welches Löschen, Deaktivieren und Aktivieren des Konto Anrufverlaufes erlaubt. -Spanische "Sprach-Strings" wurden hinzugefügt (Dank an Javier Falbo). -Das automatische Starten von Baresip ist jetzt Grundeinstellung auf neuen Installationen. ================================================ FILE: fastlane/metadata/android/de-DE/full_description.txt ================================================ baresip ist eine SIP "User Agent" App für Android, die auf baresip basiert. Aktuell unterstüzt die Baresip App Anrufe mit Ton, Textnachrichten, Anrufbeantworter (mit Anzeige für nicht abgehörte Nachrichten) und Anrufsweiterleitungen (direkt/ mit Zusatzinformationen). Zudem werden Opus, AMR, Codec2, G.729, G.722, G.722.1, G.726, oder PCMU/PCMA Codecs unterstüzt. Eine sichere Verbindung wird mit TLS oder WSS SIP und ZRTP oder (DTLS) SRTP Medienverkapselung sichergestellt. Die Entwicklung der Baresip App wurde durch das Fehlen eines sicheren, quelloffenen und SIP-basierten VoIP "User Agent" für Android motiviert, der nicht auf proprietäre und externe Push-Benachrichtigungsdienste angewiesen ist. Falls Sie Videoanrufe tätigen wollen und ein Gerät mit Android 9 oder höher besitzen, das die Camera2 API auf dem Hardware-Unterstützungslevel LEVEL 3 oder FULL unterstüzt, können Sie die Schwester-App baresip+ nutzen. Der Quellcode befindet sich auf GitHub, dort können sie Auch Fehler/ Bugs melden. ================================================ FILE: fastlane/metadata/android/de-DE/short_description.txt ================================================ VoIP User Agent App für Android basierend auf der baresip SIP Bibliothek ================================================ FILE: fastlane/metadata/android/de-DE/title.txt ================================================ baresip ================================================ FILE: fastlane/metadata/android/el/title.txt ================================================ baresip ================================================ FILE: fastlane/metadata/android/en-US/changelogs/10.0.0.txt ================================================ - Resume baresip to paused activity instead of main activity. ================================================ FILE: fastlane/metadata/android/en-US/changelogs/10.1.0.txt ================================================ - Added support for G722 codec. - Fixed saving of accounts. ================================================ FILE: fastlane/metadata/android/en-US/changelogs/10.2.0.txt ================================================ - Added support for G.726 codec. - Improved default codec priority order. ================================================ FILE: fastlane/metadata/android/en-US/changelogs/10.3.0.txt ================================================ - Added Call History menu that allows deleting, disabling, and enabling of account's call history. - Added Spanish language strings (credits to Javier Falbo). - Auto start of baresip is now default in new installations. ================================================ FILE: fastlane/metadata/android/en-US/changelogs/10.4.0.txt ================================================ - Allow deletion of account's chat history via chats activity menu item. - Ask confirmation before deleting chat and call history. - Improved updating of main activity voicemail, chat, and call icons. - Go directly to Accounts activity when baresip is started without any accounts. - Swapped positions of call Accept and Reject buttons. - Do not allow terminating outgoing call before call attempt has started. ================================================ FILE: fastlane/metadata/android/en-US/changelogs/11.0.0.txt ================================================ - Replaced exporting and importing of accounts and contacts with backup and restore of all application data. - Improved security by disallowing Android based application backup and installation of application to external storage. - When account is deleted, delete also account's call and chat history. - Fixed deleting of messages and chats. - Minor UI and string improvements. ================================================ FILE: fastlane/metadata/android/en-US/changelogs/11.1.0.txt ================================================ - Added 'Bind Address' configuration variable that allows choosing which network interface or IP address baresip is using. - Always update status icon of accounts when resuming to main activity. - Fixed crash when resuming to previously destroyed main activity. - Correctly show active incoming call when resuming to main activity. - Updated Spanish strings. ================================================ FILE: fastlane/metadata/android/en-US/changelogs/11.2.0.txt ================================================ - Added support for AMR narrowband codec. - Added Bulgarian strings. - Renamed main menu Configuration item to Settings. ================================================ FILE: fastlane/metadata/android/en-US/changelogs/11.3.0.txt ================================================ - Added to Settings possibility to choose which audio modules are loaded. Audio codecs provided by the loaded modules are available for accounts to use. - Minor code improvements. ================================================ FILE: fastlane/metadata/android/en-US/changelogs/12.0.0.txt ================================================ - Initial implementation of audio through Bluetooth headset. ================================================ FILE: fastlane/metadata/android/en-US/changelogs/12.1.0.txt ================================================ - Allow port number in account's Address of Record (AoR). - Fixed checking if account already exists. - Updated NO and BG strings. ================================================ FILE: fastlane/metadata/android/en-US/changelogs/12.2.0.txt ================================================ - Used new API functions 'net_set_address' and 'net_set_af' to implement Prefer IPv6 functionality. - Removed Bind Address from Settings (it didn't work as expected). ================================================ FILE: fastlane/metadata/android/en-US/changelogs/12.2.1.txt ================================================ - Improved and fixed handling of dynamic network changes. ================================================ FILE: fastlane/metadata/android/en-US/changelogs/13.0.0.txt ================================================ - Play notification sound also when message arrives and baresip is visible. - If contact's name is empty, use contact's SIP URI AoR as contact's name. - Relaxed checking of contact's name. - Avoid occasional crash when selecting an account in the spinner. - Ask Record Audio (Microphone) permission when baresip starts (if not already granted or permanently denied). - Improved handling of Write/Read External Storage (Storage Space) permission upon Backup/Restore. - Read Phone State (Phone) permission is not needed. ================================================ FILE: fastlane/metadata/android/en-US/changelogs/13.0.1.txt ================================================ - Always notify user agent spinner about possible data set change when resuming main activity without intent. ================================================ FILE: fastlane/metadata/android/en-US/changelogs/14.0.0.txt ================================================ - Added character based avatars to contacts, chats, and call history. - Fixed saving/restoring of contacts with non-ASCII contact names. - Improved formatting of dates and times. - Added About limitation note about multiple network interfaces. ================================================ FILE: fastlane/metadata/android/en-US/changelogs/15.0.0.txt ================================================ - Added image based contact avatars as alternative to character ones. - Small user interface improvements and fixes. ================================================ FILE: fastlane/metadata/android/en-US/changelogs/15.1.0.txt ================================================ - Added possibility to give transport protocol for account's AoR. - Do not show account's port or transport protocol except in Account Activity. ================================================ FILE: fastlane/metadata/android/en-US/changelogs/15.1.1.txt ================================================ - Minor string fixes and improvements. ================================================ FILE: fastlane/metadata/android/en-US/changelogs/16.0.0.txt ================================================ - Replaced global setting "Prefer IPv6" with account specific "Prefer IPv6 Media" setting. - Improved handling of changes in network connectivity. - Do not require port in IPv6 outbound URI. - Abandoned audio focus also at quit if not already done earlier. - Added initial Russian language support. - Updated some Norwegian strings. ================================================ FILE: fastlane/metadata/android/en-US/changelogs/16.0.1.txt ================================================ - Re-register UAs when a new active network becomes available or when link properties change even if IP addresses remain the same. ================================================ FILE: fastlane/metadata/android/en-US/changelogs/16.1.0.txt ================================================ - Do not re-register accounts when link properties change, but IP addresses remain the same. - If baresip doeds not start, let the user change the Settings that might have caused the problem. - String updates and fixes. ================================================ FILE: fastlane/metadata/android/en-US/changelogs/17.0.0.txt ================================================ - Fixed saving of an account that has ;transport parameter in its AoR. - Fixed enabling of notifications when call is closed. - Use white status icon when account's registration has not been enabled. - Default call volume now effects both voice call and music streams. - Improved Accounts activity layout and implementation. - Do not show baresip lib debug messages if Debug has not been enabled. - Do not re-create activities when screen orientation changes. - Properly show chat information that does not fit one line ================================================ FILE: fastlane/metadata/android/en-US/changelogs/17.1.0.txt ================================================ - Use usage type instead of stream type when requesting audio focus. - Fixed formatting of Account English Help text. - Don't show possible port of an AoR when asked about deleting the AoR. - Filled the non-register status icon with white color. ================================================ FILE: fastlane/metadata/android/en-US/changelogs/17.2.0.txt ================================================ - Authentication username now defaults to account's username. - Ask authentication password at baresip start if authentication username is given, but password is not. - Initialize account's status dot to yellow, when Register is checked in account activity. - Fixed bug related to creation of first account on older Android versions. - Minor string improvements. ================================================ FILE: fastlane/metadata/android/en-US/changelogs/17.2.1.txt ================================================ - Fixed bug in storing of accounts to file system. ================================================ FILE: fastlane/metadata/android/en-US/changelogs/17.2.2.txt ================================================ - Do not subscribe to message waiting indication when account is created and voicemail URI is not set. ================================================ FILE: fastlane/metadata/android/en-US/changelogs/17.2.3.txt ================================================ - Prevent crashes resulting from double clicking of various icons and list items. - Change color of account's notification icon to white when account is unregistered. ================================================ FILE: fastlane/metadata/android/en-US/changelogs/17.3.0.txt ================================================ - Added "Show Password" checkbox to authentication password prompt. - Ask microphone permission as the first thing when baresip starts. - Do not allow " character in account's authentication password. ================================================ FILE: fastlane/metadata/android/en-US/changelogs/17.4.0.txt ================================================ - Added support for Android 10 (API level 29). - Improved asking of microphone permission when baresip is started the first time. - Avoid crash if call button is touched without accounts. ================================================ FILE: fastlane/metadata/android/en-US/changelogs/18.0.0.txt ================================================ - Due to Android 10 restrictions, baresip cannot anymore be automatically started after boot without a notification. - Fixed restart of baresip on Android 10. - Removed setting of ICE Lite Mode due to upstream removal. - Added Brazilian Portuguese strings from Weblate. - Small fixes in Spanish and Norwegian strings. ================================================ FILE: fastlane/metadata/android/en-US/changelogs/18.0.1.txt ================================================ - Upstream increase of maximum number of account's audio codecs from 8 to 16. ================================================ FILE: fastlane/metadata/android/en-US/changelogs/18.1.0.txt ================================================ - Improved configuration of account's audio codecs. - Always show incoming call (if any) when baresip is resumed. - Improved and fixed resuming to non-main activities. ================================================ FILE: fastlane/metadata/android/en-US/changelogs/18.1.1.txt ================================================ - Fixed crash when calling from Call History. ================================================ FILE: fastlane/metadata/android/en-US/changelogs/18.1.2.txt ================================================ - Fixed crash when returning to the previous activity after received call or message. - Better handling of proximity sensor and speaker phone. ================================================ FILE: fastlane/metadata/android/en-US/changelogs/18.1.3.txt ================================================ - Simplified control of proximity sensing. ================================================ FILE: fastlane/metadata/android/en-US/changelogs/18.2.0.txt ================================================ - Separated configuration of account's audio codecs to a new view. ================================================ FILE: fastlane/metadata/android/en-US/changelogs/18.2.1.txt ================================================ - Fixed two chat related crashes. - Minor code improvements. ================================================ FILE: fastlane/metadata/android/en-US/changelogs/18.3.0.txt ================================================ - Dialog improvements. ================================================ FILE: fastlane/metadata/android/en-US/changelogs/19.0.0.txt ================================================ - Added TURN account medianat option. - Added possibility to give STUN/TURN server username/password. - STUN/TURN related strings for some languages have not been updated yet. - More dialog improvements. ================================================ FILE: fastlane/metadata/android/en-US/changelogs/19.1.0.txt ================================================ - Alert dialog improvements. ================================================ FILE: fastlane/metadata/android/en-US/changelogs/20.0.0.txt ================================================ - Acoustic Echo Cancellation improvements. - Moved audio settings from Settings to new activity. - Added AEC Extented Filter setting. ================================================ FILE: fastlane/metadata/android/en-US/changelogs/20.0.1.txt ================================================ - Fixed returning to Audio Activity after pause. - Fixed adding of audio modules in Audio settings. ================================================ FILE: fastlane/metadata/android/en-US/changelogs/20.0.2.txt ================================================ - Dialpad button related fixes and improvements. ================================================ FILE: fastlane/metadata/android/en-US/changelogs/20.1.0.txt ================================================ - Added contributed RU strings. - Upstream fix of possible crash. ================================================ FILE: fastlane/metadata/android/en-US/changelogs/21.0.0.txt ================================================ - Added initial support for baresip originated call transfer. - Added new language Portuguese from Weblate. ================================================ FILE: fastlane/metadata/android/en-US/changelogs/21.1.0.txt ================================================ - Added possibility to re-register selected account by swipe down gesture. - Automatically focus and show soft keyboard when passwords or transfer destination is asked. - Various string related fixes. ================================================ FILE: fastlane/metadata/android/en-US/changelogs/21.2.0.txt ================================================ - Added AMR-WB (Adaptive Multi-Rate Wideband) speech codec. - Added Licenses section to About text. - New Portuguese (Brazil) translations. ================================================ FILE: fastlane/metadata/android/en-US/changelogs/22.0.0.txt ================================================ - Added Websocket transport for SIP (RFC 7118). ================================================ FILE: fastlane/metadata/android/en-US/changelogs/22.1.0.txt ================================================ - New Portuguese (Brazil) and Russian translations. - Update icons after resuming to the application. ================================================ FILE: fastlane/metadata/android/en-US/changelogs/23.0.0.txt ================================================ - Added visibility toggle to Account's Authentication and STUN/TURN Passwords - Prefer VPN interface (if any) when choosing from available network interfaces - Added note to About text about interface preference order - Improved configuration of Account's Media NAT Traversal - Default to Google's STUN server also when Media NAT Traversal protocol is ICE - Increased maximum lengths of Account's Authentication and STUN/TURN Usernames and Display Name to 64 characters - Added/updated FR, NO, and FI string translations ================================================ FILE: fastlane/metadata/android/en-US/changelogs/23.1.0.txt ================================================ - Added G.729 audio codec. - Added a Setting to turn on/off tracing of SIP messages to logcat. - STUN/TURN Server URI schemes 'stuns' and 'turns' are not currently supported. - New Portuguese (Brazil) string translations. ================================================ FILE: fastlane/metadata/android/en-US/changelogs/23.2.0.txt ================================================ - Improved detection of VPN connectivity changes - New Portuguese (Brazil) translations ================================================ FILE: fastlane/metadata/android/en-US/changelogs/24.0.0.txt ================================================ - Use adaptive jitter buffer ================================================ FILE: fastlane/metadata/android/en-US/changelogs/24.1.0.txt ================================================ - Improved handling of volume up/down keys - Improved handling of speakerphone - New RU translations ================================================ FILE: fastlane/metadata/android/en-US/changelogs/24.2.0.txt ================================================ - Automatically show DTMF soft keyboard when call is connected - Avoid crash caused by pressing volume control keys when not in call ================================================ FILE: fastlane/metadata/android/en-US/changelogs/24.3.0.txt ================================================ - Show dialpad automatically only when device orientation is portrait - Don't play call waiting sound when second call comes in - Improved About text ================================================ FILE: fastlane/metadata/android/en-US/changelogs/24.4.0.txt ================================================ - Do not default STUN/TURN server Username/Password to Authentication Username/Password ================================================ FILE: fastlane/metadata/android/en-US/changelogs/25.0.0.txt ================================================ - Improved incoming call notification. - Added missed call notification. ================================================ FILE: fastlane/metadata/android/en-US/changelogs/26.0.0.txt ================================================ - Added dark theme and dark theme setting. - Account spinner enhancements. ================================================ FILE: fastlane/metadata/android/en-US/changelogs/26.0.1.txt ================================================ - Fixed turning on dark theme when baresip application is launched - New translations (Portuguese (Brazil)) ================================================ FILE: fastlane/metadata/android/en-US/changelogs/26.1.0.txt ================================================ - Other apps can now see baresip as a phone app for sip: and tel: URIs - Improved save/restore of call URI text when main activity is paused - Introduced Japanese translation - Color enhancements ================================================ FILE: fastlane/metadata/android/en-US/changelogs/26.1.1.txt ================================================ - Fixed handling of call action when baresip app is not running. ================================================ FILE: fastlane/metadata/android/en-US/changelogs/26.1.2.txt ================================================ - Try to detect rotation of contact avatar images ================================================ FILE: fastlane/metadata/android/en-US/changelogs/27.0.0.txt ================================================ - Added possibility to export baresip contacts to Android contacts ================================================ FILE: fastlane/metadata/android/en-US/changelogs/27.0.1.txt ================================================ - Fixed crash when baresip is started the first time ================================================ FILE: fastlane/metadata/android/en-US/changelogs/28.0.0.txt ================================================ - Added possibility to partially auto-configure new account from web page (see https://github.com/juha-h/baresip-studio/wiki/Automatic-Account-Configuration for details) ================================================ FILE: fastlane/metadata/android/en-US/changelogs/28.1.0.txt ================================================ - Improved finding of a contact that matches a SIP URI - Toolbar and menu style enhancements ================================================ FILE: fastlane/metadata/android/en-US/changelogs/28.1.1.txt ================================================ - Fixed coloring of audio modules setting - Fixed call list related crash and appearance ================================================ FILE: fastlane/metadata/android/en-US/changelogs/28.2.0.txt ================================================ - Added 'Verify Server Certificates' setting ================================================ FILE: fastlane/metadata/android/en-US/changelogs/29.0.0.txt ================================================ - Enabled swipe left/right to toggle between accounts - Added DTMF Mode account setting - Translations update from Weblate (French) ================================================ FILE: fastlane/metadata/android/en-US/changelogs/29.1.0.txt ================================================ - Portuguese (Brazil) translations from Weblate - Fixed F-Droid nb-NO locale tag - Avoid 'duplicate finish request' warnings ================================================ FILE: fastlane/metadata/android/en-US/changelogs/29.2.0.txt ================================================ - Chat, call history, and contact related bug fixes - Fixed URI completion bugs and improved URI related checks - Translations from Weblate (French, Portuguese (Brazil)) ================================================ FILE: fastlane/metadata/android/en-US/changelogs/29.2.1.txt ================================================ - Avoid possible contacts related crash at baresip start ================================================ FILE: fastlane/metadata/android/en-US/changelogs/3.2.0.txt ================================================ - Added dialpad button for choosing between phone number and text soft keyboard. - Fixed auto completion of callee based on contacts. ================================================ FILE: fastlane/metadata/android/en-US/changelogs/3.2.2.txt ================================================ - Added handling of call transfer failed event. - Avoided crash at device (re)start. - Avoided crash when coming first time back from accounts to main activity. - Used "textEmailAddress" input type for account URIs. - Changed importance of default notification channel to low. ================================================ FILE: fastlane/metadata/android/en-US/changelogs/30.0.0.txt ================================================ - Upgraded target SDK version to API level 30 - In Android versions 10 and above, let user choose the file in backup, restore, TLS certificate related operations ================================================ FILE: fastlane/metadata/android/en-US/changelogs/30.0.1.txt ================================================ - Upstream fix in selecting correct account for incoming call. ================================================ FILE: fastlane/metadata/android/en-US/changelogs/30.1.0.txt ================================================ - Password dialog and account password entry improvements. ================================================ FILE: fastlane/metadata/android/en-US/changelogs/30.2.0.txt ================================================ - Added support for AMR codec Bandwidth Efficient Mode - Allow escaped characters in SIP URI user part and authentication username - Use coroutine instead of async task to fetch account config from network - In Android 10+ use picker to choose TLS Certificate and TLS CA Files - Ask confirmation to reset settings to factory defaults - New translations (Portuguese and Portuguese (Brazil)) ================================================ FILE: fastlane/metadata/android/en-US/changelogs/30.3.0.txt ================================================ - Play auto-answer sound when auto-answering - Removed iLBC codec (removed from upstream) - Removed unused audio files ================================================ FILE: fastlane/metadata/android/en-US/changelogs/31.0.0.txt ================================================ - Improved About text - Restore chat list position when returning from chat - Fixed crash when asking for account's password - Don't try to load iLBC module ================================================ FILE: fastlane/metadata/android/en-US/changelogs/31.1.0.txt ================================================ - Upstream fix of websocket transport - New Portuguese (Brazil) translations - Check Outbound Proxy URI transport ================================================ FILE: fastlane/metadata/android/en-US/changelogs/31.2.0.txt ================================================ - Account STUN/TURN URI enhancements - Various audio related improvements - New Portuguese (Brazil) and Slovenian translations ================================================ FILE: fastlane/metadata/android/en-US/changelogs/31.2.1.txt ================================================ - Fixed crash when selecting an account in accounts activity ================================================ FILE: fastlane/metadata/android/en-US/changelogs/32.0.0.txt ================================================ - Added button to turn microphone on/off during call - Used horizontal scroll view for call control buttons ================================================ FILE: fastlane/metadata/android/en-US/changelogs/32.1.0.txt ================================================ - New Spanish and Portuguese (Brazil) translations - Minor AudioManager related improvements ================================================ FILE: fastlane/metadata/android/en-US/changelogs/32.2.0.txt ================================================ - Improved checking of SIP URI parameters - More audio manager/bluetooth related fixes and improvements ================================================ FILE: fastlane/metadata/android/en-US/changelogs/32.3.0.txt ================================================ - Acquire WiFi lock while baresip is using WiFi network - Restored accidently removed ringback asset - Added About note regarding turning off battery optimizations - Swedish language translations ================================================ FILE: fastlane/metadata/android/en-US/changelogs/33.0.0.txt ================================================ - Acquire WiFi lock when baresip is using WiFi network - Added note to About text regarding battery optimization - New Swedish and Portuguese (Brazil) translations - Various implementation improvements ================================================ FILE: fastlane/metadata/android/en-US/changelogs/34.0.0.txt ================================================ - Added limited support for multiple simultaneously active network interfaces - Show notification when baresip is started without network access ================================================ FILE: fastlane/metadata/android/en-US/changelogs/34.1.0.txt ================================================ - Use WebRTC Acoustic Echo Cancellation Mobile Mode filter ================================================ FILE: fastlane/metadata/android/en-US/changelogs/35.0.0.txt ================================================ - Use WebRTC Acoustic Echo Cancellation Mobile Mode filter - Show in notification the number missed calls if more than one - When Default Call Volume setting is set, set both media and call volume - Cancel notifications when main activity is resumed - Fixed stop ringing in older Android versions - New Swedish and Portuguese (Brazil) translations ================================================ FILE: fastlane/metadata/android/en-US/changelogs/35.1.0.txt ================================================ - When aec is enabled/disabled, load/unload webrtc_aecm module - Use BaresipService to play all sounds - Proper handling of session progress response - During call, volume keys now control media volume - Use communication audio mode when not in ringing audio mode ================================================ FILE: fastlane/metadata/android/en-US/changelogs/35.2.0.txt ================================================ - Audio focus related fixes and improvements - Remove all whitespace from callee field - New Portuguese (Brazil) translation ================================================ FILE: fastlane/metadata/android/en-US/changelogs/36.0.0.txt ================================================ - Migrated baresip to API level 31 (Android 12) - Improved available network detection - Improved permission requests - Removed account's Prefer IPv6 Media setting - New Portuguese (Brazil) translation ================================================ FILE: fastlane/metadata/android/en-US/changelogs/36.1.0.txt ================================================ - Moved call mute button from call control to action bar - Added Battery Optimizations setting - Ask record audio permission when baresip is started - New Portuguese (Brazil), French, and Swedish translations ================================================ FILE: fastlane/metadata/android/en-US/changelogs/36.1.1.txt ================================================ - Moved call mute button from call control to action bar - Added Battery Optimizations setting - Ask record audio permission when baresip is started - New Portuguese (Brazil), French, and Swedish translations ================================================ FILE: fastlane/metadata/android/en-US/changelogs/36.1.2.txt ================================================ - Prevented swipe related crash ================================================ FILE: fastlane/metadata/android/en-US/changelogs/36.2.0.txt ================================================ - Currently mic icon is active only when a call is connected - New Swedish translations ================================================ FILE: fastlane/metadata/android/en-US/changelogs/37.0.0.txt ================================================ - RFC 5589 Basic Transfer compliant call transfer implementation ================================================ FILE: fastlane/metadata/android/en-US/changelogs/37.0.1.txt ================================================ Fixed outgoing call audio settings ================================================ FILE: fastlane/metadata/android/en-US/changelogs/37.1.0.txt ================================================ - Added average bit rate, jitter, packet count, and packet lost count data to call info - Use accent color pause icon to indicate that user has put the call on hold - Inform user when the other party has put the call on hold - Minor soft input related improvements - New Portuguese (Brazil) translations ================================================ FILE: fastlane/metadata/android/en-US/changelogs/37.2.0.txt ================================================ - Better protection of baresip app when device is locked with secure keyguard - New Portuguese (Brazil) translations ================================================ FILE: fastlane/metadata/android/en-US/changelogs/37.3.0.txt ================================================ - Moved Default Call Volume setting under Audio settings - New language (German) ================================================ FILE: fastlane/metadata/android/en-US/changelogs/37.4.0.txt ================================================ - New Portuguese and German translations ================================================ FILE: fastlane/metadata/android/en-US/changelogs/38.0.0.txt ================================================ - Improved device lock/unlock handling - In Settings, added link to Android settings - Properly check if Battery Optimizations setting has been changed - New Swedish and Portuguese (Brazil) translations ================================================ FILE: fastlane/metadata/android/en-US/changelogs/39.0.0.txt ================================================ - Improved display of call history time value in call history list - Added call details activity that can be accessed by touching time in call history list - Fixed showing of avatars in contact, call, and chat lists - Allow cancelling of answered call that waits to be established - New Portuguese, Portuguese Brazil, Swedish, and Norwegian translations ================================================ FILE: fastlane/metadata/android/en-US/changelogs/39.1.0.txt ================================================ - Improved and fixed handling of message and call transfer events - Toast registration failure it to any current activity - Automatically capitalize message sentences - Replaced deprecated local broadcasts with LiveData events - Fixed some bugs related to handling of external call actions - New Portuguese (Brazil) translations ================================================ FILE: fastlane/metadata/android/en-US/changelogs/4.0.0.txt ================================================ - Improved implementation of contacts, messages, and call history. - Avoided crash when messages arrive before accounts have been added. ================================================ FILE: fastlane/metadata/android/en-US/changelogs/4.1.0.txt ================================================ - All user interface unrelated data is now kept in baresip service. allowing proper recovery in case Android kills other baresip activities. - Many other improvements and bugfixes. ================================================ FILE: fastlane/metadata/android/en-US/changelogs/4.1.1.txt ================================================ - Fixed critical bug in checking of record audio permission that prevented new baresip installation from starting. - Small improvements. ================================================ FILE: fastlane/metadata/android/en-US/changelogs/4.1.2.txt ================================================ - Fixed crash when empty accounts spinner is clicked. ================================================ FILE: fastlane/metadata/android/en-US/changelogs/40.0.0.txt ================================================ - Added Telephony Provider account configuration item - Added support for tel URI as Callee value and Contact URI - Prompt user for Telephony Provider account if needed to make the call - Show account's STUN configuration items only if needed - Improved handling of CALL action from other applications ================================================ FILE: fastlane/metadata/android/en-US/changelogs/40.0.1.txt ================================================ - Added Telephony Provider account configuration item - Added support for tel URI as Callee value and Contact URI - Prompt user for Telephony Provider account if needed to make the call - Show account's STUN configuration items only if needed - Improved handling of CALL action from other applications ================================================ FILE: fastlane/metadata/android/en-US/changelogs/40.1.0.txt ================================================ - Do not play busy or error sound when call is closed - Improved reliability of Bluetooth usage - Better handling of events from baresip core - New Portuguese (Brazil) translations ================================================ FILE: fastlane/metadata/android/en-US/changelogs/41.0.0.txt ================================================ - Added call timer chronometer to "Call to .../Call from ..." row - Added possibility to use Android contacts - Added "Show Android Contacts" setting to choose if Android contacts are shown when contacts button is touched - Check that restore file has been created by baresip app - New Swedish and Portuguese (Brazil) translations ================================================ FILE: fastlane/metadata/android/en-US/changelogs/42.0.0.txt ================================================ - Replaced 'Show Android Contacts' setting with 'Contacts' setting that allows to choose if baresip contacts, Android contacts, or both are used - Contact name <-> contact URI mapping fixes and improvements - Avoid crash when Android forces baresip app restart ================================================ FILE: fastlane/metadata/android/en-US/changelogs/42.1.0.txt ================================================ - Replaced AppCompat theme with MaterialComponents theme ================================================ FILE: fastlane/metadata/android/en-US/changelogs/42.2.0.txt ================================================ - Contact selection list now contains only those Android contacts that have exactly one URI - New Swedish and Portuguese (Brazil) translations ================================================ FILE: fastlane/metadata/android/en-US/changelogs/42.3.0.txt ================================================ - Fixed adding of new baresip contact - When selecting a new default account, keep the order of the rest unchanged - Improved handling of call auto-rejection ================================================ FILE: fastlane/metadata/android/en-US/changelogs/43.0.0.txt ================================================ - Added initial support for attended call transfer - In chat activity, replaced long click with short click - Made message text selectable - Use bold typeface in AoR spinner text when call is active ================================================ FILE: fastlane/metadata/android/en-US/changelogs/43.0.2.txt ================================================ - Added initial support for attended call transfer - In chat activity, replaced long click with short click - Made message text selectable - Use bold typeface in AoR spinner text when call is active ================================================ FILE: fastlane/metadata/android/en-US/changelogs/43.1.0.txt ================================================ - Fixed 32 bit armeabi-v7a architecture related bug - Display Anonymous or Unknown if URI host is anonymous.invalid or unknown.invalid ================================================ FILE: fastlane/metadata/android/en-US/changelogs/44.0.0.txt ================================================ - Show diverter if incoming call has been diverted - Backup related fixes in older Android versions - Improved URI to Contact matching - New Portuguese (Brazil) translations ================================================ FILE: fastlane/metadata/android/en-US/changelogs/44.1.0.txt ================================================ - Due to Google's requirement, ask user's consent if Android contacts is chosen in Settings - New Portuguese (Brazil) translations ================================================ FILE: fastlane/metadata/android/en-US/changelogs/44.2.0.txt ================================================ - Fixed crash on ARMv7 devices when message was received - New Portuguese (Brazil) translations ================================================ FILE: fastlane/metadata/android/en-US/changelogs/446.txt ================================================ - Fixed Do Not Disturb check - New translations ================================================ FILE: fastlane/metadata/android/en-US/changelogs/447.txt ================================================ - Change color of call button to yellow when call button has been touched, but call has not been made yet - Do not activate useless software based echo canceling if hardware based is not available - Always clear security icon and DTMF field when call is closed - For registered accounts, check that user part of incoming Request URI matches user part of REGISTER request contact URI - Upstream audio jitter buffer improvements - New translations (French) ================================================ FILE: fastlane/metadata/android/en-US/changelogs/448.txt ================================================ - Added Weblate note to About text - New translations (Chinese) - Baresip lib logging macro fix ================================================ FILE: fastlane/metadata/android/en-US/changelogs/449.txt ================================================ - Fixed adding of new call to history - Avoid screen "flash" when calling from contacts or call history - New translations (Czech) ================================================ FILE: fastlane/metadata/android/en-US/changelogs/45.0.0.txt ================================================ - Added Country Code account configuration option - Audio related improvements from upstream - New Portuguese (Brazil) and Finnish translations ================================================ FILE: fastlane/metadata/android/en-US/changelogs/45.1.0.txt ================================================ - Use elliptic curve cryptography based DTLS Key Establishment Protocol - New Swedish translations ================================================ FILE: fastlane/metadata/android/en-US/changelogs/45.1.1.txt ================================================ - Use elliptic curve cryptography based DTLS Key Establishment Protocol - Fixed backup of call history - New Swedish translations ================================================ FILE: fastlane/metadata/android/en-US/changelogs/45.1.2.txt ================================================ Ring and notification tone related fixes and improvements ================================================ FILE: fastlane/metadata/android/en-US/changelogs/450.txt ================================================ - Exclude starred Android contacts from Do Not Disturb - Don't loose content of new message that has not been sent yet - New translations (Swedish) ================================================ FILE: fastlane/metadata/android/en-US/changelogs/451.txt ================================================ - Added initial support for dynamic colors on many screens - Improved showing and hiding of soft keyboard - New translations (Chinese (Simplified Han script)) ================================================ FILE: fastlane/metadata/android/en-US/changelogs/452.txt ================================================ - Retain account and settings modifications until check or back button is clicked - Use TLS CA File certificates also when pre-configuring account from network - Do not leave Settings in case of input error ================================================ FILE: fastlane/metadata/android/en-US/changelogs/453.txt ================================================ - Use Material3 color theme everywhere ================================================ FILE: fastlane/metadata/android/en-US/changelogs/454.txt ================================================ - Improved visibility of Main screen Call from field and Account screen SIP URI field ================================================ FILE: fastlane/metadata/android/en-US/changelogs/455.txt ================================================ - Improved surface container colors ================================================ FILE: fastlane/metadata/android/en-US/changelogs/456.txt ================================================ - Removed G.726 codec due to too many dependencies - New G.722 codec with less dependencies - New translations (Swedish, Czech, and Chinese) ================================================ FILE: fastlane/metadata/android/en-US/changelogs/457.txt ================================================ - Fixed possible crash related to attended transfer - Show original call as being on hold when waiting for blind transfer - Use Material3 colors also in Hold, Transfer, and Info buttons ================================================ FILE: fastlane/metadata/android/en-US/changelogs/458.txt ================================================ - Replaced most drawables with Material Design icons - Avoided possible memory leak in chronometer - New translations (Chinese) ================================================ FILE: fastlane/metadata/android/en-US/changelogs/459.txt ================================================ - Replaced baresip launcher PNG image with vector drawable - Replaced call, hangup, lock, camera front/back, and screenshot icons with Material Design icons - Used outlined text field for DTMF digits - Notification fixes and improvements ================================================ FILE: fastlane/metadata/android/en-US/changelogs/46.0.0.txt ================================================ - Added possibility to turn on/off account's registration via broadcast intents (see Wiki for details) - Upstream addition of DNS query caching - Upstream fix of ICE candidate priorities - Increased Android target API level to 32 (Android 12L) ================================================ FILE: fastlane/metadata/android/en-US/changelogs/46.0.1.txt ================================================ - Do not use DNS cache that may cause long delays to address resolution ================================================ FILE: fastlane/metadata/android/en-US/changelogs/46.1.0.txt ================================================ - Long touch on current account enables or disables account's registration - Added possibility to nickname accounts ================================================ FILE: fastlane/metadata/android/en-US/changelogs/46.1.1.txt ================================================ - Long touch on current account enables or disables account's registration - Added possibility to nickname accounts ================================================ FILE: fastlane/metadata/android/en-US/changelogs/46.2.0.txt ================================================ - If unregistering fails, update registration status to white anyway - Set asked password when user agent is created - New Portuguese (Brazil) translations ================================================ FILE: fastlane/metadata/android/en-US/changelogs/460.txt ================================================ - After restore, cleared recorded call indications from calls and call details screens - On call details screen, added possibility to save call recording by long click on call duration and to delete call history by long click on call time value - Added progress bar and stop button to play recording dialog ================================================ FILE: fastlane/metadata/android/en-US/changelogs/461.txt ================================================ - Use compose based call timer - Fixed possible race condition when screen is rotated - New translations (Chinese) ================================================ FILE: fastlane/metadata/android/en-US/changelogs/462.txt ================================================ - Added proper response code and reason to user agent hangup calls - Removed possible old recordings when baresip is restored ================================================ FILE: fastlane/metadata/android/en-US/changelogs/463.txt ================================================ - Optimized amr, g7221, and opus codecs ================================================ FILE: fastlane/metadata/android/en-US/changelogs/464.txt ================================================ - Added Block Unknown account setting that allows blocking of calls and messages from peers not found in contacts - Added Check Origin account setting that allows blocking of SIP requests whose source IP address does not match IP address where register requests are sent - Added supported Transport Protocols setting - New translations (Chinese and Russian) ================================================ FILE: fastlane/metadata/android/en-US/changelogs/466.txt ================================================ - Fixed bug in restoring Block Unknown account setting - New translations (Chinese) ================================================ FILE: fastlane/metadata/android/en-US/changelogs/467.txt ================================================ - Added screens to show blocked calls and messages - Avoid showing of empty screen when back or check icons are touched repeatedly ================================================ FILE: fastlane/metadata/android/en-US/changelogs/468.txt ================================================ - Added Search to Contacts Screen - New translations (Czech) ================================================ FILE: fastlane/metadata/android/en-US/changelogs/469.txt ================================================ - Fixed baresip contact long click alert - New translations (Chinese) ================================================ FILE: fastlane/metadata/android/en-US/changelogs/47.0.0.txt ================================================ - Added support for reliable provisional response messages (RFC 3262) - Added QUIT action to intent receiver ================================================ FILE: fastlane/metadata/android/en-US/changelogs/47.1.0.txt ================================================ - Added support for SIP UPDATE method (RFC 3311) - Upstream fix of early audio (183 Session Progress) related bug ================================================ FILE: fastlane/metadata/android/en-US/changelogs/47.2.0.txt ================================================ - Delay calling by 1 sec after call icon is touched because audio mode does not immediately change to communication - Upstream fix of PRACK handling - New Swedish translations ================================================ FILE: fastlane/metadata/android/en-US/changelogs/470.txt ================================================ - Added support for conference calls ================================================ FILE: fastlane/metadata/android/en-US/changelogs/471.txt ================================================ - Added support for conference calls ================================================ FILE: fastlane/metadata/android/en-US/changelogs/472.txt ================================================ - In contact search, match also diacritic characters - In contact search, highlight matched characters ================================================ FILE: fastlane/metadata/android/en-US/changelogs/473.txt ================================================ - Improved showing of contact suggestions on Main and Chats screens - New translations (Czech) ================================================ FILE: fastlane/metadata/android/en-US/changelogs/474.txt ================================================ - Added BaresipApp to avoid baresip service starting before baresip library is loaded - Improved showing of call transfer destination suggestions ================================================ FILE: fastlane/metadata/android/en-US/changelogs/475.txt ================================================ - Fixed launch icon background - Improved showing of call icons - New translations (Hebrew) ================================================ FILE: fastlane/metadata/android/en-US/changelogs/476.txt ================================================ - Fixed crash related to deleting of messages - Start automatically also when new version of baresip is installed - Improved Audio Settings check - Improved alert dialog background color - Improved suggestion coloring - New translations (Chinese and Hebrew) ================================================ FILE: fastlane/metadata/android/en-US/changelogs/477.txt ================================================ - Migrated alert dialogs to Material design style - Improved dropdown menu coloring - Added timeout for getting account template from network - Fixed call redirect URI - New translations (Hebrew) ================================================ FILE: fastlane/metadata/android/en-US/changelogs/478.txt ================================================ - Prevent multiple call button actions when the buttons are touched repeatedly - Avoid crash if getting of call's codec info fails - Avoid possible crash related to settings viewmodel ================================================ FILE: fastlane/metadata/android/en-US/changelogs/479.txt ================================================ - Unlock locked screen and turn it on when new call is received - New translations (Portuguese (Brazil) ================================================ FILE: fastlane/metadata/android/en-US/changelogs/48.0.0.txt ================================================ - Use lock icons for call security indication - When calling Android contact, use the latest peer URI if Android contact has it - Use ZRTPCPP lib for ZRTP media encryption (peers need to be re-verified) - Enable DNS caching - New Portuguese (Brazil) translations ================================================ FILE: fastlane/metadata/android/en-US/changelogs/48.0.1.txt ================================================ - Use lock icons for call security indication - When calling Android contact, use the latest peer URI if Android contact has it - Use ZRTPCPP lib for ZRTP media encryption (peers need to be re-verified) - Enable DNS caching - New Portuguese (Brazil) translations ================================================ FILE: fastlane/metadata/android/en-US/changelogs/48.1.0.txt ================================================ - Added GSM codec - Made window un-touchable and started 5 second timer at Quit or Restart ================================================ FILE: fastlane/metadata/android/en-US/changelogs/48.2.0.txt ================================================ - Disable PRACK (RFC 3262) feature due to severe upstream bug - New Portuguese (Brazil) and Russian translations ================================================ FILE: fastlane/metadata/android/en-US/changelogs/480.txt ================================================ - Cancel full screen notification when call is closed - Issue missed call notification also when baresip app is not visible - Cancel full screen notification when call is closed ================================================ FILE: fastlane/metadata/android/en-US/changelogs/481.txt ================================================ - Avoid crash related to screen rotation - Upstream fix related to SIP 183 Session Progress response - Translations update (English, Finnish) ================================================ FILE: fastlane/metadata/android/en-US/changelogs/482.txt ================================================ - Added Unique Contact URI setting - New translations (Chinese) ================================================ FILE: fastlane/metadata/android/en-US/changelogs/483.txt ================================================ - Fixed bug in codecs screen - New translations (French and Hebrew) ================================================ FILE: fastlane/metadata/android/en-US/changelogs/484.txt ================================================ - Baresip foreground service notification is now always visible also when Android version is 14 or newer - New translations (Chinese and Czech) ================================================ FILE: fastlane/metadata/android/en-US/changelogs/487.txt ================================================ - Baresip app is now integrated with Android Telecom Framework - Added support for multiple simultaneous calls (only one can be active) - Acquire partial wake lock only if there is active registrations or calls - Acquire WiFi lock also when WiFi is available but not actively used - Always resume to active call on main screen if there is one - In attended call transfer, check if peer supports REPLACES header - Print network debug only if there was a change ================================================ FILE: fastlane/metadata/android/en-US/changelogs/488.txt ================================================ - Baresip app is now integrated with Android Telecom Framework - Added support for multiple simultaneous calls (only one can be active) - Allow hanging up individual members of conference call - Improved status notification content texts - Acquire partial wake lock only if there is active registrations or calls - Acquire WiFi lock also when WiFi is available but not actively used - Always resume to active call on main screen if there is one - In attended call transfer, check if peer supports REPLACES header - Print network debug only if there was a change ================================================ FILE: fastlane/metadata/android/en-US/changelogs/489.txt ================================================ - Prevent other conference calls from closing when one participant hangs up - Fixed initialization of account Check Origin setting - Audio improvements - New translations (Chinese and Spanish) ================================================ FILE: fastlane/metadata/android/en-US/changelogs/49.0.0.txt ================================================ - Improved selection and ordering of account's audio codecs - Due to problems, disabled DNS query cache - Migration to Android API 33 ================================================ FILE: fastlane/metadata/android/en-US/changelogs/49.0.1.txt ================================================ - Fixed upstream bug that caused long Quit delay if Voicemail checking was activated for an account - Simplified Quit/Restart process and avoided crash at exit - New Portuguese (Brazil) translations ================================================ FILE: fastlane/metadata/android/en-US/changelogs/49.1.0.txt ================================================ - Toast reason when incoming call gets closed by baresip lib ================================================ FILE: fastlane/metadata/android/en-US/changelogs/49.1.1 ================================================ - Fixed audio crackling when G722 codec is used ================================================ FILE: fastlane/metadata/android/en-US/changelogs/49.1.2.txt ================================================ - Upstream fix of Media NAT Traversal (STUN/TURN/ICE) bug ================================================ FILE: fastlane/metadata/android/en-US/changelogs/49.1.3.txt ================================================ - Upstream fix of codec negotiation ================================================ FILE: fastlane/metadata/android/en-US/changelogs/49.2.0.txt ================================================ - Added "RTCP Multiplexing" account setting - Distributes action buttons evenly at the bottom of main activity - Used colorSecondaryDark as text view hint color ================================================ FILE: fastlane/metadata/android/en-US/changelogs/49.3.1.txt ================================================ - Added "RTCP Multiplexing" account setting - Distributes action buttons evenly at the bottom of main activity - Used colorSecondaryDark as text view hint color - Show correct status notification when there is no registered UAs ================================================ FILE: fastlane/metadata/android/en-US/changelogs/49.4.0.txt ================================================ - Fixed crash at network changes - New Portuguese (Brazil)) translations ================================================ FILE: fastlane/metadata/android/en-US/changelogs/491.txt ================================================ - Added Custom Parameters account setting - Retain value of account's Check Origin when Register is toggled - New translations (Chinese) ================================================ FILE: fastlane/metadata/android/en-US/changelogs/492.txt ================================================ - Ensure that ringback and busy tone are played in communication mode - Improved network and DNS server update management - New translations (Spanish) ================================================ FILE: fastlane/metadata/android/en-US/changelogs/5.0.0.txt ================================================ - Replaced text mode EditConfigActivity with graphical config activity. - Current configuration items are auto-start, DNS servers, Opus bitrate, and ICE-Lite. - Added ICE and STUN server account options. - Fixed bug in account deletion. - Unsent messages not lost when chat activity is interrupted. ================================================ FILE: fastlane/metadata/android/en-US/changelogs/5.1.0.txt ================================================ - Added possibility to export/import contacts to/from the "Download" folder. If you want to use this feature, grant baresip storage permission in Settings → Apps → baresip → Permissions. - Fixed slow quitting of baresip when there is no network connectivity. ================================================ FILE: fastlane/metadata/android/en-US/changelogs/5.2.0.txt ================================================ - Notify user about registration failure. - Added call info button for an active call. - Do not require "sip:" scheme in Outbound Proxy URI. - Allow domain label to start with a digit. ================================================ FILE: fastlane/metadata/android/en-US/changelogs/5.2.1.txt ================================================ - Relaxed checking of Display Name and Authentication Username - Added version name to About page ================================================ FILE: fastlane/metadata/android/en-US/changelogs/5.3.0.txt ================================================ - Added notification popup telling why call was closed. - Fixed showing of callee URI if it contains port number or parameters. ================================================ FILE: fastlane/metadata/android/en-US/changelogs/5.3.1.txt ================================================ - Relaxed checking of userpart in account's SIP URI. - Added support for ARMv8-A architecture. ================================================ FILE: fastlane/metadata/android/en-US/changelogs/5.4.0.txt ================================================ - Added SRTP-MANDF media encryption protocol. - Security button color now changes from red to yellow also when call is secured by an SRTP* media encyption protocol. - Fixed setting of account media encryption to DTLS-SRTPF. ================================================ FILE: fastlane/metadata/android/en-US/changelogs/5.4.1.txt ================================================ - Removed unused baresip modules from config and libraries. - The baresip service now lets baresip activity die on its own after receiving kill intent from it. ================================================ FILE: fastlane/metadata/android/en-US/changelogs/5.4.2.txt ================================================ - Fixed rare crash that can happen when first account is created and message or call comes in before user has returned to main activity. ================================================ FILE: fastlane/metadata/android/en-US/changelogs/50.0.0.txt ================================================ - Ask POST_NOTIFICATIONS permission on Android 13+ devices - Use gray Microphone icon when microphone is not turned off - Use "Busy Here" response message when call is rejected ================================================ FILE: fastlane/metadata/android/en-US/changelogs/50.1.0.txt ================================================ - Quit/Restart process improvements - Try to support toggling of SpeakerPhone at API levels 31+ - New Portuguese (Brazil) translations ================================================ FILE: fastlane/metadata/android/en-US/changelogs/50.1.1.txt ================================================ - Quit/Restart process improvements - Speakerphone and call related fixes and enhancements for API 31+ - New Portuguese (Brazil) translations ================================================ FILE: fastlane/metadata/android/en-US/changelogs/50.1.3.txt ================================================ - Quit/Restart process improvements - Speakerphone and call related fixes and enhancements for API 31+ - New Portuguese (Brazil) translations ================================================ FILE: fastlane/metadata/android/en-US/changelogs/50.1.4.txt ================================================ - More communication device related fixes and enhancements ================================================ FILE: fastlane/metadata/android/en-US/changelogs/50.1.5.txt ================================================ - Fixed WiFi hotspot interface detection on some devices ================================================ FILE: fastlane/metadata/android/en-US/changelogs/50.2.0.txt ================================================ - Audio routing related fixed for Android 12+ devices - Use secondary dark color in codec name when in dark mode ================================================ FILE: fastlane/metadata/android/en-US/changelogs/50.2.1.txt ================================================ - Audio routing related fixed for Android 12+ devices - Use secondary dark color in codec name when in dark mode ================================================ FILE: fastlane/metadata/android/en-US/changelogs/50.2.2.txt ================================================ - Audio routing related fixed for Android 12+ devices - Use secondary dark color in codec name when in dark mode ================================================ FILE: fastlane/metadata/android/en-US/changelogs/51.0.0.txt ================================================ - Vibrate and vibrate while ringing according to Sound and vibration settings - Bluetooth fixes and improvements for Android 12+ devices - Show permissions rationale when baresip is started the first time - New Swedish and Portuguese (Brazil) translations ================================================ FILE: fastlane/metadata/android/en-US/changelogs/51.1.0.txt ================================================ - Added registration interval account setting - Use Telephony Provider also when sending messages to tel URIs - Now also ( and ) characters are accepted in telephone number - Fixed posting notifications in later Android versions - Improved permission handling in later Android versions - New Spanish, Portuguese (Brazil)), and Swedish translations ================================================ FILE: fastlane/metadata/android/en-US/changelogs/51.2.0.txt ================================================ - Several contact related fixes and improvements - New Portuguese (Brazil) translations ================================================ FILE: fastlane/metadata/android/en-US/changelogs/52.0.0.txt ================================================ - Added support for selecting from multiple tel:/sip: URIs in Android - contacts - Active account now has to have Telephony Provider configured in order to make call or send message to tel: URI - New Portuguese (Brazil), Spanish, Swedish, and Finnish translations ================================================ FILE: fastlane/metadata/android/en-US/changelogs/52.1.0.txt ================================================ - Added preliminary support for call recording - Many fixes related to asking of passwords - Prevent outside dismiss of some dialogs - New Spanish, Portuguese, and Brazil translations ================================================ FILE: fastlane/metadata/android/en-US/changelogs/52.2.0.txt ================================================ - Added possibility to stop recording playback - Added adaptive launcher icon - New Spanish translations ================================================ FILE: fastlane/metadata/android/en-US/changelogs/53.0.0.txt ================================================ - Added setting to choose IP address family - Added monochrome launcher icon - Removed icon and text from status notification when Android version is 13+ - New Spanish, Russian, Portuguese (Brazil), and Czech translations ================================================ FILE: fastlane/metadata/android/en-US/changelogs/53.1.0.txt ================================================ - Launcher icon improvements - New Russian translations ================================================ FILE: fastlane/metadata/android/en-US/changelogs/53.1.1.txt ================================================ - Launcher icon improvements - Do not try to register new account if Register has not been checked - Added Registration Interval help text - New Russian translations ================================================ FILE: fastlane/metadata/android/en-US/changelogs/53.1.2.txt ================================================ - Upstream increase of max size of account string ================================================ FILE: fastlane/metadata/android/en-US/changelogs/53.2.0.txt ================================================ - Registration status notification improvements - Replaced several icon images with material design vector drawables ================================================ FILE: fastlane/metadata/android/en-US/changelogs/53.2.1.txt ================================================ - Fixed several audio focus/routing/volume control issues ================================================ FILE: fastlane/metadata/android/en-US/changelogs/53.2.2.txt ================================================ - Fixed several audio focus/routing/volume control issues ================================================ FILE: fastlane/metadata/android/en-US/changelogs/53.2.3.txt ================================================ - Simplified playing of ringtone - New Portuguese translations ================================================ FILE: fastlane/metadata/android/en-US/changelogs/54.0.0.txt ================================================ - Requires at least Android version 6 (API level 23) - In Android versions below 12, add 1.5 sec delay before making a call in order to avoid missing audio from callee ================================================ FILE: fastlane/metadata/android/en-US/changelogs/54.1.0.txt ================================================ - Added 'Audio Delay' Audio Setting - Improved Bluetooth headset connected test ================================================ FILE: fastlane/metadata/android/en-US/changelogs/54.2.0.txt ================================================ - Play busy tone when "486 Busy Here" or "603 Decline" response is received - Avoid unnecessary re-registration when network capabilities change - Simplified abandoning of audio focus ================================================ FILE: fastlane/metadata/android/en-US/changelogs/55.0.0.txt ================================================ - Prevent starting of baresip service more than once - Color improvements - New Spanish, Russian, Portuguese (Brazil), Swedish, and Czech translations ================================================ FILE: fastlane/metadata/android/en-US/changelogs/55.0.1.txt ================================================ - Upstream fix of call remaining in active list even when closed - Better handling of WiFi hotspot enable/disable events ================================================ FILE: fastlane/metadata/android/en-US/changelogs/55.0.2.txt ================================================ - Upstream fix of call remaining in active list even when closed - Better handling of WiFi hotspot enable/disable events - Improved alert dialog text button dark theme color contrast ================================================ FILE: fastlane/metadata/android/en-US/changelogs/55.1.0.txt ================================================ - Dark theme color improvements - Improved call auto-rejection ================================================ FILE: fastlane/metadata/android/en-US/changelogs/55.2.0.txt ================================================ - In addition to CALL, handle also DIAL actions from other applications - New Spanish and Portuguese (Brazil) translations ================================================ FILE: fastlane/metadata/android/en-US/changelogs/56.0.0.txt ================================================ - Allow selecting baresip as the default Phone app (do not select if also using telephony services) - Support both CALL and DIAL actions from other apps - Fixed bug in setting account's Telephony Provider - In messages, support also other character sets than UTF-8 - Use default values for audio_buffer and jitter_buffer_delay - New Czech and Russian translations ================================================ FILE: fastlane/metadata/android/en-US/changelogs/56.1.0.txt ================================================ - In call history, Show missed (not rejected) calls using yellow up/down arrows - Use audio_buffer size 20-300 (ms) and jitter_buffer delay 0-20 (frames) - Avoid potential getNetworkInterfaces() related crash - Simplified About text and added "more information" link to Wiki - New Spanish, Portuguese (Brazil), and Czech translations ================================================ FILE: fastlane/metadata/android/en-US/changelogs/56.2.0.txt ================================================ - Added separate jitter buffer delays for audio and video - In call info, show packets, lost, and jitter as ?/? if info is not available - Upstream fix that now allows escaped characters in account's part - New Czech translations ================================================ FILE: fastlane/metadata/android/en-US/changelogs/56.3.0.txt ================================================ - Fixed jitter buffer settings that prevented hearing of incoming audio - Terminate call if no media has been received within 60 seconds - New Spanish translations ================================================ FILE: fastlane/metadata/android/en-US/changelogs/57.0.0.txt ================================================ - Major rewrite of settings implementation by separating static and dynamic settings - Revert back to original jitter buffer delays now when libre bug is fixed - Configuration cannot be reloaded on the fly when opus settings are changed - New Spanish and Portuguese (Brazil) translations ================================================ FILE: fastlane/metadata/android/en-US/changelogs/57.1.0.txt ================================================ - Added more vertical padding to account configuration - Hack to fix wrongly escaped URI when call or dial request comes from outside - New Czech translations ================================================ FILE: fastlane/metadata/android/en-US/changelogs/57.1.1.txt ================================================ - Fixed typo in Default Phone App help text ================================================ FILE: fastlane/metadata/android/en-US/changelogs/57.2.0.txt ================================================ - Added Redirect Mode account setting to choose if call redirect requests (3xx responses) are be followed automatically or if confirmation needs to be asked - New Spanish and Portuguese (Brazil) translations ================================================ FILE: fastlane/metadata/android/en-US/changelogs/58.0.0.txt ================================================ - Added Reliable Provisional Responses Account Configuration option - Added Tone Country Audio Configuration option (credits to Robert Averbeck for providing the tone files) - Fixed AudioManager related SDK version test - New Spanish, Portuguese and Portuguese (Brazil) translations - Upstream bug fixes and improvements ================================================ FILE: fastlane/metadata/android/en-US/changelogs/59.0.0.txt ================================================ - Now targeting API level 34 (Android 14) - Added support for Codec2 audio codec - Recording of next call is not cleared when call is closed - Included also Dark Theme setting in backup file ================================================ FILE: fastlane/metadata/android/en-US/changelogs/59.0.1.txt ================================================ - Now targeting API level 34 (Android 14) - Added support for Codec2 audio codec - Recording of next call is not cleared when call is closed - Included also Dark Theme setting in backup file ================================================ FILE: fastlane/metadata/android/en-US/changelogs/59.0.2.txt ================================================ - Now targeting API level 34 (Android 14) - Added support for Codec2 audio codec - Recording of next call is not cleared when call is closed - Included also Dark Theme setting in backup file ================================================ FILE: fastlane/metadata/android/en-US/changelogs/59.0.3.txt ================================================ - Now targeting API level 34 (Android 14) - Added support for Codec2 audio codec - Recording of next call is not cleared when call is closed - Included also Dark Theme setting in backup file - Upstream fix of re-INVITE (call hold/resume) ================================================ FILE: fastlane/metadata/android/en-US/changelogs/59.0.4.txt ================================================ - Upstream fix of crash related to handling of OPTIONS request ================================================ FILE: fastlane/metadata/android/en-US/changelogs/59.1.0.txt ================================================ - Added support for favorite (starred) contacts ================================================ FILE: fastlane/metadata/android/en-US/changelogs/59.2.0.txt ================================================ - Start Automatically setting now starts the app rather than notifying about it - New Spanish, Portuguese (Brazil), and Russian translations ================================================ FILE: fastlane/metadata/android/en-US/changelogs/59.3.0.txt ================================================ - Minor upstream bug fixes - New German translations ================================================ FILE: fastlane/metadata/android/en-US/changelogs/59.4.0.txt ================================================ - Use thread RTP receive mode - New Czech and Portuguese translations ================================================ FILE: fastlane/metadata/android/en-US/changelogs/59.4.1.txt ================================================ - Updated libbaresip-android submodule (all modules are now found) ================================================ FILE: fastlane/metadata/android/en-US/changelogs/59.4.2.txt ================================================ - Upstream, by default, denied incoming messages. Can be allowed again by making any change in configuration of an account. ================================================ FILE: fastlane/metadata/android/en-US/changelogs/59.4.3.txt ================================================ - Allow receiving of messages after upstream (by default) denied them ================================================ FILE: fastlane/metadata/android/en-US/changelogs/59.5.0 ================================================ - Automatically use CA certificates provided by Android OS - New Bulgarian, Japanese, Spanish, and Portuguese (Brazil) translations ================================================ FILE: fastlane/metadata/android/en-US/changelogs/59.6.0.txt ================================================ - Added baresip, CPU, and Android information in User-Agent header - Time of recorded call is now shown in accent color ================================================ FILE: fastlane/metadata/android/en-US/changelogs/59.7.0.txt ================================================ - Upstream fix of bug that may cause baresip app to stop responding - New Czech translations ================================================ FILE: fastlane/metadata/android/en-US/changelogs/6.0.0.txt ================================================ - Several user Interface improvements (mostly chat related). ================================================ FILE: fastlane/metadata/android/en-US/changelogs/6.0.1.txt ================================================ - Resume baresip app to the activity that was stopped. - Add example contact when baresip is started the first time. - Fixed showing of new message in the chats and messages list. ================================================ FILE: fastlane/metadata/android/en-US/changelogs/6.0.2.txt ================================================ - Fixed crash when STUN server is defined without port number. ================================================ FILE: fastlane/metadata/android/en-US/changelogs/6.1.0.txt ================================================ - Go to chat window when message notification is clicked. - Fixed bug in call transfer. ================================================ FILE: fastlane/metadata/android/en-US/changelogs/6.2.0.txt ================================================ - Upgraded target Android API level to 28 (Android 9). - Moved some non-UI functionality from main activity to baresip service. - Simplified implementation of call transfer. - Updated baresip launcher and status bar images. - Un-REGISTERed UA when account is deleted. ================================================ FILE: fastlane/metadata/android/en-US/changelogs/6.2.1.txt ================================================ - Used ConnectivityManager NetworkCallbacks to detect when network connectivity becomes available or is lost. - Added Android 8.1+ way of screen handling. - Set volume controls to apply to current audio stream (may not always work). - Fixed possible crash when destroyed baresip app is started again. - Shown notice when account to be created already exists. - Added note to "About" text about media volume. ================================================ FILE: fastlane/metadata/android/en-US/changelogs/6.3.0.txt ================================================ - Added Debug Config option to turn adb logcat debug on and off. - Added possibility to export and import accounts to/from file in Download directory. ================================================ FILE: fastlane/metadata/android/en-US/changelogs/6.3.1.txt ================================================ - Fixed and improved DTMF watcher implementation. - Turned off editing of incoming/outgoing call URI when call is in progress. - Destroyed user agent when account is deleted. - Removed unused KILL_BACKGROUND_PROCESSES permission. ================================================ FILE: fastlane/metadata/android/en-US/changelogs/6.4.0.txt ================================================ - Added possibility to set an account as the default account. - Show red messages icon when unread messages exist. - Ask PIN to release key guard when Callee URI is clicked. - Fixed call, message, and call transfer actions when initiated from notifications. - Fixed sending a message from contacts or call history. - Used SingleTask instead of SingleTop as Main Activity launch mode. ================================================ FILE: fastlane/metadata/android/en-US/changelogs/60.0.0.txt ================================================ - Use Android 14 compatible backup file format. Backup again before upgrading Android to version 14. - Improved thread handling - New German translations ================================================ FILE: fastlane/metadata/android/en-US/changelogs/60.1.0.txt ================================================ - Fixed deleting of avatar file when avatar is removed from contact or when contact with avatar is removed - Fixed saving of account's DTMF Mode - New Spanish, Portuguese, and Portuguese (Brazil) translations ================================================ FILE: fastlane/metadata/android/en-US/changelogs/60.2.0.txt ================================================ - Added 'In-band RTP or SIP INFO' account DTMF Mode - Added Speaker Phone audio setting - New German, Portuguese (Brazil), and Spanish translations ================================================ FILE: fastlane/metadata/android/en-US/changelogs/60.3.0.txt ================================================ - Added support for SHA-256 digest authentication - Verify Server Certificates settings can now be changed without restart - New Czech, German, Spanish, and Portuguese (Brazil) translations - Fixed formats of some strings in some languages ================================================ FILE: fastlane/metadata/android/en-US/changelogs/60.4.0.txt ================================================ - Properly handle transfer request when original call is already closed - Fixed handling of stateless TLS SIP requests - New Portuguese, Portuguese (Brazil), Spanish, French, Russian, English, Finnish, Swedish, German, and Czech translations ================================================ FILE: fastlane/metadata/android/en-US/changelogs/60.4.1.txt ================================================ - SRTP related security fix from upstream - New Czech translations ================================================ FILE: fastlane/metadata/android/en-US/changelogs/60.4.2.txt ================================================ - Fixed call recording - Do not send '180 Ringing' response if incoming call is auto-rejected - Remove also %20 (space) from call tel URI - New German translations ================================================ FILE: fastlane/metadata/android/en-US/changelogs/61.0.0.txt ================================================ - Added User Agent setting that can be used to set custom SIP request/response User-Agent header field value - Allow , and # characters in tel number (for example when calling Teams) - Use accent color instead of bold in Call History time column if a call in that call row has been recorded - Do not include uuid in backup file (prevents having many baresip UAs with same identity) - By default, include date and time in backup file name - New Spanish and Portuguese (Brazil) translations ================================================ FILE: fastlane/metadata/android/en-US/changelogs/61.0.1.txt ================================================ - If DTMF is enabled, keep focus at screen orientation changes - Changed 'The Test Call' contact's URI to 'sip:thetestcall@sip2sip.info' - When showing URI in friendly form, show URI parameters except ';transport=udp' - Removed '.bs' suffix from backup file name - New translations (Russian) ================================================ FILE: fastlane/metadata/android/en-US/changelogs/61.1.0.txt ================================================ - Added Microphone Gain audio setting - Added Logcat main menu item - New Czech, Norwegian (Bokmål), German, Portuguese (Brazil), and Spanish translations ================================================ FILE: fastlane/metadata/android/en-US/changelogs/62.0.0.txt ================================================ - If available, use Acoustic Echo Canceler, Automatic Gain Control, and Noise Suppressor provided by the device - Android 9 (API level 28) is now the minimum supported version ================================================ FILE: fastlane/metadata/android/en-US/changelogs/62.1.0.txt ================================================ - Opt out edge-to-edge enforcement for Android 15 compatibility - Avoid crashes in BootCompletedReceiver and when checking outbound proxy URI - New Finnish, Bulgarian, Japanese, Portuguese (Brazil), Spanish, Czech, and Polish translations ================================================ FILE: fastlane/metadata/android/en-US/changelogs/63.0.0.txt ================================================ - Added support for edge-to-edge content display for Android 15 compatibility - Rounded alert dialog corners - Don't automatically show soft keyboard when configuration changes - Added new language (Tamil) ================================================ FILE: fastlane/metadata/android/en-US/changelogs/63.1.0.txt ================================================ - Prevent soft keyboard from hiding input text field in some activities - Fixed telephone-event payload type number - Prevent crash when backup file is large - New translations (German) ================================================ FILE: fastlane/metadata/android/en-US/changelogs/63.2.0.txt ================================================ - Apply Acoustic Echo Cancellation only to recorder session - Do not backup recordings - New Romanian, Portuguese (Brazil), German and Czech translations ================================================ FILE: fastlane/metadata/android/en-US/changelogs/63.2.1.txt ================================================ - Improved setting of audio effects ================================================ FILE: fastlane/metadata/android/en-US/changelogs/63.2.2.txt ================================================ - Reinstalled player AEC ================================================ FILE: fastlane/metadata/android/en-US/changelogs/63.2.3.txt ================================================ - Allow use of software AEC even when hardware one would be available ================================================ FILE: fastlane/metadata/android/en-US/changelogs/63.2.5.txt ================================================ - Alert if hardware AEC is not available when software AEC is disabled in audio settings - New translations (Portuguese Brazil) ================================================ FILE: fastlane/metadata/android/en-US/changelogs/63.3.0.txt ================================================ - Allow * and # characters in telephone number - New translations (Portuguese (Brazil)) ================================================ FILE: fastlane/metadata/android/en-US/changelogs/64.0.0.txt ================================================ - Migrated of user interface to Jetpack Compose (may include bugs) - Always try to use hardware Acoustic Echo Canceler if available ================================================ FILE: fastlane/metadata/android/en-US/changelogs/64.1.0.txt ================================================ - Several user interface related fixes and improvements - New translations (Portuguese (Brazil)) ================================================ FILE: fastlane/metadata/android/en-US/changelogs/64.2.0.txt ================================================ - Added vertical scrollbars to Settings, Account, and Audio activities - Fixed call and transfer URI suggestions handling - Dialog style improvements - New translations (Portuguese) ================================================ FILE: fastlane/metadata/android/en-US/changelogs/64.3.0.txt ================================================ - Fixed handling of DTMF backspace character - Chat and contact related UI improvements - Use background color also in system bars ================================================ FILE: fastlane/metadata/android/en-US/changelogs/64.3.1.txt ================================================ - Fixed handling of DTMF backspace character - Chat and contact related UI improvements - Use background color also in system bars - Fixed accounts activity crash when no accounts ================================================ FILE: fastlane/metadata/android/en-US/changelogs/65.0.0.txt ================================================ - Completed user interface conversion to Jetpack Compose - Fixed Verify Server Certificates setting - New Portuguese translations ================================================ FILE: fastlane/metadata/android/en-US/changelogs/65.1.0.txt ================================================ - Restored swipe left/right gesture to toggle between accounts - Account and Settings user interface fixes and improvements ================================================ FILE: fastlane/metadata/android/en-US/changelogs/65.2.0.txt ================================================ - Improved pull-to-register look and implementation - Make sure bluetoothreceiver is registered before unregistering it - Fixed back press action for API < 33 compatibility ================================================ FILE: fastlane/metadata/android/en-US/changelogs/65.2.1.txt ================================================ - Improved pull-to-register look and implementation - Make sure bluetoothreceiver is registered before unregistering it - Fixed back press action for API < 33 compatibility - Fixed ZRTP verify query ================================================ FILE: fastlane/metadata/android/en-US/changelogs/65.3.0.txt ================================================ - Added Ringtone setting - Improved pull-to-register look and implementation - Make sure bluetoothreceiver is registered before unregistering it - Fixed back press action for API < 33 compatibility - Fixed ZRTP verify query ================================================ FILE: fastlane/metadata/android/en-US/changelogs/66.0.0.txt ================================================ - Use switches instead of check boxes in all activities - Alert at start if network or hardware Acoustic Echo Cancellation is not available - Fixed bug that caused truncation of called telephone number - Avoid crash related to using baresip as the dialer app ================================================ FILE: fastlane/metadata/android/en-US/changelogs/66.1.0.txt ================================================ - Added Numeric Keypad account settings - Added tooltips to main activity top app bar icons - Show account's full SIP URI on Account page - New translations (Japanese and Tamil) ================================================ FILE: fastlane/metadata/android/en-US/changelogs/66.1.1.txt ================================================ - Added Numeric Keypad account settings - Added tooltips to main activity top app bar icons - Show account's full SIP URI on Account page - Restored unsent new message after pause/resume - New translations (Japanese and Tamil) ================================================ FILE: fastlane/metadata/android/en-US/changelogs/66.1.2.txt ================================================ - Added Numeric Keypad account settings - Added tooltips to main activity top app bar icons - Show account's full SIP URI on Account page - Restored unsent new message after pause/resume - In chat activity, fixed adding chat peer to contacts - New translations (Japanese and Tamil) ================================================ FILE: fastlane/metadata/android/en-US/changelogs/66.1.3.txt ================================================ - Added Numeric Keypad account settings - Added tooltips to main activity top app bar icons - Show account's full SIP URI on Account page - Restored unsent new message after pause/resume - In chat activity, fixed adding chat peer to contact - Added cancel button for canceling outgoing call - Fixed showing of call transfer target suggestions - New translations (Japanese and Tamil) ================================================ FILE: fastlane/metadata/android/en-US/changelogs/66.1.4.txt ================================================ - Several Settings related fixes - Improved transfer dialog layout - New translations (German) ================================================ FILE: fastlane/metadata/android/en-US/changelogs/66.1.5.txt ================================================ - Fixed initialization of account from network ================================================ FILE: fastlane/metadata/android/en-US/changelogs/66.1.6.txt ================================================ - Apply volume keys to ringtone volume when call is coming in and to in-call volume during call - Use toasts for no network and no hw aec notices ================================================ FILE: fastlane/metadata/android/en-US/changelogs/67.0.0.txt ================================================ - Migrated baresip app from activities to Jetpack Compose navigation - Improved handling of calls and messages that arrive when baresip is not visible - Some other minor user interface improvements - Upstream fix of Refer-To header To/From tags ================================================ FILE: fastlane/metadata/android/en-US/changelogs/67.0.1.txt ================================================ - Stop call timer chronometer when call is closed - Add microphone to status notification only when record audio permission has been granted - New translations (Tamil) ================================================ FILE: fastlane/metadata/android/en-US/changelogs/67.0.2.txt ================================================ - Default Call Volume audio setting bug fix - Backup/restore improvement and bug fix - New translations (German) ================================================ FILE: fastlane/metadata/android/en-US/changelogs/67.0.3.txt ================================================ - Account and Settings screen fixes and improvements ================================================ FILE: fastlane/metadata/android/en-US/changelogs/67.1.0.txt ================================================ - In call history, use blue arrow down icon to indicate that call has been answered elsewhere - Added on-click help texts to call details arrow icons ================================================ FILE: fastlane/metadata/android/en-US/changelogs/67.2.0.txt ================================================ - Added Colorblind setting that currently affects registration status icons - Improved names of notification channels ================================================ FILE: fastlane/metadata/android/en-US/changelogs/68.0.0.txt ================================================ - Targeting Android 16 API level 36 - Added Proximity Sensing setting - Fixed Contacts setting Both value - New translations (Czech) ================================================ FILE: fastlane/metadata/android/en-US/changelogs/68.1.0.txt ================================================ - Improved display of call history time - Changed background color of colorblind status icons - Fixed update of status notification - Fixed possible crash related to Registration interval account setting ================================================ FILE: fastlane/metadata/android/en-US/changelogs/68.1.1.txt ================================================ - Improved display of call history time - Changed background color of colorblind status icons - Fixed possible crash related to Registration interval account setting - Fixed update of status notification ================================================ FILE: fastlane/metadata/android/en-US/changelogs/68.1.2.txt ================================================ - Improved display of call history time - Changed background color of colorblind status icons - Fixed possible crash related to Registration interval account setting - Fixed update of status notification ================================================ FILE: fastlane/metadata/android/en-US/changelogs/7.0.0.txt ================================================ - Added support for IPv6 (not tested due to lack of access to IPv6 network). - Added "Prefer IPv6" configuration option. - Added "Reset to Factory Default" configuration option. - At Quit, make sure that also baresip task is stopped. ================================================ FILE: fastlane/metadata/android/en-US/changelogs/7.1.0.txt ================================================ - Added 'Default Call Volume' configuration option. ================================================ FILE: fastlane/metadata/android/en-US/changelogs/7.1.1.txt ================================================ - Update account spinner also when main activity is resumed without action. ================================================ FILE: fastlane/metadata/android/en-US/changelogs/7.1.2.txt ================================================ - Tried to make sure that also account spinner view is updated when account spinner is updated. ================================================ FILE: fastlane/metadata/android/en-US/changelogs/7.2.0.txt ================================================ - Added new config variable Listen Address that allows specifying an IP address (or all addresses) and a port at which baresip is listening at for SIP requests. ================================================ FILE: fastlane/metadata/android/en-US/changelogs/7.3.0.txt ================================================ - Account media NAT protocol can now be chosen between STUN, ICE, or none. - Set account status to yellow immediately when account's registration is off. ================================================ FILE: fastlane/metadata/android/en-US/changelogs/7.4.0.txt ================================================ - Aligned alerts with material design guidelines. - Improved language string handling. - Added Finnish language translation as an example. ================================================ FILE: fastlane/metadata/android/en-US/changelogs/8.0.0.txt ================================================ - Added support for G.722.1 audio codec. - Added support for WebRTC based audio echo cancellation (excludes Opus codec). - Minor call notification changes. ================================================ FILE: fastlane/metadata/android/en-US/changelogs/8.0.1.txt ================================================ - Fixed restoring of volume to original level if default call volume is in use. ================================================ FILE: fastlane/metadata/android/en-US/changelogs/8.0.2.txt ================================================ - Fixed adding of new account. ================================================ FILE: fastlane/metadata/android/en-US/changelogs/8.1.0.txt ================================================ - Obtain DNS server addresses dynamically from the system if DNS Servers are not given in Configuration. - If there is only one account, append account's domain to contact URI given without hostpart. ================================================ FILE: fastlane/metadata/android/en-US/changelogs/8.2.0.txt ================================================ - Improved content and appearance of About page. - Always show currently active call (if any) when app is relaunched. - Changed location of alert dialog OK button. ================================================ FILE: fastlane/metadata/android/en-US/changelogs/8.3.0.txt ================================================ - Added Internet Low Bitrate Codec (iLBC). - Improved account's codec selection. ================================================ FILE: fastlane/metadata/android/en-US/changelogs/8.3.1.txt ================================================ - Don't set audio to communication mode when call is established, because on some devices it adds 3-4 sec delay to playing of audio. - Made Acoustic Echo Cancellation configurable. - Enable call info button when it becomes visible. - Use default device theme when displaying spinners. ================================================ FILE: fastlane/metadata/android/en-US/changelogs/8.3.2.txt ================================================ - If account is accessed from AoR spinner, return directly to Main Activity. - Use HtmlCompat to convert About HTML to text (should work on all Android API levels). - Use Ringtone looping capability on Android P+. ================================================ FILE: fastlane/metadata/android/en-US/changelogs/8.3.3.txt ================================================ - Get DNS servers dynamically also when baresip is started the very first time. - Avoid possible crash when getting account's audio codecs. ================================================ FILE: fastlane/metadata/android/en-US/changelogs/8.3.4,txt ================================================ - Use communication mode only when call is (being) established. - Introduced 2.5 sec delay to placing of a call in order to avoid loss of audio at the beginning of call. - Fixed initializing default contacts when baresip is started the first time. - Clear call URI when call is closed. ================================================ FILE: fastlane/metadata/android/en-US/changelogs/8.3.5.txt ================================================ - Surround contact URIs between < and > only when contacts are saved to file. ================================================ FILE: fastlane/metadata/android/en-US/changelogs/8.3.6.txt ================================================ - Allow changing of contact name also when only case of letters differ. - When restoring contacts, do not require that URIs are surrounded by angle brackets. ================================================ FILE: fastlane/metadata/android/en-US/changelogs/8.4.0.txt ================================================ - Echo cancellation is now also applied to calls using the Opus codec. ================================================ FILE: fastlane/metadata/android/en-US/changelogs/8.4.1.txt ================================================ - Upstream acoustic echo cancellation improvement. - New translations: el, nb_NO, ro. ================================================ FILE: fastlane/metadata/android/en-US/changelogs/8.4.2.txt ================================================ - Update DNS servers and register UAs always when network becomes available. - Fixed and updated some texts. ================================================ FILE: fastlane/metadata/android/en-US/changelogs/8.4.3.txt ================================================ - Fixed showing of account status, when default account is changed. ================================================ FILE: fastlane/metadata/android/en-US/changelogs/8.4.4.txt ================================================ - Upstream fix in opus codec implementation. - Volume control stream fixes. - Fixed auto-rejecting of new incoming call. - Minor string improvements. ================================================ FILE: fastlane/metadata/android/en-US/changelogs/8.5.0.txt ================================================ - Preliminary use of proximity sensor to turn off touch screen during call. ================================================ FILE: fastlane/metadata/android/en-US/changelogs/8.5.1.txt ================================================ - Moved handling of proximity sensing from MainActivity to BaresipService. - Fixed crash when call or message comes in and MainActivity is not running. ================================================ FILE: fastlane/metadata/android/en-US/changelogs/8.5.2.txt ================================================ - Fixed checking of Configuration DNS Servers. ================================================ FILE: fastlane/metadata/android/en-US/changelogs/8.6.0.txt ================================================ - Added support for SIP registrars that do not correctly handle nonce reuse. ================================================ FILE: fastlane/metadata/android/en-US/changelogs/9.0.0.txt ================================================ - Added possibility to configure baresip a TLS certificate and CA certificates. ================================================ FILE: fastlane/metadata/android/en-US/changelogs/9.1.0.txt ================================================ - Auto-reject incoming call if phone state is not idle. - Suppress ringtone of incoming call if device is in "Do not disturb" mode. ================================================ FILE: fastlane/metadata/android/en-US/changelogs/9.2.0.txt ================================================ - Added support for Opus Forward Error Correction (FEC) via Opus Expected Packet Loss configuration variable. - Try to update dynamic DNS information if registration fails due to "Invalid argument" error. - Use translatable string in all configuration related alert messages. ================================================ FILE: fastlane/metadata/android/en-US/changelogs/9.3.0.txt ================================================ - Added Answer Mode (Manual or Automatic) account configuration option. - Added Norwegian Bokmål strings and changelogs from Weblate. ================================================ FILE: fastlane/metadata/android/en-US/changelogs/9.4.0.txt ================================================ - Added "Restart" Main Activity menu option. - When registration fails due to DNS lookup failure, try registration once more after updating DNS servers. - Small logging improvements. ================================================ FILE: fastlane/metadata/android/en-US/full_description.txt ================================================ This is a baresip based SIP User Agent application for Android. Currently baresip app supports voice calling, voice conference calls, text messages, voicemail Message Waiting Indication as well as blind and attended call transfers. Voice can be coded with Opus, AMR, Codec2, G.729, G.722, G.722.1, or PCMU/PCMA codecs. Security is achieved via TLS or WSS SIP signaling transport and ZRTP or (DTLS) SRTP media encapsulation. Development of baresip app is motivated by need for a secure, open source SIP based VoIP User Agent for Android that does not depend on proprietary, third party push notification services. This application can be installed on Android devices running Android version 9 or later. If you need video calling, you can instead of this application install its sister application baresip+. Source code is available in master branch of GitHub project, where also issues can be reported. ================================================ FILE: fastlane/metadata/android/en-US/short_description.txt ================================================ VoIP User Agent app for Android based on baresip SIP library ================================================ FILE: fastlane/metadata/android/en-US/title.txt ================================================ baresip ================================================ FILE: fastlane/metadata/android/fi-FI/changelogs/10.0.0.txt ================================================ - Jatka Baresip keskeytettyyn toimintaan päätoiminnan sijaan. ================================================ FILE: fastlane/metadata/android/fi-FI/changelogs/10.1.0.txt ================================================ - Lisätty tuki G722-koodekille. - korjattu tilien tallentaminen. ================================================ FILE: fastlane/metadata/android/fi-FI/changelogs/10.2.0.txt ================================================ - Lisätty tuki G.726-koodekille. - Parannettu koodekin oletusetusijajärjestys. ================================================ FILE: fastlane/metadata/android/fi-FI/changelogs/10.3.0.txt ================================================ - Lisätty Puheluhistoria-valikko, joka mahdollistaa tilin puheluhistorian poistamisen, poistamisen käytöstä ja käyttöönoton. - Lisätty espanjankieliset merkkijonot (hyvityksiä Javier Falbolle). - Baresipin automaattinen käynnistys on nyt oletuksena uusissa asennuksissa. ================================================ FILE: fastlane/metadata/android/fi-FI/changelogs/10.4.0.txt ================================================ - Salli tilin pikakeskusteluhistorian poistaminen pikakeskustelutoimintavalikkokohdan kautta. - Pyydä vahvistus ennen pikakeskustelun ja puheluhistorian poistamista. - Parannettu päätoimintojen puhepostin, pikakeskustelun ja puhelukuvakkeiden päivitys. - Siirry suoraan Tilitoimintaan, kun baresip käynnistetään ilman tilejä. - Vaihdettu puhelun hyväksymis- ja hylkäämispainikkeiden paikkoja. - Älä salli lähtevän puhelun lopettamista ennen kuin soittoyritys on alkanut. ================================================ FILE: fastlane/metadata/android/fi-FI/changelogs/11.0.0.txt ================================================ - Tilien ja yhteystietojen vienti ja tuonti korvattu kaikkien sovellustietojen varmuuskopioinnilla ja palautuksella. - Parannettu suojaus estämällä Android-pohjaisten sovellusten varmuuskopiointi ja sovelluksen asentaminen ulkoiseen tallennustilaan. - Kun tili poistetaan, poista myös tilin puhelu- ja pikakeskusteluhistoria. - Korjattu viestien ja pikakeskustelujen poistaminen. - Pienet käyttöliittymä- ja merkkijonoparannukset. ================================================ FILE: fastlane/metadata/android/fi-FI/changelogs/11.1.0.txt ================================================ - Lisätty 'Bind Address' -kokoonpanomuuttuja, jonka avulla voit valita, mitä verkkoliitäntää tai IP-osoitetta Baresip käyttää. - Päivitä aina tilien tilakuvake, kun palaat päätoimintaan. - Korjattu kaatuminen, kun palataan aiemmin tuhoutuneeseen päätoimintaan. - Näytä aktiiviset saapuvat puhelut oikein, kun palaat päätoimintaan. - Päivitetyt espanjankieliset merkkijonot. ================================================ FILE: fastlane/metadata/android/fi-FI/changelogs/11.2.0.txt ================================================ - Lisätty tuki AMR-kapeakaistaiselle koodekille. - Lisätty bulgarialaiset merkkijonot. - Päävalikon Kokoonpanokohde nimettiin uudelleen Asetuksiksi. ================================================ FILE: fastlane/metadata/android/fi-FI/changelogs/11.3.0.txt ================================================ - Lisätty Asetuksiin mahdollisuus valita ladattavat äänimoduulit. Ladattujen moduulien toimittamat äänikoodekit ovat saatavilla tilien käyttöön. - Pienet koodin parannukset. ================================================ FILE: fastlane/metadata/android/fi-FI/changelogs/12.0.0.txt ================================================ - Alkutoteutus äänen käyttöönotosta Bluetooth-kuulokkeiden kautta. ================================================ FILE: fastlane/metadata/android/fi-FI/changelogs/12.1.0.txt ================================================ - Salli portin numero tilin tietueosoitteessa (AoR). - Korjattu tarkistus, onko tili jo olemassa. - Päivitetty NO- ja BG-kieliset merkkijonot. ================================================ FILE: fastlane/metadata/android/fi-FI/changelogs/12.2.0.txt ================================================ - Käytettiin uusia API-funktioita 'net_set_address' ja 'net_set_af' Prefer IPv6 -toiminnallisuuden toteuttamiseen. - Sidososoite poistettiin asetuksista (se ei toiminut odotetulla tavalla). ================================================ FILE: fastlane/metadata/android/fi-FI/changelogs/12.2.1.txt ================================================ - Dynaamisten verkkojen käsittely parannettu ja korjattu. ================================================ FILE: fastlane/metadata/android/fi-FI/changelogs/13.0.0.txt ================================================ - Toista ilmoitusääni myös kun viesti saapuu ja baresip on näkyvissä. - Jos yhteyshenkilön nimi on tyhjä, käytä yhteyshenkilön nimenä yhteyshenkilön SIP URI AoR :ta. - Yhteyshenkilön nimen tarkistaminen tehtiin rennommaksi. - Vältä satunnaista kaatumista valitessasi tiliä spinnerissä. - Pyydä äänitallentamisen (mikrofonin) lupa, kun Baresip alkaa (jos sitä ei ole jo myönnetty tai evätty pysyvästi). - Ulkoisen tallennustilan (tallennustilan) kirjoitus-/lukulupien parannettu käsittely varmuuskopioinnin/palautuksen yhteydessä. - Puhelimen tilan (Puhelin) -lupaa ei tarvita. ================================================ FILE: fastlane/metadata/android/fi-FI/full_description.txt ================================================ Tämä on baresip-pohjainen SIP-käyttäjäagenttisovellus Androidille. Tällä hetkellä baresip-sovellus tukee äänipuheluita. tekstiviestejä, vastaajaviestin odotusilmoitus sekä sokeat ja osallistuneet puhelunsiirrot. Ääni voidaan koodata Opus-, AMR-, Codec2-, G.729-, G.722-, G.722.1-, G.726- tai PCMU/PCMA-koodekeilla. Turvallisuus saavutetaan TLS- tai WSS-SIP-signalointikuljetuksella ja ZRTP- tai (DTLS) SRTP-mediakapseloinnilla. Baresip-sovelluksen kehitystä motivoi tarve turvalliselle, avoimen lähdekoodin SIP-pohjaiselle VoIP-käyttäjäagentille Androidille, joka ei ole riippuvainen patentoiduista, kolmannen osapuolen työntö-ilmoituspalveluista. Jos tarvitset videopuheluita ja sinulla on Android-versio 9.0 tai uudempi laite, joka tukee Camera2 API:a laitteistotukitasolla LEVEL 3 tai FULL, voit tämän sovelluksen sijaan asentaa sen sisarsovelluksen baresip+. Lähdekoodi on saatavilla GitHubissa, jossa myös ongelmista voi ilmoittaa. ================================================ FILE: fastlane/metadata/android/fi-FI/short_description.txt ================================================ VoIP-käyttäjäagenttisovellus Androidille, joka perustuu Baresip SIP -kirjastoon ================================================ FILE: fastlane/metadata/android/fi-FI/title.txt ================================================ baresip ================================================ FILE: fastlane/metadata/android/iw-IL/changelogs/10.0.0.txt ================================================ - שיחזור של baresip לפעילות שהושהתה במקום לפעילות הראשית. ================================================ FILE: fastlane/metadata/android/iw-IL/changelogs/10.1.0.txt ================================================ - נוספה תמיכה לקודק G722. - תוקנה שמירת חשבונות. ================================================ FILE: fastlane/metadata/android/iw-IL/changelogs/10.2.0.txt ================================================ - נוספה תמיכה בקודק G.726.. - שיפור סדר עדיפויות ברירת המחדל של הקודקים. ================================================ FILE: fastlane/metadata/android/iw-IL/changelogs/10.3.0.txt ================================================ - נוסף תפריט "היסטוריית שיחות" שמאפשר מחיקה, השבתה והפעלה מחדש של היסטוריית השיחות בחשבון. - נוספו מיתוגים בשפה הספרדית (תודה ל־Javier Falbo). - הפעלה אוטומטית של baresip היא עכשיו ברירת המחדל בהתקנות חדשות. ================================================ FILE: fastlane/metadata/android/iw-IL/changelogs/10.4.0.txt ================================================ - מאפשרת מחיקת היסטוריית הצ'אטים של החשבון דרך תפריט פעילות הצ'אטים. - בקשת אישור לפני מחיקת היסטוריית הצ'אטים והשיחות. - שיפור בעדכון אייקוני תיבת דואר קולי, צ'אט ושיחות בפעילות הראשית. - העברה ישירה לפעילות החשבונות כאשר baresip מתחיל ללא חשבונות. - הוחלפו המקומות של כפתורי קבלה ודחיית שיחה. - אין אפשרות לסיום שיחה יוצאת לפני שהניסיון להתחיל את השיחה התרחש. ================================================ FILE: fastlane/metadata/android/iw-IL/changelogs/11.0.0.txt ================================================ - הוחלפו ייבוא ויצוא של חשבונות ואנשי קשר בגיבוי ושחזור של כל נתוני האפליקציה. - שיפור האבטחה על ידי מניעת גיבוי של אפליקציות מבוססות אנדרואיד והתקנת אפליקציות בזיכרון חיצוני. - כאשר חשבון נמחק, נמחקים גם היסטוריית השיחות והצ'אטים של החשבון. - תוקנה בעיית מחיקת הודעות וצ'אטים. - שיפורים קטנים בממשק ובמיתוגים. ================================================ FILE: fastlane/metadata/android/iw-IL/changelogs/11.1.0.txt ================================================ - נוספה משתנה קונפיגורציה 'Bind Address' שמאפשר לבחור איזה ממשק רשת או כתובת IP אפליקציית baresip תשתמש. - תמיד לעדכן את אייקון הסטטוס של החשבונות בעת חזרה לפעילות הראשית. - תוקנה קריסה בעת חזרה לפעילות הראשית שהושמדה קודם. - שיחה נכנסת פעילה מוצגת כראוי בעת חזרה לפעילות הראשית. - עודכנו המיתוגים בספרדית. ================================================ FILE: fastlane/metadata/android/iw-IL/changelogs/11.2.0.txt ================================================ - נוספה תמיכה בקודק AMR narrowband. - נוספו מיתוגים בשפה הבולגרית. - שונה שם פריט בתפריט הראשי מ-"Configuration" ל-"Settings". ================================================ FILE: fastlane/metadata/android/iw-IL/changelogs/11.3.0.txt ================================================ - נוספה בהגדרות אפשרות לבחור אילו מודולי אודיו ייטענו. קודקי האודיו שסופקו על ידי המודולים הטעונים זמינים לשימוש עבור החשבונות. - שיפורי קוד קטנים. ================================================ FILE: fastlane/metadata/android/iw-IL/changelogs/12.0.0.txt ================================================ - יישום ראשוני של אודיו דרך אוזניות Bluetooth. ================================================ FILE: fastlane/metadata/android/iw-IL/changelogs/12.1.0.txt ================================================ - אפשור מספר פורט בכתובת הרשומה של החשבון (AoR). - תוקנה בדיקה אם החשבון כבר קיים. - עודכנו המיתוגים בנורווגית ובולגרית. ================================================ FILE: fastlane/metadata/android/iw-IL/changelogs/12.2.0.txt ================================================ - השתמש בפונקציות API חדשות 'net_set_address' ו־'net_set_af' כדי ליישם את פונקציית ההעדפה ל־IPv6. - הוסר פריט "Bind Address" מהגדרות (כיוון שלא פעל כפי שצפוי). ================================================ FILE: fastlane/metadata/android/iw-IL/changelogs/12.2.1.txt ================================================ - שיפור ותיקון טיפול בשינויים דינמיים ברשת. ================================================ FILE: fastlane/metadata/android/iw-IL/changelogs/13.0.0.txt ================================================ - הפעלת צליל התראה גם כאשר הודעה מגיעה ו־baresip נמצא במצב גלוי. - אם שם איש הקשר ריק, השתמש ב־SIP URI AoR של איש הקשר כשם. - שיפור בבדיקת שם איש הקשר. - מניעת קריסה אקראית בעת בחירת חשבון ברשימת הספינר. - בקשת הרשאת הקלטת אודיו (מיקרופון) כאשר baresip מתחיל (אם לא אושרה או הושללה לצמיתות). - שיפור בטיפול בהרשאות קריאה/כתיבה לאחסון חיצוני בעת גיבוי/שחזור. - הרשאת קריאת מצב הטלפון (Phone) איננה נדרשת. ================================================ FILE: fastlane/metadata/android/iw-IL/changelogs/13.0.1.txt ================================================ - תמיד להודיע לספינר של סוכן המשתמש על שינוי אפשרי בסט הנתונים בעת חזרה לפעילות הראשית ללא כוונה. ================================================ FILE: fastlane/metadata/android/iw-IL/changelogs/14.0.0.txt ================================================ - נוספו אווטארים מבוססי תווים לאנשי קשר, צ'אטים והיסטוריית שיחות. - תוקנה שמירה/שחזור של אנשי קשר עם שמות לא ASCII. - שיפור בעיצוב התאריכים והשעות. - נוספה הערת הגבלה ב"אודות" לגבי ממשקי רשת מרובים. ================================================ FILE: fastlane/metadata/android/iw-IL/changelogs/15.0.0.txt ================================================ - נוספה תמיכה בתמונות כאווטארים של אנשי קשר, כחלופה לאווטארים מבוססי תווים. - שיפורים ותיקונים קלים בממשק המשתמש. ================================================ FILE: fastlane/metadata/android/iw-IL/changelogs/15.1.0.txt ================================================ - נוספה אפשרות להגדיר פרוטוקול תעבורה עבור ה־AoR של החשבון. - אל תציג את יציאת (port) החשבון או את פרוטוקול התעבורה, למעט במסך פעילות החשבון. ================================================ FILE: fastlane/metadata/android/iw-IL/changelogs/15.1.1.txt ================================================ - תיקונים ושיפורים קטנים במיתוגים. ================================================ FILE: fastlane/metadata/android/iw-IL/changelogs/16.0.0.txt ================================================ - הוחלפה ההגדרה הגלובלית "העדף IPv6" בהגדרה ייעודית לחשבון "העדף מדיית IPv6". - שיפור הטיפול בשינויים בקישוריות הרשת. - לא נדרש ציון פורט ב־URI יוצא של IPv6. - נטישת מיקוד השמע מתבצע גם בעת יציאה מהיישומון, אם לא בוצע קודם לכן. - נוספה תמיכה ראשונית בשפה הרוסית. - עודכנו מספר מחרוזות בנורווגית. ================================================ FILE: fastlane/metadata/android/iw-IL/changelogs/16.0.1.txt ================================================ - רישום מחדש של סוכני משתמש (UA) מתבצע כאשר רשת פעילה חדשה זמינה או כאשר מאפייני הקישור משתנים, גם אם כתובות ה־IP נשארות זהות. ================================================ FILE: fastlane/metadata/android/iw-IL/changelogs/16.1.0.txt ================================================ - אין לבצע רישום מחדש של חשבונות כאשר מאפייני הקישור משתנים אך כתובות ה־IP נשארות זהות. - אם baresip אינו מופעל, אפשר למשתמש לשנות את ההגדרות שייתכן וגרמו לבעיה. - עדכונים ותיקונים למחרוזות. ================================================ FILE: fastlane/metadata/android/iw-IL/changelogs/17.0.0.txt ================================================ - תוקנה שמירת חשבון שיש בפרמטר ה־AoR שלו ;transport. - תוקנה הפעלת ההתראות כאשר שיחה נסגרת. - שימוש באייקון סטטוס לבן כאשר רישום החשבון אינו מופעל. - עוצמת השיחה המוגדרת כברירת מחדל משפיעה כעת גם על שיחה קולית וגם על מוזיקה. - שופר העיצוב והמימוש של מסך החשבונות. - אין להציג הודעות ניפוי באגים של ספריית baresip כאשר האפשרות 'ניפוי באגים' אינה מופעלת. - אין ליצור מחדש פעילויות כאשר כיוון המסך משתנה. - הצגה תקינה של פרטי צ’אט שאינם נכנסים לשורה אחת ================================================ FILE: fastlane/metadata/android/iw-IL/changelogs/17.1.0.txt ================================================ - שימוש בסוג שימוש (usage type) במקום בסוג זרם (stream type) בעת בקשת מיקוד שמע. - תוקן העיצוב של טקסט העזרה לחשבון באנגלית. - אין להציג את הפורט האפשרי של ה־AoR בעת בקשה למחיקת ה־AoR. - אייקון מצב חשבון לא רשום נצבע בלבן. ================================================ FILE: fastlane/metadata/android/iw-IL/changelogs/17.2.0.txt ================================================ - שם המשתמש לאימות מוגדר כעת כברירת מחדל לשם המשתמש של החשבון. - בעת הפעלת baresip, אם צוין שם משתמש לאימות אך לא סיסמה, תתבצע בקשה להזנת סיסמת האימות. - אתחול נקודת מצב החשבון לצהוב כאשר האפשרות 'הרשמה' מסומנת בפעילות החשבון. - תוקן באג הקשור ליצירת החשבון הראשון בגרסאות אנדרואיד ישנות יותר. - שיפורים קלים במחרוזות. ================================================ FILE: fastlane/metadata/android/iw-IL/changelogs/17.2.1.txt ================================================ - תוקן באג בשמירת חשבונות במערכת הקבצים. ================================================ FILE: fastlane/metadata/android/iw-IL/changelogs/17.2.2.txt ================================================ - אין לבצע הרשמה (subscribe) לחיווי המתנה להודעות בעת יצירת חשבון כשכתובת ה־URI של התא הקולי אינה מוגדרת. ================================================ FILE: fastlane/metadata/android/iw-IL/changelogs/17.2.3.txt ================================================ - מניעת קריסות הנגרמות מלחיצה כפולה על אייקונים ופריטי רשימה שונים. - שינוי צבע אייקון ההתראה של החשבון ללבן כאשר החשבון אינו רשום. ================================================ FILE: fastlane/metadata/android/iw-IL/changelogs/17.3.0.txt ================================================ - נוספה תיבת סימון "הצג סיסמה" למסך הזנת סיסמת האימות. - בקשת הרשאת מיקרופון מתבצעת כפעולה הראשונה עם הפעלת baresip. - אין לאפשר שימוש בתו " בסיסמת האימות של החשבון. ================================================ FILE: fastlane/metadata/android/iw-IL/changelogs/17.4.0.txt ================================================ - נוספה תמיכה באנדרואיד 10 (API רמה 29). - שיפור תהליך בקשת הרשאת המיקרופון בעת הפעלת baresip בפעם הראשונה. - מניעת קריסה בלחיצה על כפתור השיחה כשאין חשבונות מוגדרים. ================================================ FILE: fastlane/metadata/android/iw-IL/changelogs/18.0.0.txt ================================================ - בשל מגבלות אנדרואיד 10, אין אפשרות עוד להפעיל את baresip אוטומטית לאחר אתחול המכשיר ללא הצגת התראה. - תוקנה הפעלה מחדש של baresip באנדרואיד 10. - הוסרה ההגדרה של ICE Lite Mode עקב הסרתה במקור (upstream). - נוספו מחרוזות בפורטוגזית ברזילאית מ־Weblate. - תיקונים קטנים במחרוזות בספרדית ובנורווגית. ================================================ FILE: fastlane/metadata/android/iw-IL/changelogs/18.0.1.txt ================================================ - הגדלה בצד ה־Upstream של המספר המרבי של קודקי האודיו בחשבון מ־8 ל־16. ================================================ FILE: fastlane/metadata/android/iw-IL/changelogs/18.1.0.txt ================================================ - שופרה הגדרת מקודדי השמע של החשבון. - הצג תמיד שיחה נכנסת (אם קיימת) בעת חידוש הפעילות של baresip. - שופרו ותוקנו חזרה לפעילויות שאינן הפעילות הראשית. ================================================ FILE: fastlane/metadata/android/iw-IL/changelogs/18.1.1.txt ================================================ - תוקנה קריסה כאשר מוציאים שיחה ממסך היסטוריית השיחות. ================================================ FILE: fastlane/metadata/android/iw-IL/changelogs/18.1.2.txt ================================================ - תוקנה קריסה בעת חזרה לפעילות הקודמת לאחר קבלת שיחה או הודעה. - שיפור הטיפול בחיישן הקרבה וברמקול. ================================================ FILE: fastlane/metadata/android/iw-IL/changelogs/18.1.3.txt ================================================ - פישוט השליטה בחיישן הקרבה. ================================================ FILE: fastlane/metadata/android/iw-IL/changelogs/18.2.0.txt ================================================ - הגדרת מקודדי השמע של החשבון הופרדה למסך חדש. ================================================ FILE: fastlane/metadata/android/iw-IL/changelogs/18.2.1.txt ================================================ - תוקנו 2 קריסות הקשורות לצ'אט. - שיפורי קוד קטנים. ================================================ FILE: fastlane/metadata/android/iw-IL/changelogs/18.3.0.txt ================================================ - שיפורי דיאלוגים. ================================================ FILE: fastlane/metadata/android/iw-IL/changelogs/19.0.0.txt ================================================ - נוספה אפשרות medianat בחשבון TURN. - נוספה אפשרות להגדיר שם משתמש וסיסמה לשרת STUN/TURN. - מחרוזות הקשורות ל-STUN/TURN בחלק מהשפות עדיין לא עודכנו. - שיפורים נוספים בתיבות דו־שיח. ================================================ FILE: fastlane/metadata/android/iw-IL/changelogs/19.1.0.txt ================================================ - שיפורים בתיבת הדו־שיח של ההתראה. ================================================ FILE: fastlane/metadata/android/iw-IL/changelogs/20.0.0.txt ================================================ - שיפורים בביטול הד אקוסטי. - הגדרות השמע הועברו מתפריט ההגדרות לפעילות חדשה. - נוספה הגדרת AEC Extended Filter. ================================================ FILE: fastlane/metadata/android/iw-IL/changelogs/20.0.1.txt ================================================ - תוקנה חזרה לפעילות השמע לאחר השהיה. - תוקנה הוספת מודולי שמע בהגדרות שמע. ================================================ FILE: fastlane/metadata/android/iw-IL/changelogs/20.0.2.txt ================================================ - תיקונים ושיפורים הקשורים ללחצן לוח המקשים. ================================================ FILE: fastlane/metadata/android/iw-IL/changelogs/20.1.0.txt ================================================ - נוספו מחרוזות בשפה הרוסית (RU) שתרמו משתמשים. - תיקון במקור (Upstream) לבעיה שעלולה הייתה לגרום לקריסה. ================================================ FILE: fastlane/metadata/android/iw-IL/changelogs/21.0.0.txt ================================================ - נוספה תמיכה ראשונית בהעברת שיחות שמקורן ב־Baresip. - נוספה שפה חדשה פורטוגזית מתוך Weblate. ================================================ FILE: fastlane/metadata/android/iw-IL/changelogs/21.1.0.txt ================================================ - נוספה אפשרות לרישום מחדש של חשבון נבחר באמצעות מחוות החלקה כלפי מטה. - מיקוד אוטומטי והצגת המקלדת הווירטואלית כאשר מתבקשת סיסמה או יעד להעברה. - תיקונים שונים הקשורים למחרוזות. ================================================ FILE: fastlane/metadata/android/iw-IL/changelogs/21.2.0.txt ================================================ - נוסף מקודד הדיבור AMR-WB (Adaptive Multi-Rate Wideband). - נוסף סעיף רישיונות לטקסט "אודות". - נוספו תרגומים חדשים לפורטוגזית (ברזיל). ================================================ FILE: fastlane/metadata/android/iw-IL/changelogs/22.0.0.txt ================================================ - נוספה תמיכת WebSocket עבור SIP (RFC 7118). ================================================ FILE: fastlane/metadata/android/iw-IL/changelogs/22.1.0.txt ================================================ - תרגומים חדשים בפורטוגזית (ברזיל) וברוסית. - עדכון האייקונים לאחר חזרה לאפליקציה. ================================================ FILE: fastlane/metadata/android/iw-IL/changelogs/23.0.0.txt ================================================ - נוספה אפשרות להסתיר/להציג סיסמאות אימות בחשבון ו־STUN/TURN - העדפת ממשק VPN (אם קיים) בבחירה מתוך ממשקי הרשת הזמינים - נוספה הערה בטקסט "אודות" לגבי סדר העדיפויות של הממשקים - שיפור הגדרת Media NAT Traversal בחשבון - שימוש בשרת STUN של Google כברירת מחדל גם כאשר פרוטוקול Media NAT Traversal הוא ICE - הגדלת האורך המקסימלי של שמות משתמש אימות החשבון ו־STUN/TURN ושם התצוגה ל־64 תווים - נוספו/עודכנו תרגומים בשפות צרפתית (FR), נורווגית (NO) ופינית (FI) ================================================ FILE: fastlane/metadata/android/iw-IL/changelogs/23.1.0.txt ================================================ - נוסף קודק אודיו G.729. - נוספה הגדרה להפעלת/כיבוי מעקב אחרי הודעות SIP ל־logcat. - בשרתי STUN/TURN סכמות URI של 'stuns' ו־'turns' אינן נתמכות כרגע. - נוספו תרגומים חדשים בפורטוגזית (ברזיל). ================================================ FILE: fastlane/metadata/android/iw-IL/changelogs/23.2.0.txt ================================================ - זיהוי משופר של שינויים בחיבור VPN * תרגומים חדשים בפורטוגזית (ברזיל). ================================================ FILE: fastlane/metadata/android/iw-IL/changelogs/24.0.0.txt ================================================ - השתמש בחוצץ ריצוד אדפטיבי ================================================ FILE: fastlane/metadata/android/iw-IL/changelogs/24.1.0.txt ================================================ - שיפור התנהגות מקשי הגברת/הנמכת עוצמת הקול - שיפור התנהגות הרמקול - תרגומים חדשים לרוסית (RU) ================================================ FILE: fastlane/metadata/android/iw-IL/changelogs/24.2.0.txt ================================================ - הצגת מקלדת DTMF אוטומטית כאשר השיחה מחוברת - מניעת קריסה שנגרמת מלחיצה על מקשי עוצמת קול כאשר אין שיחה פעילה ================================================ FILE: fastlane/metadata/android/iw-IL/changelogs/24.3.0.txt ================================================ - הצג לוח המקשים באופן אוטומטי רק כאשר כיוון המכשיר הוא לאורך - אל תשמיע צליל שיחה ממתינה כאשר נכנסת שיחה שנייה - שיפור טקסט "אודות" ================================================ FILE: fastlane/metadata/android/iw-IL/changelogs/24.4.0.txt ================================================ - אל תגדיר כברירת מחדל את שם המשתמש/סיסמה של שרת STUN/TURN לפי שם המשתמש/סיסמה של האימות ================================================ FILE: fastlane/metadata/android/iw-IL/changelogs/25.0.0.txt ================================================ - שיפור התראת שיחה נכנסת. - הוספת התראת שיחה שלא נענתה. ================================================ FILE: fastlane/metadata/android/iw-IL/changelogs/26.0.0.txt ================================================ - נוספה ערכת נושא כהה והגדרות ערכת נושא כהה. - שיפורים ברשימת הבחירה של חשבונות (Account spinner). ================================================ FILE: fastlane/metadata/android/iw-IL/changelogs/26.0.1.txt ================================================ - תוקנה הפעלת מצב כהה בעת הפעלת אפליקציית Baresip * תרגומים חדשים בפורטוגזית (ברזיל) ================================================ FILE: fastlane/metadata/android/iw-IL/changelogs/26.1.0.txt ================================================ - אפליקציות אחרות יכולות כעת לזהות את Baresip כאפליקציית טלפון עבור sip: ו־tel: מסוג URI - שיפור שמירה/שחזור של טקסט URI של שיחה כאשר הפעילות הראשית במצב הפסקה - נוסף תרגום ליפנית - שיפור בצבעים ================================================ FILE: fastlane/metadata/android/iw-IL/changelogs/26.1.1.txt ================================================ - תוקן הטיפול בפעולת שיחה כאשר אפליקציית Baresip אינה פעילה. ================================================ FILE: fastlane/metadata/android/iw-IL/changelogs/26.1.2.txt ================================================ - נסה לזהות כיוון סיבוב של תמונות אווטאר אנשי הקשר ================================================ FILE: fastlane/metadata/android/iw-IL/changelogs/27.0.0.txt ================================================ - נוספה אפשרות לייצא את אנשי הקשר של baresip לאנשי הקשר של אנדרואיד ================================================ FILE: fastlane/metadata/android/iw-IL/changelogs/27.0.1.txt ================================================ - תיקון קריסה בהפעלת baresip בפעם הראשונה ================================================ FILE: fastlane/metadata/android/iw-IL/changelogs/28.0.0.txt ================================================ - נוספה אפשרות להגדרה אוטומטית חלקית של חשבון חדש דרך דף אינטרנט (ראו https://github.com/juha-h/baresip-studio/wiki/Automatic-Account-Configuration for details) ================================================ FILE: fastlane/metadata/android/iw-IL/changelogs/28.1.0.txt ================================================ - שיפור במציאת איש קשר שתואם ל־SIP URI - שיפורי סרגל כלים ותפריטים ================================================ FILE: fastlane/metadata/android/iw-IL/changelogs/28.1.1.txt ================================================ - תוקנה צביעת הגדרות מודולי שמע - תוקנה קריסה הקשורה לרשימת השיחות ונראות ================================================ FILE: fastlane/metadata/android/iw-IL/changelogs/28.2.0.txt ================================================ - נוספה הגדרת 'ודא תעודות שרת' ================================================ FILE: fastlane/metadata/android/iw-IL/changelogs/29.0.0.txt ================================================ - הפעלת מחוות החלקה ימינה/שמאלה להחלפה בין חשבונות - הוספת הגדרת חשבון של מצב DTMF - עדכוני תרגום מ־Weblate (צרפתית) ================================================ FILE: fastlane/metadata/android/iw-IL/changelogs/29.1.0.txt ================================================ - תרגומים מ־Weblate שלפורטוגזית (ברזיל) - תוקן F-Droid nb-NO locale tag - מניעת התראות מסוג "duplicate finish request" ================================================ FILE: fastlane/metadata/android/iw-IL/changelogs/29.2.0.txt ================================================ - תיקון תקלות הקשורות לצ'אט, היסטוריית שיחות ואנשי קשר - תוקנו תקלות בהשלמת URI ושופרו הבדיקות הקשורות ל־URI. - תרגומים מ־Weblate (צרפתית, פורטוגזית (ברזיל)) ================================================ FILE: fastlane/metadata/android/iw-IL/changelogs/29.2.1.txt ================================================ - הימנעות מקריסה אפשרית הקשורה לאנשי קשר בהפעלת baresip ================================================ FILE: fastlane/metadata/android/iw-IL/changelogs/3.2.0.txt ================================================ - נוסף לחצן בלוח חיוג לבחירה בין מספר טלפון למקלדת טקסט. - תוקנה השלמה אוטומטית של הנמען בהתבסס על אנשי קשר. ================================================ FILE: fastlane/metadata/android/iw-IL/changelogs/3.2.2.txt ================================================ - נוסף טיפול באירוע כשל בהעברת שיחה. - מניעת קריסה בעת הפעלה או הפעלה מחדש של המכשיר. - מניעת קריסה כאשר חוזרים לראשונה מחשבונות לפעילות הראשית. - שימוש בסוג קלט "textEmailAddress" עבור URI של חשבונות. - שינוי חשיבות ערוץ ההתראות המוגדר כברירת מחדל לנמוך. ================================================ FILE: fastlane/metadata/android/iw-IL/changelogs/30.0.0.txt ================================================ - שודרגה גרסת ה־SDK המיועדת ל־API level 30 - באנדרואיד גרסאות 10 ומעלה, אפשר למשתמש לבחור קובץ בגיבוי, שחזור או פעולות הקשורות לתעודת TLS ================================================ FILE: fastlane/metadata/android/iw-IL/changelogs/30.1.0.txt ================================================ - שיפורים בתיבת הדו־שיח של הסיסמה ובהזנת סיסמת החשבון. ================================================ FILE: fastlane/metadata/android/iw-IL/changelogs/30.2.0.txt ================================================ - נוספה תמיכה בקודק AMR במצב Bandwidth Efficient - אפשרות להשתמש בתווים מנותבים (escaped) בחלק המשתמש של URI ב־SIP ובשם המשתמש לאימות - שימוש ב־coroutine במקום ב־async task לקבלת הגדרות חשבון מהרשת - באנדרואיד 10 ומעלה, שימוש בבורר (picker) לבחירת תעודת TLS וקבצי TLS CA - בקשת אישור לאיפוס ההגדרות לברירת המחדל של היצרן - תרגומים חדשים בפורטוגזית ובפורטוגזית (ברזיל) ================================================ FILE: fastlane/metadata/android/iw-IL/changelogs/30.3.0.txt ================================================ - השמע צליל מענה אוטומטי כאשר השיחה נענית אוטומטית - הסרת קודק iLBC (הוסר מקוד המקור) - הוסרו קובצי קול שאינם בשימוש ================================================ FILE: fastlane/metadata/android/iw-IL/changelogs/37.4.0.txt ================================================ - תרגומים חדשים של פורטוגזית וגרמנית ================================================ FILE: fastlane/metadata/android/iw-IL/changelogs/53.2.2.txt ================================================ - תוקנו מספר בעיות הקשורות למיקוד שמע/ניתוב שמע/בקרת עוצמת קול ================================================ FILE: fastlane/metadata/android/iw-IL/changelogs/8.5.0.txt ================================================ - שימוש ראשוני בחיישן הקרבה לכיבוי מסך המגע במהלך שיחה. ================================================ FILE: fastlane/metadata/android/iw-IL/full_description.txt ================================================ זוהי אפליקציית סוכן משתמש SIP לאנדרואיד המבוססת על baresip. נכון לעכשיו, אפליקציית baresip תומכת בשיחות קוליות ושיחות ועידה, הודעות טקסט, תא קולי עם חיווי להמתנת הודעות (Message Waiting Indication), וכן בהעברות שיחה עיוורות ומלוות. הקול יכול להיות מקודד באמצעות הקודקים Opus, ‏AMR, ‏Codec2, ‏G.729, ‏G.722, ‏G.722.1, ‏G.726 או ‏PCMU/PCMA. האבטחה מושגת באמצעות תעבורת איתות SIP מעל TLS או WSS, והצפנת מדיה באמצעות ZRTP או (DTLS) SRTP. פיתוח אפליקציית baresip מונע מהצורך בסוכן משתמש VoIP מבוסס SIP, מאובטח ובקוד פתוח לאנדרואיד, שאינו תלוי בשירותי התראות דחיפה קנייניים של צד שלישי. אם אתם זקוקים לשיחות וידאו, תוכלו להתקין במקום אפליקציה זו את אפליקציית האחות שלה, baresip+. קוד המקור זמין ב־GitHub, שם ניתן גם לדווח על תקלות (issues). ================================================ FILE: fastlane/metadata/android/iw-IL/short_description.txt ================================================ אפליקציית סוכן משתמש VoIP לאנדרואיד המבוססת על ספריית ה־SIP של baresip ================================================ FILE: fastlane/metadata/android/iw-IL/title.txt ================================================ baresip ================================================ FILE: fastlane/metadata/android/nb-NO/changelogs/10.2.0.txt ================================================ - Tillagt støtte for G.726-kodek. - Forbedret prioritetsrang for forvalgt kodek. ================================================ FILE: fastlane/metadata/android/nb-NO/changelogs/12.0.0.txt ================================================ - Første implementasjon av lyd gjennom Blåtannshodesett. ================================================ FILE: fastlane/metadata/android/nb-NO/changelogs/12.2.1.txt ================================================ - Forbedret og fikset håndtering av dynamiske nettverksendringer. ================================================ FILE: fastlane/metadata/android/nb-NO/changelogs/3.2.0.txt ================================================ - Tillagt nummerskiveknapp for valg mellom telefonnummer og tekst for skjermtastatur. - Fikset autofullføring av ringer basert på kontakter. ================================================ FILE: fastlane/metadata/android/nb-NO/changelogs/3.2.2.txt ================================================ - Håntering av anropsvideresending som mislykkes lagt til. - Unngått krasj ved (om)start av enhet. - Unngått krasj ved første gangs navigasjon fra kontoer til hovedaktivitet. - Brukt "textEmailAddress"-inndatatype for konto-URI-er. - Endret vektlegging av forvalgt merknadskanal til "lav". ================================================ FILE: fastlane/metadata/android/nb-NO/changelogs/36.0.0.txt ================================================ - Flyttet til API-nivå 31 (Android 12) - Forbedret oppdagelse av tilgjengelig nettverk - Forbedrede tilgangsforespørsler - Fjernet kontoens «Foretrekk IPv6-media»-innstilling - Ny Portugisisk (Brasil)-oversettelse ================================================ FILE: fastlane/metadata/android/nb-NO/changelogs/36.1.2.txt ================================================ - Forhindret dragnigsrelatert krasj ================================================ FILE: fastlane/metadata/android/nb-NO/changelogs/4.0.0.txt ================================================ - Forbedret implementasjon av kontakter, melding og anropshistorikk. - Unngå krasj når meldinger ankommer før kontoer har blitt lagt til. ================================================ FILE: fastlane/metadata/android/nb-NO/changelogs/4.1.0.txt ================================================ - All urelatert data fra brukergrensesnitt beholdes nå i baresip-tjeneste, noe som tillater riktig gjenoppretting i fall Android dreper andre baresp-aktiviteter. - Mange andre forbedringer og feilrettinger. ================================================ FILE: fastlane/metadata/android/nb-NO/changelogs/4.1.1.txt ================================================ - Fikset kritisk feil i sjekking av lydopptakstilgang som forhindret ny baresip-installasjon fra å starte. - Små forbedringer. ================================================ FILE: fastlane/metadata/android/nb-NO/changelogs/4.1.2.txt ================================================ - Fikset krasj når tom kontospinner klikkes. ================================================ FILE: fastlane/metadata/android/nb-NO/changelogs/5.0.0.txt ================================================ - Erstattet tekstmodus-EditConfigActivity med grafisk oppsettsaktivitet. - Nåværende oppsettselementer er autostart, DNS-tjenere, Opus-bitrate, og ICE-Lite. - Tillagt ICE og STUN-tjenerkontovalg. - Fikset feil i kontosletting. - Usendte meldinger går ikke tapt når sludreaktivitet forstyrres. ================================================ FILE: fastlane/metadata/android/nb-NO/changelogs/5.1.0.txt ================================================ - Tillagt mulighet for eksport/import av kontakter til/fra "Nedlastinger"-mappe. Hvis du ønsker å bruke denne funksjonen, innvilg baresip lagringstilgang i Innstillinger → Apper → baresip → Tilganger. - Fikset treg avslutning av baresip når nettverkstilkobling mangler. ================================================ FILE: fastlane/metadata/android/nb-NO/changelogs/5.2.0.txt ================================================ - Gi bruker merknad om mislykket registrering. - Tillagt samtaleinfoknapp for aktiv samtale. - Ikke krev "sip:"-del i utgående mellomtjener-URI. - Tillat domeneetikett å starte med et siffer. ================================================ FILE: fastlane/metadata/android/nb-NO/changelogs/5.2.1.txt ================================================ - Mindre strikt sjekking av visningsnavn og identitetsbekreftelses-brukernavn - Tillagt versjonsnavn i Om-side ================================================ FILE: fastlane/metadata/android/nb-NO/changelogs/5.3.0.txt ================================================ - Tillagt støtte for merknadsoppsprett som forteller hvorfor en samtale ble lukket. - Fikset visning av ringer-URI hvis det inneholder portnummer eller parameter. ================================================ FILE: fastlane/metadata/android/nb-NO/changelogs/5.3.1.txt ================================================ - Mindre strikt sjekking av brukerdel i kontoens SIP-URI. - Tillagt støtte for ARMv8-A-arkitektur. ================================================ FILE: fastlane/metadata/android/nb-NO/changelogs/5.4.0.txt ================================================ - Tillagt SRTP-MANDF-mediakrypteringsprotokoll. - Sikkerhetsknappefarge endres fra rød til gul også når samtalen er sikret av en SRTP*-mediakrypteringsprotokoll. - Fikset innstilling av kontomediakryptering til DTLS-SRTPF. ================================================ FILE: fastlane/metadata/android/nb-NO/changelogs/5.4.1.txt ================================================ - Fjernet ubrukt baresip-moduler fra oppsett og bilbiotek. - Nå lar baresip-tjenesten baresip-aktivitet dø på egenhånd etter å ha mottatt drapsforespørsel fra den. ================================================ FILE: fastlane/metadata/android/nb-NO/changelogs/5.4.2.txt ================================================ - Fikset sjeldent krasj som kan inntreffe når første konto opprettes og melding eller anrop kommer inn før brukeren har gått tilbake til hovedaktivitet. ================================================ FILE: fastlane/metadata/android/nb-NO/changelogs/6.0.0.txt ================================================ - Flere brukergrensesnittforbedringer (for det mestre sludringsrelatert). ================================================ FILE: fastlane/metadata/android/nb-NO/changelogs/6.0.1.txt ================================================ - Gjenoppta baresip-program til aktiviteten som ble stoppet. - Tillagt eksempelkontakt når baresip startes for første gang. - Fikset visning av ny melding i sludre- og meldingslisten. ================================================ FILE: fastlane/metadata/android/nb-NO/changelogs/6.0.2.txt ================================================ - Fikset krasj når STUN-tjener er definert uten portnummer. ================================================ FILE: fastlane/metadata/android/nb-NO/changelogs/6.1.0.txt ================================================ - Gå til sludrevindu når meldingsmerknad klikkes. - Fikset feil i anropsoverføring. ================================================ FILE: fastlane/metadata/android/nb-NO/changelogs/6.2.0.txt ================================================ - Oppgadert mål for Android API-nivå til 28 (Android 9). - Flyttet noe funksjonalitet som ikke har med grensesnitt å gjøre fra hovedaktivitet til baresip-tjeneste. - Oppdatert baresip-oppstarter og statusfeltbilder. - Avregistrert UA når konto slettes. ================================================ FILE: fastlane/metadata/android/nb-NO/changelogs/6.2.1.txt ================================================ - ConnectivityManager NetworkCallbacks brukt til å fastsette når nettverks- tilgang er tilgjengelig eller tapes. - Bruk av Android 8.1+måten å håndtere skjerm. - Lydstyrkekontroller satt til å håndtere nåværende lydstrøm (kan fuske). - Fikset mulig krasj når ødelagt baresip-program startes igjen. - Notis vist når kontoen som skal opprettes allerede finnes. - Tillagt notis i "Om"-tekst om medialydstyrke. ================================================ FILE: fastlane/metadata/android/nb-NO/changelogs/6.3.0.txt ================================================ - Tillagt avlusingsoppsettsvalg for å skru adb-logcat-avlusing av og på. - Tillagt mulighet for å eksportere og importere kontoer til/fra fil i nedlastingsmappe. ================================================ FILE: fastlane/metadata/android/nb-NO/changelogs/6.3.1.txt ================================================ - Fikset og forbedret implementasjon av DTMF-tonesignalering. - Avskrudd redigering av innkommende/utgående anrops-URI når samtale pågår. - Slettet brukeragent når konto slettes. - Fjernet ubrukt DREP_BAKGRUNNSPROSESSER-tilgang. ================================================ FILE: fastlane/metadata/android/nb-NO/changelogs/6.4.0.txt ================================================ - Tillagt mulighet for å sette konto som forvalg. - Vis rødt meldingsikon når det finnes uleste meldinger. - Spør PIN om å slippe nøkkelvakt når oppringer-URI klikkes. - Fikset anrop, melding og anropsvideresendingshandlinger når igangsatt fra merknader. - Fikset forsendelse av melding fra kontakter eller samtalehistorikk. - Bruk av SingleTask istedenfor SingleTop som oppstartsmodus for hovedaktivitet. ================================================ FILE: fastlane/metadata/android/nb-NO/changelogs/7.0.0.txt ================================================ - Tillagt støtte for IPv6 (ikke testet som følge av mangel på IPv6-nettverk). - Tillagt "Foretrekk IPv6"-oppsettsvalg. - Tillagt "Tilbakestill til fabrikkforvalg"-oppsettsvalg. - Ved avslutning, forsikre at baresip-oppgaven stoppes. ================================================ FILE: fastlane/metadata/android/nb-NO/changelogs/7.1.0.txt ================================================ - "Forvalgt samtalelydstyrke" lagt til som oppsettsvalg. ================================================ FILE: fastlane/metadata/android/nb-NO/changelogs/7.1.1.txt ================================================ - Oppdater kontospinner også når hovedaktivitet gjenopptas uten handling. ================================================ FILE: fastlane/metadata/android/nb-NO/changelogs/7.1.2.txt ================================================ - Forsøksvis oppdatering av kontospinnervisning når kontospinneren oppdateres. ================================================ FILE: fastlane/metadata/android/nb-NO/changelogs/7.2.0.txt ================================================ - Tillagt variabel, lytteadresse som tillater spesifisering av IP-adresse (eller alle adresser) og en port som baresip lytter til for SIP-forespørsler. ================================================ FILE: fastlane/metadata/android/nb-NO/changelogs/7.3.0.txt ================================================ - Mulighet for å velge STUN, ICE eller ingen NAT-protokoll. - Kontostatus satt til gul umiddelbart når kontoregistrering er av. ================================================ FILE: fastlane/metadata/android/nb-NO/changelogs/7.4.0.txt ================================================ - Varslinger nå i henhold til designretningslinjer for materielt design. - Forbedret språkstrengshåndtering. - Finsk lagt til som språkeksempel. ================================================ FILE: fastlane/metadata/android/nb-NO/changelogs/8.0.0.txt ================================================ - Støtte for G.722.1-lydkodek lagt til. - Støtte for WebRTC-basert lydekkokansellering (ikke for Opus-kodek). - Mindre samtalemerknadsinnstillinger. ================================================ FILE: fastlane/metadata/android/nb-NO/changelogs/8.0.1.txt ================================================ - Fikset gjenoppretting av lydstyrke til opprinnelig nivå hvis forvalg lydstyrke brukes. ================================================ FILE: fastlane/metadata/android/nb-NO/changelogs/8.0.2.txt ================================================ - Fikset opprettelse av ny konto. ================================================ FILE: fastlane/metadata/android/nb-NO/changelogs/8.1.0.txt ================================================ - Innhenting av DNS-tjeneradresser dynamisk fra systemet hvis DNS-tjenere ikke oppgis i oppsettet. - Hvis det er kun én konto, legg til kontoens domene i kontakt-URI uten gitt vertsdel. ================================================ FILE: fastlane/metadata/android/nb-NO/changelogs/8.2.0.txt ================================================ - Forbedret innhold og utseende for "Om"-siden. - Alltid vis aktivt anrop (hvis noe), når programmet startes på ny. - Endret plassering for varselsdialog for OK-knapp. ================================================ FILE: fastlane/metadata/android/nb-NO/changelogs/8.3.0.txt ================================================ - Internett lav bitratekodek (iLBC) lagt til. - Forbedret kontokodekutvelgelse. ================================================ FILE: fastlane/metadata/android/nb-NO/changelogs/8.3.1.txt ================================================ - Ikke sett lyd til kommunikasjonsmodus når anrop opprettes, fordi på noen enheter legger det til 3-4 sekunders forsinkelse for avspilling av lyd. - Akustisk ekkokansellering nå justerbar. - Skru på samtaleinfoknapp når den blir synlig. - Bruk forvalgt enhetsdrakt ved visning av spinnere. ================================================ FILE: fastlane/metadata/android/nb-NO/changelogs/8.3.2.txt ================================================ - Hvis kontoen åpnes fra AoR-spinneren, gå direkte til hovedaktivitet. - Bruk HtmlCompat til konvertering av Om-HTML til tekst (burde virke på alle API-nivåer). - Bruk ringetone-gjentagelsesmulighet på Android P+. ================================================ FILE: fastlane/metadata/android/nb-NO/changelogs/8.3.3.txt ================================================ - DNS-tjenere hentet dynamisk, også når baresip startes for første gang. - Mulig krasj unngått under henting av kontoens lydkodeker. ================================================ FILE: fastlane/metadata/android/nb-NO/changelogs/8.3.5.txt ================================================ - Omkrans kontakt-URI-er i < og > når kontakter lagres til fil. ================================================ FILE: fastlane/metadata/android/nb-NO/changelogs/8.3.6.txt ================================================ - Tillat endring av kontaktnavn også når det kun er forskjell på små og store bokstaver. - Ved gjenoppretting av kontakter, ikke krev at URI-er omkranses av vinkelparenteser. ================================================ FILE: fastlane/metadata/android/nb-NO/changelogs/8.4.0.txt ================================================ - Ekkokansellering virker nå for samtaler som bruker Opus-kodeket. ================================================ FILE: fastlane/metadata/android/nb-NO/changelogs/8.4.1.txt ================================================ - Forbedring av ekko-kansellering oppstrøms. - Nye oversettelser: el, nb_NO, ro. ================================================ FILE: fastlane/metadata/android/nb-NO/changelogs/8.4.2.txt ================================================ - Alltid oppdater DNS-tjenere og registrer UA-er når nettverket blir tilgjengelig. - Fikset og oppdaterte noen tekster. ================================================ FILE: fastlane/metadata/android/nb-NO/changelogs/8.4.3.txt ================================================ - Fikset visning av kontostatus, når forvalgt konto endres. ================================================ FILE: fastlane/metadata/android/nb-NO/changelogs/8.4.4.txt ================================================ - Oppstrømsfiks i implementasjon av Opus-kodek. - Fiks for kontroll av lydstyrkestrøm. - Fiks for å legge på røret automatisk ved innkommende anrop. - Mindre strengforbedringer. ================================================ FILE: fastlane/metadata/android/nb-NO/changelogs/8.5.0.txt ================================================ - Foreløpig bruk av nærhetsføler for å skru av skjer under samtale. ================================================ FILE: fastlane/metadata/android/nb-NO/changelogs/8.5.1.txt ================================================ - Flyttet håndtering av nærhetsføling fra MainActivity til BaresipService. - Fikset krasj ved anrop eller innkommende melding når MainActivity ikke kjører. ================================================ FILE: fastlane/metadata/android/nb-NO/changelogs/8.5.2.txt ================================================ - Fikset sjekking av DNS-tjeneroppsett. ================================================ FILE: fastlane/metadata/android/nb-NO/changelogs/8.6.0.txt ================================================ - Tillagt støtte for SIP-registeringstjenester som ikke håndterer gjenbruk av engangsord rett. ================================================ FILE: fastlane/metadata/android/nb-NO/changelogs/9.0.0.txt ================================================ - Tillagt mulighet for å sette opp baresip med TLS- og CA-sertifikater. ================================================ FILE: fastlane/metadata/android/nb-NO/changelogs/9.1.0.txt ================================================ - Automatisk avslag av innkommende samtale hvis telefontilstand ikke er ledig. - Forstum ringetone for innkommende samtale hvis enheten er i "Ikke forstyrr"-modus. ================================================ FILE: fastlane/metadata/android/nb-NO/changelogs/9.2.0.txt ================================================ - Tillagt støtte for Opus framtidsfeilkorrigering (FEC) via Opus sin oppsettsvariabel for forventet pakketap. - Forsøksvis oppdatering av dynamisk DNS-info hvis registrering mislykkes som følge av "ugyldig argument"-feil. - Bruk av oversettbar streng i alle oppsettsrelaterte varselsmeldinger. ================================================ FILE: fastlane/metadata/android/nb-NO/changelogs/9.3.0.txt ================================================ - Svarsmodus (manuell eller automatisk) -kontooppsettsvalg lagt til. - Strenger for norsk bokmål lagt til, sammen med endringslogger fra Weblate. ================================================ FILE: fastlane/metadata/android/nb-NO/full_description.txt ================================================ baresip VoIP-brukeragent støtter nå SIP-stemmeanrop, og meldinger, telefonsvar-meldingsventingsindikasjon (MWI), og innkommende androps- videresendinger via REFERENT-metode. Stemme kan kodes via PCMU/PCMA, Opus, G.722.1, ok iLBC -kodek. Sikkerhets oppnås via TLS-signaleringstransport, ZRTP og (DTLS) SRTP mediainnkapsling. Utvikling av baresip-programmet er motivert av behovet for en sikker og fri SIP-brukeragent for Android, som ikke avhenger av proprietære dyttingsmerknadstjenester fra tredjeparter. Kildekode tilgjengelig fra https://github.com/juha-h/baresip-studio, der problemer kan innrapporteres. ================================================ FILE: fastlane/metadata/android/nb-NO/short_description.txt ================================================ VoIP-brukeragentprogram for Android, basert på SIP-biblioteket baresip ================================================ FILE: fastlane/metadata/android/nb-NO/title.txt ================================================ baresip ================================================ FILE: fastlane/metadata/android/pl/changelogs/15.1.1.txt ================================================ - Drobne poprawki i ulepszenia łańcucha. ================================================ FILE: fastlane/metadata/android/ro/full_description.txt ================================================ Clientul VoIP baresip suportă apeluri vocale și mesaje SIP, notificări căsuță vocală în așteptare (Message Waiting Indication - MWI) și transferuri apeluri primite prin metoda REFER. Vocea poate fi transmisă utilizând codecurile PCMU/PCMA, opus, G.722.1 și iLBC. Securizarea conexiunii este realizată folosind transportul TLS precum și încapsularea media ZRTP și (DTLS) SRTP. Dezvoltarea aplicației baresip este motivată de nevoia de a avea un client Android SIP securizat, cu sursă deschisă ce nu depinde de servicii terțe proprietare de notificare. Codul sursă este disponibil la https://github.com/juha-h/baresip-studio unde se și pot raporta eventualele probleme. ================================================ FILE: fastlane/metadata/android/ro/short_description.txt ================================================ Client VoIP pentru Android bazat pe biblioteca SIP baresip ================================================ FILE: fastlane/metadata/android/ro/title.txt ================================================ baresip ================================================ FILE: fastlane/metadata/android/ru-RU/changelogs/10.0.0.txt ================================================ - Возобновлять baresip на приостановленном действии вместо основного. ================================================ FILE: fastlane/metadata/android/ru-RU/changelogs/10.1.0.txt ================================================ - Добавлена поддержка кодека G722. - Исправлено сохранение учётных записей. ================================================ FILE: fastlane/metadata/android/ru-RU/changelogs/10.2.0.txt ================================================ - Добавлена поддержка кодека G.726. - Улучшен порядок кодеков по умолчанию. ================================================ FILE: fastlane/metadata/android/ru-RU/changelogs/10.3.0.txt ================================================ - Добавлено меню История звонков, позволяющее удалять, отключать и включать историю вызовов учётной записи. - Добавлен испанский перевод (спасибо, Javier Falbo). - Автозапуск baresip по умолчанию в новых установках. ================================================ FILE: fastlane/metadata/android/ru-RU/changelogs/10.4.0.txt ================================================ - Разрешить удаление истории чатов учётной записи через меню действий с ними. - Запрашивать подтверждение перед удалением истории чатов и звонков. - Улучшено обновление значков голосовой почты, чатов и звонков. - Открытие раздела учётных записей при запуске baresip без учётных записей. - Кнопки приёма и отклонения вызова поменялись местами. - Не позволять завершать исходящий вызов до начала попытки его выполнения. ================================================ FILE: fastlane/metadata/android/ru-RU/changelogs/11.0.0.txt ================================================ - Экспорт и импорт учётных записей и контактов заменйнён с резервным копированием и восстановлением всех данных приложения. - Улучшена безопасность за счёт запрета Android-резервирования приложения и установки приложения на внешнее хранилище. - При удалении учётной записи, удалять также её историю звонков и чатов. - Исправлено удаление сообщений и чатов. - Незначительные улучшения пользовательского интерфейса и текстов. ================================================ FILE: fastlane/metadata/android/ru-RU/changelogs/11.1.0.txt ================================================ - Добавлена переменная конфигурации «Bind Address», которая позволяет выбирать, какой сетевой интерфейс или IP-адрес использует baresip. - Всегда обновлять значок статуса учётных записей при возобновлении основной деятельности. - Устранён сбой при возобновлении уничтоженного ранее основного действия. - Правильно отображать активный входящий вызов при возобновлении основной деятельности. - Обновлены испанские тексты. ================================================ FILE: fastlane/metadata/android/ru-RU/changelogs/11.2.0.txt ================================================ - Добавлена поддержка узкополосного кодека AMR. - Добавлены болгарские тексты. - Пункт Конфигурация главного меню переименован в Настройки. ================================================ FILE: fastlane/metadata/android/ru-RU/changelogs/11.3.0.txt ================================================ - В Настройки добавлен выбор, какие аудиомодули загружать. Для использования в учётных записях доступны аудиокодеки загруженных модулей. - Незначительные улучшения кода. ================================================ FILE: fastlane/metadata/android/ru-RU/changelogs/12.0.0.txt ================================================ - Начальная реализация передачи звука через Bluetooth-гарнитуру. ================================================ FILE: fastlane/metadata/android/ru-RU/changelogs/12.1.0.txt ================================================ - Разрешить номер порта в Адресе записи учётки (AoR). - Исправлена проверка существования учётной записи. - Обновлены норвежские и болгарские тексты. ================================================ FILE: fastlane/metadata/android/ru-RU/changelogs/12.2.0.txt ================================================ - Новые функции API 'net_set_address' и 'net_set_af' использованы для реализации предпочтения IPv6. - Удалена настройка привязки к адресу (не работала должным образом). ================================================ FILE: fastlane/metadata/android/ru-RU/changelogs/12.2.1.txt ================================================ - Улучшена и исправлена обработка динамичных сетевых изменений. ================================================ FILE: fastlane/metadata/android/ru-RU/changelogs/15.1.1.txt ================================================ - Незначительные исправления и улучшения текстов. ================================================ FILE: fastlane/metadata/android/ru-RU/changelogs/17.2.1.txt ================================================ - Исправлена ошибка при сохранении учётных записей в файловую систему. ================================================ FILE: fastlane/metadata/android/ru-RU/changelogs/18.0.1.txt ================================================ - Увеличение максимального количества аудиокодеков учётной записи с 8 до 16. ================================================ FILE: fastlane/metadata/android/ru-RU/changelogs/18.1.1.txt ================================================ - Устранён сбой при звонке из Истории вызовов. ================================================ FILE: fastlane/metadata/android/ru-RU/changelogs/18.1.3.txt ================================================ - Упрощённое управление датчиком приближения. ================================================ FILE: fastlane/metadata/android/ru-RU/changelogs/18.2.0.txt ================================================ - Настройка аудиокодеков учётной записи выделена в новый экран. ================================================ FILE: fastlane/metadata/android/ru-RU/changelogs/18.2.1.txt ================================================ — Устранены два сбоя, связанных с чатом. — Незначительные улучшения кода. ================================================ FILE: fastlane/metadata/android/ru-RU/changelogs/18.3.0.txt ================================================ - Улучшения диалогов. ================================================ FILE: fastlane/metadata/android/ru-RU/changelogs/19.1.0.txt ================================================ - Улучшения диалогов-предупреждений. ================================================ FILE: fastlane/metadata/android/ru-RU/changelogs/20.0.2.txt ================================================ - Исправления и улучшения, связанные с кнопками панели набора номера. ================================================ FILE: fastlane/metadata/android/ru-RU/changelogs/20.1.0.txt ================================================ - Обновлён русский перевод. - Устранён возможный сбой. ================================================ FILE: fastlane/metadata/android/ru-RU/changelogs/22.0.0.txt ================================================ - Добавлен Websocket-транспорт для SIP (RFC 7118). ================================================ FILE: fastlane/metadata/android/ru-RU/changelogs/22.1.0.txt ================================================ - Новые переводы на бразильский португальский и русский. - Обновлять значки после возобновления работы приложения. ================================================ FILE: fastlane/metadata/android/ru-RU/changelogs/23.2.0.txt ================================================ - Улучшенное обнаружение изменений VPN-подключения - Новые переводы на бразильский португальский ================================================ FILE: fastlane/metadata/android/ru-RU/changelogs/24.0.0.txt ================================================ - Использовать адаптивный джиттер-буфер ================================================ FILE: fastlane/metadata/android/ru-RU/changelogs/24.4.0.txt ================================================ - Не использовать по умолчанию логин/пароль сервера STUN/TURN в качестве логина/пароля для аутентификации ================================================ FILE: fastlane/metadata/android/ru-RU/changelogs/25.0.0.txt ================================================ - Улучшено уведомление о входящем вызове. - Добавлено уведомление о пропущенном вызове. ================================================ FILE: fastlane/metadata/android/ru-RU/changelogs/26.0.0.txt ================================================ - Добавлены тёмная тема и настройка тёмной темы. - Улучшен спиннер учётных записей. ================================================ FILE: fastlane/metadata/android/ru-RU/changelogs/26.1.1.txt ================================================ - Исправлена обработка вызова при незапущенном приложении baresip. ================================================ FILE: fastlane/metadata/android/ru-RU/changelogs/26.1.2.txt ================================================ - Пытаться определять повёрнутость изображений аватаров контактов ================================================ FILE: fastlane/metadata/android/ru-RU/changelogs/27.0.0.txt ================================================ - Добавлена возможность экспорта контактов baresip в контакты Android ================================================ FILE: fastlane/metadata/android/ru-RU/changelogs/27.0.1.txt ================================================ - Устранён сбой при первом запуске baresip ================================================ FILE: fastlane/metadata/android/ru-RU/changelogs/28.0.0.txt ================================================ - Добавлена возможность частичной автоконфигурации новой учётной записи с веб-страницы (подробнее см. https://github.com/juha-h/baresip-studio/wiki/Automatic-Account-Configuration) ================================================ FILE: fastlane/metadata/android/ru-RU/changelogs/28.1.1.txt ================================================ - Исправлено окрашивание настроек аудиомодулей - Устранён сбой, связанный со списком вызовов и исправлен его внешний вид ================================================ FILE: fastlane/metadata/android/ru-RU/changelogs/28.2.0.txt ================================================ - Добавлен параметр «Проверка сертификатов сервера» ================================================ FILE: fastlane/metadata/android/ru-RU/changelogs/29.2.1.txt ================================================ - Устранение возможных сбоев, связанных с контактами, при запуске baresip ================================================ FILE: fastlane/metadata/android/ru-RU/changelogs/30.0.1.txt ================================================ - Исправление выбора учётной записи для входящего вызова. ================================================ FILE: fastlane/metadata/android/ru-RU/changelogs/30.1.0.txt ================================================ - Улучшения диалогового окна ввода пароля и поля с паролем учётной записи. ================================================ FILE: fastlane/metadata/android/ru-RU/changelogs/31.2.1.txt ================================================ - Устранён сбой при выборе учётной записи ================================================ FILE: fastlane/metadata/android/ru-RU/changelogs/32.1.0.txt ================================================ - Новые переводы на испанский и бразильский португальский - Незначительные улучшения, связанные с AudioManager'ом ================================================ FILE: fastlane/metadata/android/ru-RU/changelogs/32.2.0.txt ================================================ - Улучшена проверка параметров SIP URI - Больше исправлений и улучшений, связанных с аудиоменеджером и Bluetooth ================================================ FILE: fastlane/metadata/android/ru-RU/changelogs/34.1.0.txt ================================================ - Использовать фильтр мобильного режима акустического эхоподавления WebRTC ================================================ FILE: fastlane/metadata/android/ru-RU/changelogs/36.1.2.txt ================================================ - Устранён сбой, связанный со смахиванием ================================================ FILE: fastlane/metadata/android/ru-RU/changelogs/37.0.0.txt ================================================ - Реализация перевода вызова, совместимая с Basic Transfer из RFC 5589 ================================================ FILE: fastlane/metadata/android/ru-RU/changelogs/37.0.1.txt ================================================ Исправлены настройки звука исходящего вызова ================================================ FILE: fastlane/metadata/android/ru-RU/changelogs/37.3.0.txt ================================================ - Параметр Громкость вызова по умолчанию перемещён в раздел Настройки звука - Новый язык (немецкий) ================================================ FILE: fastlane/metadata/android/ru-RU/changelogs/37.4.0.txt ================================================ - Новые переводы на португальский и немецкий ================================================ FILE: fastlane/metadata/android/ru-RU/changelogs/4.1.2.txt ================================================ - Устранён сбой при нажатии на спиннер отсутствия учётных записей. ================================================ FILE: fastlane/metadata/android/ru-RU/changelogs/42.1.0.txt ================================================ - Тема AppCompat заменена на MaterialComponents ================================================ FILE: fastlane/metadata/android/ru-RU/changelogs/44.2.0.txt ================================================ - Устранён сбой при получении сообщения на устройствах ARMv7 - Новые переводы на бразильский португальский ================================================ FILE: fastlane/metadata/android/ru-RU/changelogs/45.1.0.txt ================================================ - Использовать протокол установления ключа DTLS на основе криптографии на эллиптических кривых - Новые шведские переводы ================================================ FILE: fastlane/metadata/android/ru-RU/changelogs/45.1.2.txt ================================================ Исправления и улучшения, связанные с мелодией звонка и уведомлений ================================================ FILE: fastlane/metadata/android/ru-RU/changelogs/46.0.1.txt ================================================ - Не использовать кэш DNS, который может вызвать длительные задержки при разрешении адресов ================================================ FILE: fastlane/metadata/android/ru-RU/changelogs/49.1.0.txt ================================================ - Показывать уведомление с причиной прекращения входящего вызова библиотекой baresip ================================================ FILE: fastlane/metadata/android/ru-RU/changelogs/49.1.2.txt ================================================ - Исправлена ошибка обхода Media NAT (STUN/TURN/ICE) ================================================ FILE: fastlane/metadata/android/ru-RU/changelogs/49.1.3.txt ================================================ - Исправление согласования кодеков ================================================ FILE: fastlane/metadata/android/ru-RU/changelogs/49.4.0.txt ================================================ - Устранён сбой при сетевых изменениях - Новые переводы на бразильский португальский ================================================ FILE: fastlane/metadata/android/ru-RU/changelogs/5.3.1.txt ================================================ - Ослаблена проверка пользовательской части в SIP URI учётной записи. - Добавлена поддержка архитектуры ARMv8-A. ================================================ FILE: fastlane/metadata/android/ru-RU/changelogs/50.1.4.txt ================================================ - Больше исправлений и улучшений, связанных с устройствами связи ================================================ FILE: fastlane/metadata/android/ru-RU/changelogs/50.1.5.txt ================================================ - Исправлено обнаружение интерфейса точки доступа Wi-Fi на некоторых устройствах ================================================ FILE: fastlane/metadata/android/ru-RU/changelogs/51.2.0.txt ================================================ - Несколько исправлений и улучшений, связанных с контактами - Новые переводы на бразильский португальский ================================================ FILE: fastlane/metadata/android/ru-RU/changelogs/53.0.0.txt ================================================ - Добавлена настройка для выбора семейства IP-адресов - Добавлен монохромный значок для лаунчера - Значок и текст убраны из уведомления о статусе для Android версии 13+ - Новые переводы на испанский, русский, бразильский португальский и чешский ================================================ FILE: fastlane/metadata/android/ru-RU/changelogs/53.1.0.txt ================================================ - Улучшения значка для лаунчера - Новые переводы на русский ================================================ FILE: fastlane/metadata/android/ru-RU/changelogs/53.1.1.txt ================================================ - Улучшен значок для лаунчера - Не пытаться регистрировать новую учётную запись, если её регистрация отключена - Добавлена подсказка для Интервала регистрации - Новые переводы на русский ================================================ FILE: fastlane/metadata/android/ru-RU/changelogs/6.0.0.txt ================================================ - Несколько улучшений интерфейса (в основном связанных с чатом). ================================================ FILE: fastlane/metadata/android/ru-RU/changelogs/6.0.2.txt ================================================ - Устранён сбой при указании STUN-сервера без номера порта. ================================================ FILE: fastlane/metadata/android/ru-RU/changelogs/7.1.0.txt ================================================ - Добавлен параметр «Громкость вызова по умолчанию». ================================================ FILE: fastlane/metadata/android/ru-RU/changelogs/7.1.1.txt ================================================ - Обновлять спиннер учётных записей и при возобновлении основной активности без каких-либо действий. ================================================ FILE: fastlane/metadata/android/ru-RU/changelogs/8.0.1.txt ================================================ - Исправлено восстановление исходного уровня громкости, если используется громкость вызова по умолчанию. ================================================ FILE: fastlane/metadata/android/ru-RU/changelogs/8.0.2.txt ================================================ - Исправлено создание учётной записи. ================================================ FILE: fastlane/metadata/android/ru-RU/changelogs/8.2.0.txt ================================================ - Улучшено содержание и вид страницы О приложении. - Всегда отображать текущий активный вызов (если он есть) при перезапуске приложения. - Изменено расположение кнопки OK диалога предупреждений. ================================================ FILE: fastlane/metadata/android/ru-RU/changelogs/8.3.0.txt ================================================ - Добавлен Internet Low Bitrate Codec (iLBC). - Улучшен выбор кодека для учётной записи. ================================================ FILE: fastlane/metadata/android/ru-RU/changelogs/8.3.5.txt ================================================ - Заключать URI контактов в < и > только при сохранении контактов в файл. ================================================ FILE: fastlane/metadata/android/ru-RU/changelogs/8.4.0.txt ================================================ - Эхоподавление теперь применяется и при звонках с использованием кодека Opus. ================================================ FILE: fastlane/metadata/android/ru-RU/changelogs/8.4.1.txt ================================================ - Улучшение акустического эхоподавления. - Новые переводы: греческий, норвежский букмол, румынский. ================================================ FILE: fastlane/metadata/android/ru-RU/changelogs/8.4.3.txt ================================================ - Исправлено отображение состояния учётной записи при смене учётной записи по умолчанию. ================================================ FILE: fastlane/metadata/android/ru-RU/changelogs/8.5.0.txt ================================================ - Предварительное использование датчика приближения для отключения сенсорного экрана во время вызова. ================================================ FILE: fastlane/metadata/android/ru-RU/changelogs/8.5.2.txt ================================================ - Исправлена проверка конфигурации DNS-серверов. ================================================ FILE: fastlane/metadata/android/ru-RU/changelogs/8.6.0.txt ================================================ - Добавлена поддержка SIP-регистраторов, которые некорректно обрабатывают повторное использование параметра «nonce». ================================================ FILE: fastlane/metadata/android/ru-RU/changelogs/9.0.0.txt ================================================ - Добавлена возможность настраивать в baresip TLS-сертификат и корневые сертификаты. ================================================ FILE: fastlane/metadata/android/ru-RU/changelogs/9.1.0.txt ================================================ - Автоотклонение входящего вызова, если телефон не в режиме ожидания. - Подавление мелодии входящего вызова, если устройство находится в режиме «Не беспокоить». ================================================ FILE: fastlane/metadata/android/ru-RU/changelogs/9.2.0.txt ================================================ - Добавлена поддержка Opus Forward Error Correction (FEC) через переменную конфигурации Opus Expected Packet Loss. - Пытаться обновить информацию динамического DNS, если регистрация не удалась из-за ошибки «Недопустимый аргумент». - Использовать переводимые строки во всех предупреждениях, связанных с конфигурацией. ================================================ FILE: fastlane/metadata/android/ru-RU/changelogs/9.3.0.txt ================================================ — Добавлена настройка учётной записи Режим ответа (ручной или автоматический). — Добавлены переводы на норвежский букмол и журналы изменений из Weblate. ================================================ FILE: fastlane/metadata/android/ru-RU/changelogs/9.4.0.txt ================================================ - В главное меню добавлен пункт «Перезапуск». - Если регистрация не удалась из-за сбоя DNS, пробовать ещё раз после обновления DNS-серверов. - Небольшие улучшения логирования. ================================================ FILE: fastlane/metadata/android/ru-RU/full_description.txt ================================================ Это приложение SIP-клиент для Android, основанный на baresip. В настоящее время приложение baresip поддерживает голосовые вызовы, текстовые сообщения, уведомления голосовой почты, а также переводы вызовов «вслепую» и «с участием». Голос может кодироваться с помощью Opus, AMR, GSM, G.729, G.722, G.722.1, G.726 или PCMU/PCMA. Безопасность обеспечивается посредством TLS или WSS для SIP-сигнализации и инкапсуляции мультимедиа в ZRTP или (DTLS) SRTP. Разработка приложения baresip обусловлена необходимостью создания безопасного VoIP клиента на основе SIP с открытым исходным кодом для Android, который не зависит от закрытых сторонних сервисов пуш-уведомлений. Если вам нужны видеозвонки и имеется устройство с Android версии 9 или новее, поддерживающее API Camera2 на третьем или полном уровне аппаратной поддержки, можете вместо этого приложения установить родственное baresip+. Исходный код доступен на GitHub, там же можно сообщить о проблемах. ================================================ FILE: fastlane/metadata/android/ru-RU/short_description.txt ================================================ VoIP-клиент для Android, основанный на одноимённой SIP-библиотеке ================================================ FILE: fastlane/metadata/android/ru-RU/title.txt ================================================ baresip ================================================ FILE: fastlane/metadata/android/sv/changelogs/10.0.0.txt ================================================ - Återuppta baresip till pausad aktivitet istället för huvudaktivitet. ================================================ FILE: fastlane/metadata/android/sv/changelogs/10.1.0.txt ================================================ - Lagt till stöd för G722-codec. - Fixat sparande av konton. ================================================ FILE: fastlane/metadata/android/sv/changelogs/10.2.0.txt ================================================ - Stöd för G.726-codec har lagts till. - Förbättrad prioritetsordning för standardkodek. ================================================ FILE: fastlane/metadata/android/sv/changelogs/10.3.0.txt ================================================ - Lagt till menyn Samtalshistorik som gör det möjligt att radera, inaktivera och aktivera av kontots samtalshistorik. - Spanska språksträngar tillagda (tack till Javier Falbo). - Automatisk start av baresip är nu standard i nya installationer. ================================================ FILE: fastlane/metadata/android/sv/changelogs/10.4.0.txt ================================================ - Tillåt radering av kontots chatthistorik via menyalternativet Chattaktivitet. - Be om bekräftelse innan du raderar chatt- och samtalshistorik. - Förbättrad uppdatering av ikonerna för röstbrevlåda, chatt och samtal i huvudaktiviteten. - Gå direkt till aktiviteten Konton när baresip startas utan några konton. - Ombytta positioner för knapparna för att acceptera och avvisa samtal. - Tillåt inte att avsluta utgående samtal innan samtalsförsöket har påbörjats. ================================================ FILE: fastlane/metadata/android/sv/changelogs/11.0.0.txt ================================================ - Ersatte export och import av konton och kontakter med säkerhetskopiering och återställning av all applikationsdata. - Förbättrad säkerhet genom att inte tillåta säkerhetskopiering av Android-baserade applikationer och installation av applikationer till extern lagring. - När ett konto raderas kommer även kontots samtals- och chatthistorik raderas. - Fast radering av meddelanden och chattar. - Mindre förbättringar av användargränssnitt och strängar. ================================================ FILE: fastlane/metadata/android/sv/changelogs/11.1.0.txt ================================================ - Lagt till konfigurationsvariabeln "Bind Address" som gör det möjligt att välja vilket nätverksgränssnitt eller IP-adress som baresip använder. - Uppdatera alltid statusikonen för konton när du återupptar huvudaktiviteten. - Fixad krasch när man återupptar en tidigare förstörd huvudaktivitet. - Visar korrekt aktivt inkommande samtal när du återupptar huvudaktiviteten. - Uppdaterade spanska strängar. ================================================ FILE: fastlane/metadata/android/sv/changelogs/11.2.0.txt ================================================ - Lagt till stöd för AMR narrowband codec. - Bulgariska strängar har lagts till. - Konfigurationsobjektet i huvudmenyn har bytt namn till Inställningar. ================================================ FILE: fastlane/metadata/android/sv/changelogs/11.3.0.txt ================================================ - Lagt till möjligheten att välja vilka ljudmoduler som ska laddas i Settings laddas. Ljudcodecs som tillhandahålls av de laddade modulerna är tillgängliga för konton att använda. - Mindre förbättringar av koden. ================================================ FILE: fastlane/metadata/android/sv/changelogs/12.0.0.txt ================================================ - Initial implementering av ljud via Bluetooth-headset. ================================================ FILE: fastlane/metadata/android/sv/changelogs/12.1.0.txt ================================================ - Tillåt portnummer i kontots Address of Record (AoR). - Fixat kontroll av om konto redan finns. - Uppdaterade NO- och BG-strängar. ================================================ FILE: fastlane/metadata/android/sv/changelogs/12.2.0.txt ================================================ - Använde nya API-funktioner 'net_set_address' och 'net_set_af' för att implementera Prefer IPv6-funktionalitet. - Tog bort Bind Address från Inställningar (det fungerade inte som förväntat). ================================================ FILE: fastlane/metadata/android/sv/changelogs/12.2.1.txt ================================================ - Förbättrad och fixerad hantering av dynamiska nätverksförändringar. ================================================ FILE: fastlane/metadata/android/sv/changelogs/13.0.0.txt ================================================ - Spela upp aviseringsljud även när meddelandet anländer och baresip är synligt. - Om kontaktens namn är tomt, använd kontaktens SIP URI AoR som kontaktens namn. - Avslappnad kontroll av kontaktens namn. - Undvik enstaka krascher när du väljer ett konto i spinnern. - Be om tillstånd att spela in ljud (mikrofon) när baresip startar (om det inte redan beviljats eller permanent nekats). - Förbättrad hantering av skriv-/läsbehörighet för extern lagring (lagringsutrymme) vid säkerhetskopiering/återställning. - Behörigheten Read Phone State (Phone) behövs inte längre. ================================================ FILE: fastlane/metadata/android/sv/changelogs/13.0.1.txt ================================================ - Meddela alltid user agent spinner om eventuell ändring av datauppsättning när återuppta huvudaktivitet utan avsikt. ================================================ FILE: fastlane/metadata/android/sv/changelogs/14.0.0.txt ================================================ - Lagt till karaktärsbaserade avatarer i kontakter, chattar och samtalshistorik. - Fixat sparande/återställning av kontakter med icke-ASCII-kontaktnamn. - Förbättrad formatering av datum och klockslag. - Lagt till anmärkning om begränsning av flera nätverksgränssnitt. ================================================ FILE: fastlane/metadata/android/sv/changelogs/15.0.0.txt ================================================ - Bildbaserade kontaktavatarer har lagts till som alternativ till karaktärsavatarer. - Små förbättringar och korrigeringar av användargränssnittet. ================================================ FILE: fastlane/metadata/android/sv/changelogs/15.1.0.txt ================================================ - Lagt till möjlighet att ange transportprotokoll för kontots AoR. - Visa inte kontots port eller transportprotokoll förutom i Account Activity. ================================================ FILE: fastlane/metadata/android/sv/changelogs/15.1.1.txt ================================================ - Mindre strängfixar och förbättringar. ================================================ FILE: fastlane/metadata/android/sv/changelogs/16.0.0.txt ================================================ - Ersatte den globala inställningen "Prefer IPv6" med den kontospecifika inställningen "Prefer IPv6 Media". - Förbättrad hantering av förändringar i nätverksanslutningen. - Kräver inte port i utgående URI för IPv6. - Övergav ljudfokus även vid avslutning om det inte redan gjorts tidigare. - Lagt till initialt stöd för ryska språket. - Uppdaterade vissa norska strängar. ================================================ FILE: fastlane/metadata/android/sv/changelogs/16.0.1.txt ================================================ - Omregistrera UA:er när ett nytt aktivt nätverk blir tillgängligt eller när länkens egenskaper ändras även om IP-adresserna förblir desamma. ================================================ FILE: fastlane/metadata/android/sv/changelogs/16.1.0.txt ================================================ - Omregistrera inte konton när länkens egenskaper ändras, men IP-adresserna förblir desamma. - Om baresip inte startar, låt användaren ändra de inställningar som kan ha orsakat problemet. - String-uppdateringar och korrigeringar. ================================================ FILE: fastlane/metadata/android/sv/changelogs/17.0.0.txt ================================================ - Fixat sparande av ett konto som har ;transportparameter i sin AoR. - Fixat aktivering av meddelanden när samtal stängs. - Använd vit statusikon när kontots registrering inte har aktiverats. - Standardvolymen för samtal påverkar nu både röstsamtal och musikströmmar. - Förbättrad layout och implementering av kontoaktivitet. - Visa inte debug-meddelanden från Baresip Lib om Debug inte har aktiverats. - Skapa inte om aktiviteter när skärmens orientering ändras. - Visa chattinformation som inte ryms på en rad på rätt sätt ================================================ FILE: fastlane/metadata/android/sv/changelogs/17.1.0.txt ================================================ - Använd användningstyp istället för strömtyp vid begäran om ljudfokus. - Fixad formatering av engelsk hjälptext för konton. - Visa inte möjlig port för en AoR när du får frågan om att radera AoR. - Fyllde ikonen för icke-registerstatus med vit färg. ================================================ FILE: fastlane/metadata/android/sv/changelogs/17.2.0.txt ================================================ - Autentiseringens användarnamn är nu standard för kontots användarnamn. - Fråga om autentiseringslösenord vid baresip-start om autentiserings användarnamn har angetts, men inte lösenordet. - Initialisera kontots statuspunkt till gul, när Register är markerat i kontoaktivitet. - Åtgärdat fel i samband med skapandet av första kontot på äldre Android-versioner. - Mindre förbättringar av strängar. ================================================ FILE: fastlane/metadata/android/sv/changelogs/17.2.1.txt ================================================ - Åtgärdat fel i lagring av konton i filsystemet. ================================================ FILE: fastlane/metadata/android/sv/changelogs/17.2.2.txt ================================================ - Prenumerera inte på indikering av väntande meddelande när konto skapas och URI för röstbrevlåda inte är inställd. ================================================ FILE: fastlane/metadata/android/sv/changelogs/17.2.3.txt ================================================ - Förhindra krascher till följd av dubbelklick på olika ikoner och listobjekt. - Ändra färgen på kontots meddelandeikon till vit när kontot är avregistrerat. ================================================ FILE: fastlane/metadata/android/sv/changelogs/17.3.0.txt ================================================ - Lagt till kryssrutan "Visa lösenord" i meddelandet om autentiseringslösenord. - Be om mikrofontillstånd som det första när baresip startar. - Tillåt inte " tecken i kontots autentiseringslösenord. ================================================ FILE: fastlane/metadata/android/sv/changelogs/17.4.0.txt ================================================ - Lagt till stöd för Android 10 (API-nivå 29). - Förbättrad fråga om mikrofontillstånd när baresip startas för första gången. - Undvik krasch om samtalsknappen vidrörs utan konton. ================================================ FILE: fastlane/metadata/android/sv/changelogs/18.0.0.txt ================================================ - På grund av Android 10-begränsningar kan baresip inte längre startas automatiskt startas automatiskt efter uppstart utan ett meddelande. - Fixad omstart av baresip på Android 10. - Borttagen inställning av ICE Lite Mode på grund av borttagning uppströms. - Lagt till brasiliansk portugisiska strängar från Weblate. - Små korrigeringar i spanska och norska strängar. ================================================ FILE: fastlane/metadata/android/sv/changelogs/18.0.1.txt ================================================ - Ökning av det maximala antalet ljudcodecs för kontot från 8 till 16. ================================================ FILE: fastlane/metadata/android/sv/changelogs/18.1.0.txt ================================================ - Förbättrad konfiguration av kontots ljudcodecs. - Visa alltid inkommande samtal (om något) när baresip återupptas. - Förbättrad och fixad återupptagning till icke-huvudaktiviteter. ================================================ FILE: fastlane/metadata/android/sv/changelogs/18.1.1.txt ================================================ - Fixad krasch när du ringer från samtalshistoriken. ================================================ FILE: fastlane/metadata/android/sv/changelogs/18.1.2.txt ================================================ - Fixad krasch när man återvänder till föregående aktivitet efter att ha fått samtal eller meddelande. - Bättre hantering av närhetssensor och högtalartelefon. ================================================ FILE: fastlane/metadata/android/sv/changelogs/18.1.3.txt ================================================ - Förenklad styrning av närhetsavkänning. ================================================ FILE: fastlane/metadata/android/sv/changelogs/18.2.0.txt ================================================ - Separat konfiguration av kontots ljudcodecs till en ny vy. ================================================ FILE: fastlane/metadata/android/sv/changelogs/18.2.1.txt ================================================ - Fixat två chattrelaterade krascher. - Mindre förbättringar av koden. ================================================ FILE: fastlane/metadata/android/sv/changelogs/18.3.0.txt ================================================ - Förbättringar av dialogen. ================================================ FILE: fastlane/metadata/android/sv/changelogs/19.0.0.txt ================================================ - Lagt till alternativet TURN-konto medianat. - Lagt till möjlighet att ange användarnamn/lösenord för STUN/TURN-servern. - STUN/TURN-relaterade strängar för vissa språk har inte uppdaterats ännu. - Fler förbättringar av dialogrutan. ================================================ FILE: fastlane/metadata/android/sv/changelogs/19.1.0.txt ================================================ - Förbättringar i dialogrutan för varningar. ================================================ FILE: fastlane/metadata/android/sv/changelogs/20.0.0.txt ================================================ - Förbättringar av akustisk ekoavstörning. - Flyttat ljudinställningar från Inställningar till ny aktivitet. - Lagt till inställningen AEC Extented Filter. ================================================ FILE: fastlane/metadata/android/sv/changelogs/20.0.1.txt ================================================ - Fixat återgång till Audio Activity efter paus. - Fast tillägg av ljudmoduler i ljudinställningar. ================================================ FILE: fastlane/metadata/android/sv/changelogs/20.0.2.txt ================================================ - Fixar och förbättringar relaterade till knappen Dialpad. ================================================ FILE: fastlane/metadata/android/sv/changelogs/20.1.0.txt ================================================ - Lagt till bidragande RU-strängar. - Uppströmsfix av möjlig krasch. ================================================ FILE: fastlane/metadata/android/sv/changelogs/21.0.0.txt ================================================ - Inledande stöd för överföring av samtal från baresip har lagts till. - Lagt till nytt språk portugisiska från Weblate. ================================================ FILE: fastlane/metadata/android/sv/changelogs/21.1.0.txt ================================================ - Lagt till möjlighet att omregistrera valt konto genom att svepa nedåt. - Automatisk fokus och visning av mjukt tangentbord när lösenord eller överförings destination frågas. - Olika strängrelaterade korrigeringar. ================================================ FILE: fastlane/metadata/android/sv/changelogs/21.2.0.txt ================================================ - Lagt till AMR-WB (Adaptive Multi-Rate Wideband) talkodek. - Avsnittet Licenser har lagts till i texten Om. - Nya översättningar till portugisiska (Brasilien). ================================================ FILE: fastlane/metadata/android/sv/changelogs/22.0.0.txt ================================================ - Websocket-transport för SIP har lagts till (RFC 7118). ================================================ FILE: fastlane/metadata/android/sv/changelogs/22.1.0.txt ================================================ - Nya översättningar till portugisiska (Brasilien) och ryska. - Uppdatera ikoner efter återupptagande av applikationen. ================================================ FILE: fastlane/metadata/android/sv/changelogs/23.0.0.txt ================================================ - Lagt till synlighetsknapp för kontots autentiserings- och STUN/TURN-lösenord - Föredrar VPN-gränssnitt (om det finns något) när du väljer bland tillgängliga nätverksgränssnitt - Lagt till en anmärkning i About-texten om preferensordning för gränssnitt - Förbättrad konfiguration av kontots Media NAT Traversal - Standard till Googles STUN-server även när protokollet Media NAT Traversal är ICE - Ökat maxlängden för Kontots autentiserings- och STUN/TURN-användarnamn samt visningsnamn till 64 tecken - Lagt till/uppdaterat översättningar av FR-, NO- och FI-strängar ================================================ FILE: fastlane/metadata/android/sv/changelogs/23.1.0.txt ================================================ - Ljudkodek G.729 har lagts till. - Lagt till en inställning för att slå på/av spårning av SIP-meddelanden till logcat. - STUN/TURN Server URI-scheman "stuns" och "turns" stöds inte för närvarande. - Nya portugisiska (Brasilien) strängöversättningar. ================================================ FILE: fastlane/metadata/android/sv/changelogs/23.2.0.txt ================================================ - Förbättrad upptäckt av förändringar i VPN-anslutningen - Nya översättningar till portugisiska (Brasilien) ================================================ FILE: fastlane/metadata/android/sv/changelogs/24.0.0.txt ================================================ - Använd adaptiv jitterbuffert ================================================ FILE: fastlane/metadata/android/sv/changelogs/24.1.0.txt ================================================ - Förbättrad hantering av volymknappar för upp/ned - Förbättrad hantering av högtalartelefon - Nya RU-översättningar ================================================ FILE: fastlane/metadata/android/sv/changelogs/24.2.0.txt ================================================ - Visa automatiskt DTMF soft keyboard när samtalet är anslutet - Undvik krasch som orsakas av att du trycker på volymkontrollknapparna när du inte är i samtal ================================================ FILE: fastlane/metadata/android/sv/changelogs/24.3.0.txt ================================================ - Visa nummerplatta automatiskt endast när enheten är orienterad i stående läge - Spela inte upp samtalsvänteljudet när ett andra samtal kommer in - Förbättrad Om-text ================================================ FILE: fastlane/metadata/android/sv/changelogs/24.4.0.txt ================================================ - Ange inte STUN/TURN-serverns användarnamn/lösenord som standard till Authentication Username/Password ================================================ FILE: fastlane/metadata/android/sv/changelogs/25.0.0.txt ================================================ - Förbättrad avisering av inkommande samtal. - Meddelande om missat samtal har lagts till. ================================================ FILE: fastlane/metadata/android/sv/changelogs/26.0.0.txt ================================================ - Lagt till mörkt tema och inställning för mörkt tema. - Förbättringar av kontospinnare. ================================================ FILE: fastlane/metadata/android/sv/changelogs/26.0.1.txt ================================================ - Fixade att slå på mörkt tema när Baresip-applikationen startas - Nya översättningar (portugisiska (Brasilien)) ================================================ FILE: fastlane/metadata/android/sv/changelogs/26.1.0.txt ================================================ - Andra appar kan nu se baresip som en telefonapp för sip: och tel: URI:er - Förbättrat sparande/återställning av samtals-URI-text när huvudaktiviteten pausas - Introducerad japansk översättning - Förbättringar av färger ================================================ FILE: fastlane/metadata/android/sv/changelogs/26.1.1.txt ================================================ - Korrigerad hantering av samtalsåtgärd när Baresip-appen inte körs. ================================================ FILE: fastlane/metadata/android/sv/changelogs/26.1.2.txt ================================================ - Försök att upptäcka rotation av kontaktavatarbilder ================================================ FILE: fastlane/metadata/android/sv/changelogs/27.0.0.txt ================================================ - Lagt till möjlighet att exportera Baresip-kontakter till Android-kontakter ================================================ FILE: fastlane/metadata/android/sv/changelogs/27.0.1.txt ================================================ - Fixad krasch när baresip startas första gången ================================================ FILE: fastlane/metadata/android/sv/changelogs/28.0.0.txt ================================================ - Lagt till möjlighet att delvis autokonfigurera ett nytt konto från en webbsida (se https://github.com/juha-h/baresip-studio/wiki/Automatic-Account-Configuration för detaljer) ================================================ FILE: fastlane/metadata/android/sv/changelogs/28.1.0.txt ================================================ - Förbättrad sökning av en kontakt som matchar en SIP URI - Förbättringar av verktygsfält och menystil ================================================ FILE: fastlane/metadata/android/sv/changelogs/28.1.1.txt ================================================ - Fixad färgning av inställningen för ljudmoduler - Fixad krasch och utseende relaterad till samtalslista ================================================ FILE: fastlane/metadata/android/sv/changelogs/28.2.0.txt ================================================ - Lagt till inställningen "Verifiera servercertifikat ================================================ FILE: fastlane/metadata/android/sv/changelogs/29.0.0.txt ================================================ - Aktiverat svep vänster/höger för att växla mellan konton - Lagt till kontoinställning för DTMF-läge - Uppdatering av översättningar från Weblate (franska) ================================================ FILE: fastlane/metadata/android/sv/changelogs/29.1.0.txt ================================================ - Portugisiska (Brasilien) översättningar från Weblate - Fixad F-Droid nb-NO locale tagg - Undvik varningar för "duplicerad begäran om slutförande ================================================ FILE: fastlane/metadata/android/sv/changelogs/29.2.0.txt ================================================ - Buggfixar relaterade till chatt, samtalshistorik och kontakter - Fixade buggar i URI-komplettering och förbättrade URI-relaterade kontroller - Översättningar från Weblate (franska, portugisiska (Brasilien)) ================================================ FILE: fastlane/metadata/android/sv/changelogs/29.2.1.txt ================================================ - Undvik eventuella kontaktrelaterade krascher vid start av Baresip ================================================ FILE: fastlane/metadata/android/sv/changelogs/3.2.0.txt ================================================ - Lagt till knapp för att välja mellan telefonnummer och mjukt tangentbord för text. - Fixat automatisk komplettering av callee baserat på kontakter. ================================================ FILE: fastlane/metadata/android/sv/changelogs/3.2.2.txt ================================================ - Lagt till hantering av händelsen "Samtalsöverföring misslyckades". - Undvikit krasch vid (åter)start av enhet. - Undvikit krasch när man för första gången kommer tillbaka från konton till huvudaktivitet. - Använd "textEmailAddress"-inmatningstyp för konto-URI:er. - Ändrade betydelsen av standardmeddelandekanal till låg. ================================================ FILE: fastlane/metadata/android/sv/changelogs/30.0.0.txt ================================================ - Uppgraderad mål SDK-version till API-nivå 30 - I Android-versioner 10 och högre, låt användaren välja filen i säkerhetskopiering, återställning, TLS-certifikatrelaterade operationer ================================================ FILE: fastlane/metadata/android/sv/changelogs/30.0.1.txt ================================================ - Uppströmslösning för att välja rätt konto för inkommande samtal. ================================================ FILE: fastlane/metadata/android/sv/changelogs/30.1.0.txt ================================================ - Lösenordsdialog och inmatning av kontolösenord har förbättrats. ================================================ FILE: fastlane/metadata/android/sv/changelogs/30.2.0.txt ================================================ - Lagt till stöd för AMR-codec Bandwidth Efficient Mode - Tillåt undangömda tecken i SIP URI-användardelen och autentiseringsanvändarnamnet - Använd coroutine istället för async task för att hämta kontokonfiguration från nätverket - I Android 10+ använd väljare för att välja TLS-certifikat och TLS CA-filer - Be om bekräftelse för att återställa inställningar till fabriksinställningar - Nya översättningar (portugisiska och portugisiska (Brasilien)) ================================================ FILE: fastlane/metadata/android/sv/changelogs/30.3.0.txt ================================================ - Spela upp autosvarsljud vid autosvar - Borttagen iLBC-codec (borttagen från uppströms) - Tog bort oanvända ljudfiler ================================================ FILE: fastlane/metadata/android/sv/changelogs/31.0.0.txt ================================================ - Förbättrad Om-text - Återställer chattlistans position när du återvänder från chatten - Fixad krasch när man frågar efter kontots lösenord - Försök inte ladda iLBC-modulen ================================================ FILE: fastlane/metadata/android/sv/changelogs/31.1.0.txt ================================================ - Uppströmsfix av websocket-transport - Nya översättningar till portugisiska (Brasilien) - Kontrollera utgående proxy URI-transport ================================================ FILE: fastlane/metadata/android/sv/changelogs/31.2.0.txt ================================================ - Förbättringar av konto STUN/TURN URI - Diverse ljudrelaterade förbättringar - Nya översättningar till portugisiska (Brasilien) och slovenska ================================================ FILE: fastlane/metadata/android/sv/changelogs/31.2.1.txt ================================================ - Korrigerad krasch när du väljer ett konto i kontoaktiviteten ================================================ FILE: fastlane/metadata/android/sv/changelogs/32.0.0.txt ================================================ - Lagt till knapp för att slå på/av mikrofonen under samtal - Använd horisontell rullningsvy för knappar för samtalsstyrning ================================================ FILE: fastlane/metadata/android/sv/changelogs/32.1.0.txt ================================================ - Nya översättningar till spanska och portugisiska (Brasilien) - Mindre förbättringar relaterade till AudioManager ================================================ FILE: fastlane/metadata/android/sv/changelogs/32.2.0.txt ================================================ - Förbättrad kontroll av SIP URI-parametrar - Fler korrigeringar och förbättringar relaterade till ljudhanterare/bluetooth ================================================ FILE: fastlane/metadata/android/sv/changelogs/32.3.0.txt ================================================ - Förvärva WiFi-lås medan baresip använder WiFi-nätverk - Återställde oavsiktligt borttagen ringsignal - Lagt till en notis om att stänga av batterioptimering - Svenska språköversättningar ================================================ FILE: fastlane/metadata/android/sv/changelogs/33.0.0.txt ================================================ - Förvärva WiFi-lås när baresip använder WiFi-nätverk - Lagt till anmärkning i Om-texten angående batterioptimering - Nya översättningar till svenska och portugisiska (Brasilien) - Diverse implementeringsförbättringar ================================================ FILE: fastlane/metadata/android/sv/changelogs/34.0.0.txt ================================================ - Lagt till begränsat stöd för flera samtidigt aktiva nätverksgränssnitt - Visa meddelande när baresip startas utan nätverksåtkomst ================================================ FILE: fastlane/metadata/android/sv/changelogs/34.1.0.txt ================================================ - Använd WebRTC Acoustic Echo Cancellation Mobile Mode-filter ================================================ FILE: fastlane/metadata/android/sv/changelogs/35.0.0.txt ================================================ - Använd WebRTC Acoustic Echo Cancellation Mobile Mode-filter - Visa antalet missade samtal i meddelandet om det är fler än ett - När standardinställningen för samtalsvolym är inställd, ställ in både media- och samtalsvolym - Avbryt aviseringar när huvudaktiviteten återupptas - Fixat stopp för ringning i äldre Android-versioner - Nya översättningar till svenska och portugisiska (Brasilien) ================================================ FILE: fastlane/metadata/android/sv/changelogs/35.1.0.txt ================================================ - När aec är aktiverat/inaktiverat, ladda/avladda webrtc_aecm-modulen - Använd BaresipService för att spela upp alla ljud - Korrekt hantering av svar på sessionsförlopp - Under samtal styr volymknapparna nu medievolymen - Använd kommunikationsljudläge när du inte är i ringljudläge ================================================ FILE: fastlane/metadata/android/sv/changelogs/35.2.0.txt ================================================ - Fixar och förbättringar relaterade till ljudfokus - Ta bort alla blanksteg från callee-fältet - Ny översättning till portugisiska (Brasilien) ================================================ FILE: fastlane/metadata/android/sv/changelogs/36.0.0.txt ================================================ - Migrerade baresip till API-nivå 31 (Android 12) - Förbättrad detektering av tillgängliga nätverk - Förbättrade behörighetsförfrågningar - Tog bort kontoinställningen Föredra IPv6 Media - Ny översättning till portugisiska (Brasilien) ================================================ FILE: fastlane/metadata/android/sv/changelogs/36.1.0.txt ================================================ - Flyttat knappen för att stänga av samtal från samtalskontrollen till åtgärdsfältet - Lagt till inställning för batterioptimering - Be om tillstånd att spela in ljud när baresip startas - Nya översättningar till portugisiska (Brasilien), franska och svenska ================================================ FILE: fastlane/metadata/android/sv/changelogs/36.1.1.txt ================================================ - Flyttat knappen för att stänga av samtal från samtalskontrollen till åtgärdsfältet - Lagt till inställning för batterioptimering - Be om tillstånd att spela in ljud när baresip startas - Nya översättningar till portugisiska (Brasilien), franska och svenska ================================================ FILE: fastlane/metadata/android/sv/changelogs/36.1.2.txt ================================================ - Förhindrade swipe-relaterad krasch ================================================ FILE: fastlane/metadata/android/sv/changelogs/36.2.0.txt ================================================ - För närvarande är mikrofonikonen endast aktiv när ett samtal är anslutet - Nya svenska översättningar ================================================ FILE: fastlane/metadata/android/sv/changelogs/37.0.0.txt ================================================ - Implementering av samtalsöverföring enligt RFC 5589 Basic Transfer ================================================ FILE: fastlane/metadata/android/sv/changelogs/37.0.1.txt ================================================ Fastställda ljudinställningar för utgående samtal ================================================ FILE: fastlane/metadata/android/sv/changelogs/37.1.0.txt ================================================ - Lagt till uppgifter om genomsnittlig bithastighet, jitter, antal paket och antal förlorade paket i samtalsinfo - Använd pausikonen med accentfärg för att indikera att användaren har satt samtalet på vänt - Informera användaren när den andra parten har satt samtalet i vänteläge - Mindre förbättringar relaterade till mjuk inmatning - Nya översättningar till portugisiska (Brasilien) ================================================ FILE: fastlane/metadata/android/sv/changelogs/37.2.0.txt ================================================ - Bättre skydd för baresip-appen när enheten är låst med secure keyguard - Nya översättningar till portugisiska (Brasilien) ================================================ FILE: fastlane/metadata/android/sv/changelogs/37.3.0.txt ================================================ - Flyttad inställning för standardvolym för samtal under Ljudinställningar - Nytt språk (tyska) ================================================ FILE: fastlane/metadata/android/sv/changelogs/37.4.0.txt ================================================ - Nya översättningar till portugisiska och tyska ================================================ FILE: fastlane/metadata/android/sv/changelogs/38.0.0.txt ================================================ - Förbättrad hantering av låsning/upplåsning av enheter - I Inställningar har en länk till Android-inställningar lagts till - Kontrollera korrekt om inställningen för batterioptimering har ändrats - Nya översättningar till svenska och portugisiska (Brasilien) ================================================ FILE: fastlane/metadata/android/sv/changelogs/39.0.0.txt ================================================ - Förbättrad visning av tidsvärdet i samtalshistoriken i samtalshistoriklistan - Lagt till aktivitet för samtalsinformation som kan nås genom att trycka på tid i samtalshistoriklistan - Fixat visning av avatarer i kontakt-, samtals- och chattlistor - Tillåt avbrytande av besvarat samtal som väntar på att etableras - Nya översättningar till portugisiska, portugisiska Brasilien, svenska och norska ================================================ FILE: fastlane/metadata/android/sv/changelogs/39.1.0.txt ================================================ - Förbättrad och korrigerad hantering av meddelanden och samtalsöverföring - Toastregistrering misslyckas med alla aktuella aktiviteter - Automatisk kapitalisering av meddelandemeningar - Ersatte föråldrade lokala sändningar med LiveData-händelser - Fixat några buggar relaterade till hantering av externa samtalsåtgärder - Nya översättningar till portugisiska (Brasilien) ================================================ FILE: fastlane/metadata/android/sv/changelogs/4.0.0.txt ================================================ - Förbättrad implementering av kontakter, meddelanden och samtalshistorik. - Undviker krasch när meddelanden kommer innan konton har lagts till. ================================================ FILE: fastlane/metadata/android/sv/changelogs/4.1.0.txt ================================================ - Alla data som inte är relaterade till användargränssnittet sparas nu i baresip-tjänsten. vilket möjliggör korrekt återställning om Android dödar andra baresip-aktiviteter. - Många andra förbättringar och buggfixar. ================================================ FILE: fastlane/metadata/android/sv/changelogs/4.1.1.txt ================================================ - Åtgärdat en kritisk bugg i kontrollen av inspelningstillstånd för ljud som förhindrade ny baresip-installation från att starta. - Små förbättringar. ================================================ FILE: fastlane/metadata/android/sv/changelogs/4.1.2.txt ================================================ - Korrigerad krasch när man klickar på spinnern för tomma konton. ================================================ FILE: fastlane/metadata/android/sv/changelogs/40.0.0.txt ================================================ - Konfigurationsobjekt för konto för telefonileverantör har lagts till - Lagt till stöd för tel URI som Callee-värde och Contact URI - Fråga användaren efter Telephony Provider-konto om det behövs för att ringa samtalet - Visar kontots STUN-konfigurationsobjekt endast om det behövs - Förbättrad hantering av CALL-åtgärder från andra program ================================================ FILE: fastlane/metadata/android/sv/changelogs/40.0.1.txt ================================================ - Konfigurationsobjekt för konto för telefonileverantör har lagts till - Lagt till stöd för tel URI som Callee-värde och Contact URI - Fråga användaren efter Telephony Provider-konto om det behövs för att ringa samtalet - Visar kontots STUN-konfigurationsobjekt endast om det behövs - Förbättrad hantering av CALL-åtgärder från andra program ================================================ FILE: fastlane/metadata/android/sv/changelogs/40.1.0.txt ================================================ - Spela inte upptaget- eller felljud när samtalet är avslutat - Förbättrad tillförlitlighet vid användning av Bluetooth - Bättre hantering av händelser från baresip-kärnan - Nya översättningar till portugisiska (Brasilien) ================================================ FILE: fastlane/metadata/android/sv/changelogs/41.0.0.txt ================================================ - Lagt till kronometer för samtalstimer i raden "Samtal till .../Samtal från ..." - Lagt till möjlighet att använda Android-kontakter - Lagt till inställningen "Visa Android-kontakter" för att välja om Android-kontakter ska visas när du trycker på kontaktknappen - Kontrollera att återställningsfilen har skapats av Baresip-appen - Nya översättningar till svenska och portugisiska (Brasilien) ================================================ FILE: fastlane/metadata/android/sv/changelogs/42.0.0.txt ================================================ - Ersatt inställningen ”Visa Android-kontakter” med inställningen ”Kontakter” som gör det möjligt att välja om baresip-kontakter, Android-kontakter eller båda ska användas - Korrigeringar och förbättringar av mappningen mellan kontaktnamn och kontakt-URI - Undvik krasch när Android tvingar baresip-appen att starta om ================================================ FILE: fastlane/metadata/android/sv/changelogs/42.1.0.txt ================================================ - Ersatte AppCompat-temat med MaterialComponents-temat ================================================ FILE: fastlane/metadata/android/sv/changelogs/42.2.0.txt ================================================ - Kontaktvalslistan innehåller nu bara de Android-kontakter som har exakt en URI - Nya översättningar till svenska och portugisiska (Brasilien) ================================================ FILE: fastlane/metadata/android/sv/changelogs/42.3.0.txt ================================================ - Fast tillägg av ny baresip-kontakt - När du väljer ett nytt standardkonto, behåll ordningen på resten oförändrad - Förbättrad hantering av samtal som avvisas automatiskt ================================================ FILE: fastlane/metadata/android/sv/changelogs/43.0.0.txt ================================================ - Inledande stöd för överföring av samtal med uppvaktning har lagts till - I chattaktivitet ersattes långklick med kortklick - Gjorde meddelandetexten valbar - Använd fetstil i AoR-spinnertexten när samtalet är aktivt ================================================ FILE: fastlane/metadata/android/sv/changelogs/43.0.2.txt ================================================ - Inledande stöd för överföring av samtal med uppvaktning har lagts till - I chattaktivitet ersattes långklick med kortklick - Gjorde meddelandetexten valbar - Använd fetstil i AoR-spinnertexten när samtalet är aktivt ================================================ FILE: fastlane/metadata/android/sv/changelogs/43.1.0.txt ================================================ - Fixat 32 bitars armeabi-v7a arkitekturrelaterad bugg - Visa Anonym eller Okänd om URI-värden är anonymous.invalid eller unknown.invalid ================================================ FILE: fastlane/metadata/android/sv/changelogs/44.0.0.txt ================================================ - Visa vidarekoppling om inkommande samtal har vidarekopplats - Backup-relaterade korrigeringar i äldre Android-versioner - Förbättrad matchning av URI till kontakt - Nya översättningar till portugisiska (Brasilien) ================================================ FILE: fastlane/metadata/android/sv/changelogs/44.1.0.txt ================================================ - På grund av Googles krav, be användarens samtycke om Android-kontakter väljs i Inställningar - Nya översättningar till portugisiska (Brasilien) ================================================ FILE: fastlane/metadata/android/sv/changelogs/44.2.0.txt ================================================ - Fixad krasch på ARMv7-enheter när meddelande togs emot - Nya översättningar till portugisiska (Brasilien) ================================================ FILE: fastlane/metadata/android/sv/changelogs/446.txt ================================================ - Fixad Kontrollera att du inte stör - Nya översättningar ================================================ FILE: fastlane/metadata/android/sv/changelogs/447.txt ================================================ - Ändra färgen på samtalsknappen till gul när samtalsknappen har vidrörts, men samtalet ännu inte har ringts - Aktivera inte värdelös mjukvarubaserad ekosläckning om hårdvarubaserad inte är tillgänglig - Ta alltid bort säkerhetsikonen och DTMF-fältet när samtalet avslutas - För registrerade konton, kontrollera att användardelen av inkommande Request URI matchar användardelen av REGISTER request contact URI - Förbättringar av jitterbufferten för uppströms ljud - Nya översättningar (franska) ================================================ FILE: fastlane/metadata/android/sv/changelogs/448.txt ================================================ - Weblate-anteckning tillagd i texten Om - Nya översättningar (kinesiska) - Baresip lib loggning makro fix ================================================ FILE: fastlane/metadata/android/sv/changelogs/449.txt ================================================ - Fixat tillägg av nytt samtal till historik - Undvik "flash" på skärmen när du ringer från kontakter eller samtalshistorik - Nya översättningar (tjeckiska) ================================================ FILE: fastlane/metadata/android/sv/changelogs/45.0.0.txt ================================================ - Lagt till konfigurationsalternativ för landskodskonto - Ljudrelaterade förbättringar från uppströms - Nya översättningar till portugisiska (Brasilien) och finska ================================================ FILE: fastlane/metadata/android/sv/changelogs/45.1.0.txt ================================================ - Använd elliptisk kurvkryptografi baserad på DTLS Key Establishment Protocol - Nya svenska översättningar ================================================ FILE: fastlane/metadata/android/sv/changelogs/45.1.1.txt ================================================ - Använd elliptisk kurvkryptografi baserad på DTLS Key Establishment Protocol - Fixad säkerhetskopiering av samtalshistorik - Nya svenska översättningar ================================================ FILE: fastlane/metadata/android/sv/changelogs/45.1.2.txt ================================================ Fixar och förbättringar relaterade till ring- och notifieringston ================================================ FILE: fastlane/metadata/android/sv/changelogs/46.0.0.txt ================================================ - Lagt till möjlighet att aktivera/avaktivera registrering av konto via sändningsintentioner (se Wiki för detaljer) - Uppströms tillägg av cachelagring av DNS-förfrågningar - Uppströmsfix av ICE-kandidatprioriteringar - Höjd API-nivå för Android-mål till 32 (Android 12L) ================================================ FILE: fastlane/metadata/android/sv/changelogs/46.0.1.txt ================================================ - Använd inte DNS-cache som kan orsaka långa fördröjningar i adresslösningen ================================================ FILE: fastlane/metadata/android/sv/changelogs/46.1.0.txt ================================================ - Lång tryckning på aktuellt konto aktiverar eller inaktiverar registrering av kontot - Lagt till möjlighet att ge konton smeknamn ================================================ FILE: fastlane/metadata/android/sv/changelogs/46.1.1.txt ================================================ - Lång tryckning på aktuellt konto aktiverar eller inaktiverar registrering av kontot - Lagt till möjlighet att ge konton smeknamn ================================================ FILE: fastlane/metadata/android/sv/changelogs/46.2.0.txt ================================================ - Om avregistreringen misslyckas, uppdatera ändå registreringsstatusen till vit - Ställ in lösenord när användaragent skapas - Nya översättningar till portugisiska (Brasilien) ================================================ FILE: fastlane/metadata/android/sv/changelogs/47.0.0.txt ================================================ - Lagt till stöd för tillförlitliga provisoriska svarsmeddelanden (RFC 3262) - Lagt till QUIT-åtgärd för avsiktsmottagare ================================================ FILE: fastlane/metadata/android/sv/changelogs/47.1.0.txt ================================================ - Lagt till stöd för SIP UPDATE-metoden (RFC 3311) - Uppströmsfix av tidig ljudrelaterad bugg (183 Session Progress) ================================================ FILE: fastlane/metadata/android/sv/changelogs/47.2.0.txt ================================================ - Fördröj uppringning med 1 sek efter att samtalsikonen har tryckts på eftersom ljudläget inte omedelbart ändras till kommunikation - Uppströmsfix av PRACK-hantering - Nya svenska översättningar ================================================ FILE: fastlane/metadata/android/sv/changelogs/48.0.0.txt ================================================ - Använd låsikoner för säkerhetsindikering av samtal - När du ringer till Android-kontakt, använd den senaste peer URI om Android-kontakten har den - Använd ZRTPCPP lib för ZRTP-mediekryptering (peers måste verifieras på nytt) - Aktivera DNS-cachelagring - Nya översättningar till portugisiska (Brasilien) ================================================ FILE: fastlane/metadata/android/sv/changelogs/48.0.1.txt ================================================ - Använd låsikoner för säkerhetsindikering av samtal - När du ringer till Android-kontakt, använd den senaste peer URI om Android-kontakten har den - Använd ZRTPCPP lib för ZRTP-mediekryptering (peers måste verifieras på nytt) - Aktivera DNS-cachelagring - Nya översättningar till portugisiska (Brasilien) ================================================ FILE: fastlane/metadata/android/sv/changelogs/48.1.0.txt ================================================ - Lade till GSM-codec - Gjorde fönstret orörbart och startade 5 sekunders timer vid Avsluta eller Starta om ================================================ FILE: fastlane/metadata/android/sv/changelogs/48.2.0.txt ================================================ - Inaktivera PRACK (RFC 3262)-funktionen på grund av ett allvarligt uppströmsfel - Nya översättningar till portugisiska (Brasilien) och ryska ================================================ FILE: fastlane/metadata/android/sv/changelogs/49.0.0.txt ================================================ - Förbättrat urval och ordning av kontots ljudcodecs - På grund av problem inaktiverades DNS-frågans cache - Migrering till Android API 33 ================================================ FILE: fastlane/metadata/android/sv/changelogs/49.0.1.txt ================================================ - Fixat uppströmsbugg som orsakade lång avslutningsfördröjning om röstbrevlådekontroll var aktiverad för ett konto - Förenklad Avsluta/omstarta-process och undvikit krasch vid avslutning - Nya översättningar till portugisiska (Brasilien) ================================================ FILE: fastlane/metadata/android/sv/changelogs/49.1.0.txt ================================================ - Anledning till skål när inkommande samtal stängs av baresip lib ================================================ FILE: fastlane/metadata/android/sv/changelogs/49.1.2.txt ================================================ - Uppströmsfix av felet Media NAT Traversal (STUN/TURN/ICE) ================================================ FILE: fastlane/metadata/android/sv/changelogs/49.1.3.txt ================================================ - Uppströms fixering av codec-förhandling ================================================ FILE: fastlane/metadata/android/sv/changelogs/49.2.0.txt ================================================ - Lagt till kontoinställningen "RTCP Multiplexing" - Fördelar åtgärdsknappar jämnt längst ner i huvudaktiviteten - Använde colorSecondaryDark som ledtrådsfärg i textvyn ================================================ FILE: fastlane/metadata/android/sv/changelogs/49.3.1.txt ================================================ - Lagt till kontoinställningen "RTCP Multiplexing" - Fördelar åtgärdsknappar jämnt längst ner i huvudaktiviteten - Använde colorSecondaryDark som färg för textvyns ledtråd - Visa korrekt statusmeddelande när det inte finns några registrerade UA ================================================ FILE: fastlane/metadata/android/sv/changelogs/49.4.0.txt ================================================ - Fixad krasch vid nätverksändringar - Nya översättningar till portugisiska (Brasilien) ================================================ FILE: fastlane/metadata/android/sv/changelogs/5.0.0.txt ================================================ - Ersatte textläget EditConfigActivity med en grafisk konfigurationsaktivitet. - Aktuella konfigurationsobjekt är automatisk start, DNS-servrar, Opus bithastighet, och ICE-Lite. - Lagt till kontoalternativ för ICE- och STUN-server. - Fixat fel vid borttagning av konto. - Ej skickade meddelanden går inte förlorade när chattaktiviteten avbryts. ================================================ FILE: fastlane/metadata/android/sv/changelogs/5.1.0.txt ================================================ - Lagt till möjlighet att exportera/importera kontakter till/från mappen "Download". Om du vill använda den här funktionen ska du ge baresip lagringsbehörighet i Inställningar → Appar → baresip → Behörigheter. - Fixat långsam avslutning av baresip när det inte finns någon nätverksanslutning. ================================================ FILE: fastlane/metadata/android/sv/changelogs/5.2.0.txt ================================================ - Meddela användaren om misslyckad registrering. - Lagt till knapp för samtalsinfo för ett aktivt samtal. - Kräver inte "sip:"-schema i URI för utgående proxy. - Tillåt att domänetiketten börjar med en siffra. ================================================ FILE: fastlane/metadata/android/sv/changelogs/5.2.1.txt ================================================ - Lättare kontroll av visningsnamn och autentiseringsnamn - Lagt till versionsnamn på sidan Om ================================================ FILE: fastlane/metadata/android/sv/changelogs/5.3.0.txt ================================================ - Lagt till popup-meddelande som berättar varför samtalet avslutades. - Fixat visning av URI för anropande part om den innehåller portnummer eller parametrar. ================================================ FILE: fastlane/metadata/android/sv/changelogs/5.3.1.txt ================================================ - Lättare kontroll av userpart i kontots SIP URI. - Lagt till stöd för ARMv8-A-arkitektur. ================================================ FILE: fastlane/metadata/android/sv/changelogs/5.4.0.txt ================================================ - Protokollet SRTP-MANDF för kryptering av media har lagts till. - Säkerhetsknappens färg ändras nu från rött till gult även när samtalet är skyddas av ett SRTP*-protokoll för mediekryptering. - Fast inställning av kontomediakryptering till DTLS-SRTPF. ================================================ FILE: fastlane/metadata/android/sv/changelogs/5.4.1.txt ================================================ - Tog bort oanvända baresip-moduler från konfiguration och bibliotek. - Baresip-tjänsten låter nu baresip-aktiviteten dö på egen hand efter att ha efter att ha fått en dödsavsikt från den. ================================================ FILE: fastlane/metadata/android/sv/changelogs/5.4.2.txt ================================================ - Åtgärdat en sällsynt krasch som kan inträffa när det första kontot skapas och meddelande eller samtal kommer in innan användaren har återgått till huvudaktiviteten. ================================================ FILE: fastlane/metadata/android/sv/changelogs/50.0.0.txt ================================================ - Be om POST_NOTIFICATIONS-behörighet på Android 13+-enheter - Använd grå mikrofonikon när mikrofonen inte är avstängd - Använd svarsmeddelandet "Upptagen här" när ett samtal avvisas ================================================ FILE: fastlane/metadata/android/sv/changelogs/50.1.0.txt ================================================ - Förbättringar av processen Avsluta/omstarta - Försök att stödja växling av SpeakerPhone på API-nivåer 31+ - Nya översättningar till portugisiska (Brasilien) ================================================ FILE: fastlane/metadata/android/sv/changelogs/50.1.1.txt ================================================ - Förbättringar av processen Avsluta/omstarta - Fixar och förbättringar relaterade till högtalartelefon och samtal för API 31+ - Nya översättningar till portugisiska (Brasilien) ================================================ FILE: fastlane/metadata/android/sv/changelogs/50.1.3.txt ================================================ - Förbättringar av processen Avsluta/omstarta - Fixar och förbättringar relaterade till högtalartelefon och samtal för API 31+ - Nya översättningar till portugisiska (Brasilien) ================================================ FILE: fastlane/metadata/android/sv/changelogs/50.1.4.txt ================================================ - Fler korrigeringar och förbättringar relaterade till kommunikationsenheter ================================================ FILE: fastlane/metadata/android/sv/changelogs/50.1.5.txt ================================================ - Fixat detektering av WiFi hotspot-gränssnitt på vissa enheter ================================================ FILE: fastlane/metadata/android/sv/changelogs/50.2.0.txt ================================================ - Problem med ljudrouting åtgärdat för Android 12+-enheter - Använd sekundär mörk färg i codec-namnet när du är i mörkt läge ================================================ FILE: fastlane/metadata/android/sv/changelogs/50.2.1.txt ================================================ - Problem med ljudrouting åtgärdat för Android 12+-enheter - Använd sekundär mörk färg i codec-namnet när du är i mörkt läge ================================================ FILE: fastlane/metadata/android/sv/changelogs/50.2.2.txt ================================================ - Problem med ljudrouting åtgärdat för Android 12+-enheter - Använd sekundär mörk färg i codec-namnet när du är i mörkt läge ================================================ FILE: fastlane/metadata/android/sv/changelogs/51.0.0.txt ================================================ - Vibrera och vibrera medan du ringer enligt inställningarna för ljud och vibrationer - Bluetooth-fixar och förbättringar för Android 12+-enheter - Visa behörighetsinformation när baresip startas för första gången - Nya översättningar till svenska och portugisiska (Brasilien) ================================================ FILE: fastlane/metadata/android/sv/changelogs/51.1.0.txt ================================================ - Lagt till kontoinställning för registreringsintervall - Använd telefoniprovider även när du skickar meddelanden till tel URI:er - Nu accepteras även ( och ) tecken i telefonnummer - Fixade aviseringar om inlägg i senare Android-versioner - Förbättrad behörighetshantering i senare Android-versioner - Nya översättningar till spanska, portugisiska (Brasilien) och svenska ================================================ FILE: fastlane/metadata/android/sv/changelogs/51.2.0.txt ================================================ - Flera kontaktrelaterade korrigeringar och förbättringar - Nya översättningar till portugisiska (Brasilien) ================================================ FILE: fastlane/metadata/android/sv/changelogs/52.0.0.txt ================================================ - Lagt till stöd för att välja från flera tel:/sip: URI:er i Android - kontakter - Ett aktivt konto måste nu ha en telefoniprovider konfigurerad för att kunna ringa eller skicka ett meddelande till tel: URI - Nya översättningar till portugisiska (Brasilien), spanska, svenska och finska ================================================ FILE: fastlane/metadata/android/sv/changelogs/52.1.0.txt ================================================ - Lagt till preliminärt stöd för samtalsinspelning - Många korrigeringar relaterade till att fråga efter lösenord - Förhindrar att vissa dialogrutor avfärdas utifrån - Nya översättningar till spanska, portugisiska och brasilianska ================================================ FILE: fastlane/metadata/android/sv/changelogs/52.2.0.txt ================================================ - Lagt till möjlighet att stoppa inspelningsuppspelning - Lagt till adaptiv startikon - Nya spanska översättningar ================================================ FILE: fastlane/metadata/android/sv/changelogs/53.0.0.txt ================================================ - Lagt till inställning för att välja IP-adressfamilj - Lagt till monokrom lanseringsikon - Tog bort ikon och text från statusmeddelande när Android-versionen är 13+ - Nya översättningar till spanska, ryska, portugisiska (Brasilien) och tjeckiska ================================================ FILE: fastlane/metadata/android/sv/changelogs/53.1.0.txt ================================================ - Förbättringar av ikonerna i startprogrammet - Nya ryska översättningar ================================================ FILE: fastlane/metadata/android/sv/changelogs/53.1.1.txt ================================================ - Förbättringar av ikonen för startprogrammet - Försök inte att registrera ett nytt konto om Register inte har markerats - Lagt till hjälptext för registreringsintervall - Nya ryska översättningar ================================================ FILE: fastlane/metadata/android/sv/changelogs/53.1.2.txt ================================================ - Uppströms ökning av max storlek på kontosträng ================================================ FILE: fastlane/metadata/android/sv/changelogs/53.2.0.txt ================================================ - Förbättringar av meddelanden om registreringsstatus - Flera ikonbilder har ersatts med vektorritningar i materialdesign ================================================ FILE: fastlane/metadata/android/sv/changelogs/53.2.1.txt ================================================ - Åtgärdat flera problem med ljudfokus/routing/volymkontroll ================================================ FILE: fastlane/metadata/android/sv/changelogs/53.2.2.txt ================================================ - Åtgärdat flera problem med ljudfokus/routing/volymkontroll ================================================ FILE: fastlane/metadata/android/sv/changelogs/53.2.3.txt ================================================ - Förenklad uppspelning av ringsignal - Nya portugisiska översättningar ================================================ FILE: fastlane/metadata/android/sv/changelogs/54.0.0.txt ================================================ - Kräver minst Android version 6 (API-nivå 23) - I Android-versioner under 12, lägg till 1,5 sekunders fördröjning innan du ringer ett samtal för att undvika att missa ljud från den som ringer ================================================ FILE: fastlane/metadata/android/sv/changelogs/54.1.0.txt ================================================ - Ljudinställningen "Audio Delay" har lagts till - Förbättrat test för anslutning av Bluetooth-headset ================================================ FILE: fastlane/metadata/android/sv/changelogs/54.2.0.txt ================================================ - Spela upptagetton när svaret "486 Upptagen här" eller "603 Avböj" tas emot - Undvik onödig omregistrering när nätverkskapaciteten ändras - Förenklat övergivande av ljudfokus ================================================ FILE: fastlane/metadata/android/sv/changelogs/55.0.0.txt ================================================ - Förhindra att baresip-tjänsten startas mer än en gång - Förbättringar av färger - Nya översättningar till spanska, ryska, portugisiska (Brasilien), svenska och tjeckiska ================================================ FILE: fastlane/metadata/android/sv/changelogs/55.0.1.txt ================================================ - Uppströmsfix för samtal som ligger kvar i aktiv lista även när det stängs - Bättre hantering av händelser för aktivering/inaktivering av WiFi-hotspot ================================================ FILE: fastlane/metadata/android/sv/changelogs/55.0.2.txt ================================================ - Uppströmsfix för samtal som ligger kvar i aktiv lista även när det stängs - Bättre hantering av händelser för aktivering/inaktivering av WiFi-hotspot - Förbättrad färgkontrast för textknappen i varningsdialogen i mörka teman ================================================ FILE: fastlane/metadata/android/sv/changelogs/55.1.0.txt ================================================ - Förbättringar av färger i mörkt tema - Förbättrat automatiskt avvisande av samtal ================================================ FILE: fastlane/metadata/android/sv/changelogs/55.2.0.txt ================================================ - Förutom CALL kan du även hantera DIAL-åtgärder från andra applikationer - Nya översättningar till spanska och portugisiska (Brasilien) ================================================ FILE: fastlane/metadata/android/sv/changelogs/56.0.0.txt ================================================ - Tillåt att baresip väljs som standardtelefonapp (välj inte om du också använder telefonitjänster) - Stöd för både CALL- och DIAL-åtgärder från andra appar - Åtgärdat fel vid inställning av kontots telefonileverantör - I meddelanden, stöd även för andra teckenuppsättningar än UTF-8 - Använd standardvärden för audio_buffer och jitter_buffer_delay - Nya översättningar till tjeckiska och ryska ================================================ FILE: fastlane/metadata/android/sv/changelogs/56.1.0.txt ================================================ - I samtalshistoriken, Visa missade (inte avvisade) samtal med gula upp-/nerpilar - Använd audio_buffer size 20-300 (ms) och jitter_buffer delay 0-20 (frames) - Undvik potentiell krasch relaterad till getNetworkInterfaces() - Förenklad "Om"-text och länk till "mer information" i Wiki - Nya översättningar till spanska, portugisiska (Brasilien) och tjeckiska ================================================ FILE: fastlane/metadata/android/sv/changelogs/56.2.0.txt ================================================ - Lagt till separata jitterbuffertfördröjningar för ljud och video - I samtalsinformation visas paket, förlorade paket och jitter som ?/? om informationen inte är tillgänglig - Uppströmsfix som nu tillåter escape-tecken i kontots -del - Ny tjeckisk översättning ================================================ FILE: fastlane/metadata/android/sv/changelogs/56.3.0.txt ================================================ - Korrigerade inställningar för jitterbuffert som förhindrade att inkommande ljud hördes - Avsluta samtal om ingen media har tagits emot inom 60 sekunder - Nya spanska översättningar ================================================ FILE: fastlane/metadata/android/sv/changelogs/57.0.0.txt ================================================ - Större omskrivning av inställningsimplementeringen genom att separera statiska och dynamiska inställningar - Återgå till ursprungliga jitterbuffertfördröjningar nu när libre-buggen är fixad - Konfigurationen kan inte laddas om i farten när opus-inställningarna ändras - Nya översättningar till spanska och portugisiska (Brasilien) ================================================ FILE: fastlane/metadata/android/sv/changelogs/57.1.0.txt ================================================ - Lagt till mer vertikal utfyllnad i kontokonfigurationen - Hack för att fixa felaktigt escaped URI när samtal eller uppringningsbegäran kommer från utsidan - Nya tjeckiska översättningar ================================================ FILE: fastlane/metadata/android/sv/changelogs/57.1.1.txt ================================================ - Korrigerat skrivfel i hjälptexten för standardtelefonappen ================================================ FILE: fastlane/metadata/android/sv/changelogs/57.2.0.txt ================================================ - Lagt till kontoinställningen Redirect Mode för att välja om begäran om vidarekoppling av samtal (3xx-svar) ska följas automatiskt eller om bekräftelse behöver att bli tillfrågad - Nya översättningar till spanska och portugisiska (Brasilien) ================================================ FILE: fastlane/metadata/android/sv/changelogs/58.0.0.txt ================================================ - Lagt till konfigurationsalternativ för konto för tillförlitliga provisoriska svar - Lagt till alternativet Tone Country Audio Configuration (tack till Robert Averbeck för att han tillhandahöll tonfilerna) - Fixat AudioManager-relaterat SDK-versionstest - Nya översättningar till spanska, portugisiska och portugisiska (Brasilien) - Uppströms buggfixar och förbättringar ================================================ FILE: fastlane/metadata/android/sv/changelogs/59.0.0.txt ================================================ - Riktar sig nu mot API-nivå 34 (Android 14) - Lagt till stöd för Codec2 ljudkodek - Inspelning av nästa samtal rensas inte när samtalet avslutas - Inkluderade även Dark Theme-inställning i backup-fil ================================================ FILE: fastlane/metadata/android/sv/changelogs/59.0.1.txt ================================================ - Riktar sig nu mot API-nivå 34 (Android 14) - Lagt till stöd för Codec2 ljudkodek - Inspelning av nästa samtal rensas inte när samtalet avslutas - Inkluderade även Dark Theme-inställning i backup-fil ================================================ FILE: fastlane/metadata/android/sv/changelogs/59.0.2.txt ================================================ - Riktar sig nu mot API-nivå 34 (Android 14) - Lagt till stöd för Codec2 ljudkodek - Inspelning av nästa samtal rensas inte när samtalet avslutas - Inkluderade även Dark Theme-inställning i backup-fil ================================================ FILE: fastlane/metadata/android/sv/changelogs/59.0.3.txt ================================================ - Riktar sig nu mot API-nivå 34 (Android 14) - Lagt till stöd för Codec2 ljudkodek - Inspelning av nästa samtal rensas inte när samtalet avslutas - Inkluderade även Dark Theme-inställning i backup-fil - Uppströmsfix av re-INVITE (samtal väntar/återupptas) ================================================ FILE: fastlane/metadata/android/sv/changelogs/59.0.4.txt ================================================ - Uppströmsfix av krasch relaterad till hantering av OPTIONS-begäran ================================================ FILE: fastlane/metadata/android/sv/changelogs/59.1.0.txt ================================================ - Stöd för favoritkontakter (stjärnmärkta) har lagts till ================================================ FILE: fastlane/metadata/android/sv/changelogs/59.2.0.txt ================================================ - Inställningen Starta automatiskt startar nu appen i stället för att meddela om den - Nya översättningar till spanska, portugisiska (Brasilien) och ryska ================================================ FILE: fastlane/metadata/android/sv/changelogs/59.3.0.txt ================================================ - Mindre buggfixar uppströms - Nya tyska översättningar ================================================ FILE: fastlane/metadata/android/sv/changelogs/59.4.0.txt ================================================ - Använda RTP-mottagningsläge för tråd - Nya översättningar till tjeckiska och portugisiska ================================================ FILE: fastlane/metadata/android/sv/changelogs/59.4.1.txt ================================================ - Uppdaterad undermodul för libbaresip-android (alla moduler hittas nu) ================================================ FILE: fastlane/metadata/android/sv/changelogs/59.4.2.txt ================================================ - Upstream nekade som standard inkommande meddelanden. Kan tillåtas igen genom att göra någon ändring i konfigurationen av ett konto. ================================================ FILE: fastlane/metadata/android/sv/changelogs/59.4.3.txt ================================================ - Tillåt mottagande av meddelanden efter att uppströms (som standard) nekat dem ================================================ FILE: fastlane/metadata/android/sv/changelogs/59.6.0.txt ================================================ - Lagt till information om baresip, CPU och Android i User-Agent-huvudet - Tidpunkten för det inspelade samtalet visas nu i accentfärg ================================================ FILE: fastlane/metadata/android/sv/changelogs/59.7.0.txt ================================================ - Uppströmsfix av bugg som kan orsaka att Baresip-appen slutar svara - Nya tjeckiska översättningar ================================================ FILE: fastlane/metadata/android/sv/changelogs/6.0.0.txt ================================================ - Flera förbättringar av användargränssnittet (mestadels chattrelaterade). ================================================ FILE: fastlane/metadata/android/sv/changelogs/6.0.1.txt ================================================ - Återuppta baresip app till den aktivitet som stoppades. - Lägg till exempelkontakt när baresip startas första gången. - Fixat visning av nytt meddelande i chatt- och meddelandelistan. ================================================ FILE: fastlane/metadata/android/sv/changelogs/6.0.2.txt ================================================ - Korrigerad krasch när STUN-servern definieras utan portnummer. ================================================ FILE: fastlane/metadata/android/sv/changelogs/6.1.0.txt ================================================ - Gå till chattfönstret när du klickar på meddelandeaviseringen. - Åtgärdat fel i samtalsöverföring. ================================================ FILE: fastlane/metadata/android/sv/changelogs/6.2.0.txt ================================================ - Uppgraderade målnivån för Android API till 28 (Android 9). - Flyttat viss icke-UI-funktionalitet från huvudaktiviteten till baresip-tjänsten. - Förenklad implementering av samtalsöverföring. - Uppdaterade bilderna för baresip-startprogrammet och statusfältet. - Avregistrerat UA när konto raderas. ================================================ FILE: fastlane/metadata/android/sv/changelogs/6.2.1.txt ================================================ - Använder ConnectivityManager NetworkCallbacks för att upptäcka när nätverks nätverksanslutning blir tillgänglig eller förloras. - Lagt till Android 8.1+ sätt att hantera skärmen. - Ställ in volymkontroller så att de gäller för aktuell ljudström (kanske inte alltid fungerar). - Fixade möjlig krasch när förstörd baresip-app startas igen. - Visar meddelande när kontot som ska skapas redan existerar. - Lagt till en notis i "Om"-texten om medievolym. ================================================ FILE: fastlane/metadata/android/sv/changelogs/6.3.0.txt ================================================ - Lagt till Debug Config-alternativ för att slå på och av adb logcat debug. - Lagt till möjlighet att exportera och importera konton till/från fil i Download-katalogen. ================================================ FILE: fastlane/metadata/android/sv/changelogs/6.3.1.txt ================================================ - Fixat och förbättrat implementeringen av DTMF Watcher. - Stängde av redigering av inkommande/utgående samtals URI när samtal pågår. - Förstörde användaragenten när kontot raderas. - Tog bort oanvänd KILL_BACKGROUND_PROCESSES-behörighet. ================================================ FILE: fastlane/metadata/android/sv/changelogs/6.4.0.txt ================================================ - Lagt till möjlighet att ange ett konto som standardkonto. - Visa röd meddelandeikon när det finns olästa meddelanden. - Fråga PIN för att släppa nyckelvakten när Callee URI klickas. - Fixat åtgärder för samtal, meddelande och samtalsöverföring när de initieras från meddelanden. - Fixat att skicka ett meddelande från kontakter eller samtalshistorik. - Använde SingleTask istället för SingleTop som startläge för huvudaktivitet. ================================================ FILE: fastlane/metadata/android/sv/changelogs/60.0.0.txt ================================================ - Använd Android 14-kompatibelt filformat för säkerhetskopiering. Säkerhetskopiera igen innan du uppgraderar Android till version 14. - Förbättrad trådhantering - Nya tyska översättningar ================================================ FILE: fastlane/metadata/android/sv/changelogs/60.1.0.txt ================================================ - Fixad radering av avatarfilen när avataren tas bort från kontakten eller när kontakten med avataren tas bort - Fixat sparande av kontots DTMF-läge - Nya översättningar av spanska, portugisiska och portugisiska (Brasilien) ================================================ FILE: fastlane/metadata/android/sv/changelogs/60.2.0.txt ================================================ - Lagt till DTMF-läge för "RTP eller SIP INFO i band - Ljudinställning för högtalartelefon har lagts till - Nya översättningar till tyska, portugisiska (Brasilien) och spanska ================================================ FILE: fastlane/metadata/android/sv/changelogs/60.3.0.txt ================================================ - Stöd för autentisering med SHA-256 digest har lagts till - Inställningarna för Verifiera servercertifikat kan nu ändras utan omstart - Nya översättningar till tjeckiska, tyska, spanska och portugisiska (Brasilien) - Fixade format för vissa strängar på vissa språk ================================================ FILE: fastlane/metadata/android/sv/changelogs/60.4.0.txt ================================================ - Korrekt hantering av överföringsbegäran när det ursprungliga samtalet redan är avslutat - Korrigerad hantering av stateless TLS SIP-begäranden - Nya översättningar till portugisiska, portugisiska (Brasilien), spanska, franska, ryska, engelska, finska, svenska, tyska och tjeckiska ================================================ FILE: fastlane/metadata/android/sv/changelogs/60.4.1.txt ================================================ - SRTP-relaterad säkerhetsfix från uppströms - Nya tjeckiska översättningar ================================================ FILE: fastlane/metadata/android/sv/changelogs/60.4.2.txt ================================================ - Fast samtalsinspelning - Skicka inte "180 Ringing"-svar om inkommande samtal avvisas automatiskt - Ta även bort %20 (mellanslag) från URI för tel-samtal - Nya tyska översättningar ================================================ FILE: fastlane/metadata/android/sv/changelogs/61.0.0.txt ================================================ - Inställningen User Agent har lagts till och kan användas för att ange värdet för User-Agent-fältet i SIP-begäran/-svaret - Tillåt , och #-tecken i telefonnumret (till exempel när du ringer Teams) - Använd accentfärg i stället för fetstil i kolumnen Samtalshistorik om ett samtal i den samtalsraden har spelats in - Inkludera inte uuid i backupfilen (förhindrar att många baresip UAs har samma identitet) - Som standard inkluderas datum och tid i backupfilens namn - Nya översättningar till spanska och portugisiska (Brasilien) ================================================ FILE: fastlane/metadata/android/sv/changelogs/61.0.1.txt ================================================ - Om DTMF är aktiverat, behåll fokus när skärmen ändrar orientering - Ändrade kontaktens URI för "The Test Call" till "sip:thetestcall@sip2sip.info - När du visar URI i vänlig form, visa URI-parametrar utom ';transport=udp' - Tog bort suffixet ".bs" från backupfilens namn - Nya översättningar (ryska) ================================================ FILE: fastlane/metadata/android/sv/changelogs/61.1.0.txt ================================================ - Ljudinställningen Microphone Gain har lagts till - Lagt till huvudmenyalternativet Logcat - Nya översättningar till tjeckiska, norska (bokmål), tyska, portugisiska (Brasilien) och spanska ================================================ FILE: fastlane/metadata/android/sv/changelogs/62.0.0.txt ================================================ - Om tillgängligt, använd akustisk ekoreducering, automatisk förstärkningskontroll och Brusreducerare som tillhandahålls av enheten - Android 9 (API-nivå 28) är nu den version som stöds minst ================================================ FILE: fastlane/metadata/android/sv/changelogs/62.1.0.txt ================================================ - Välj bort kant-till-kant-implementering för Android 15-kompatibilitet - Undvik krascher i BootCompletedReceiver och vid kontroll av utgående proxy-URI - Nya översättningar till finska, bulgariska, japanska, portugisiska (Brasilien), spanska, tjeckiska och polska översättningar ================================================ FILE: fastlane/metadata/android/sv/changelogs/63.0.0.txt ================================================ - Nytt stöd för kant-till-kant-visning av innehåll för kompatibilitet med Android 15 - Rundade hörn i dialogrutan för varningar - Visa inte automatiskt mjukt tangentbord när konfigurationen ändras - Lagt till nytt språk (tamil) ================================================ FILE: fastlane/metadata/android/sv/changelogs/63.1.0.txt ================================================ - Förhindra att mjukt tangentbord döljer inmatningstextfältet i vissa aktiviteter - Fixat typnummer för nyttolast för telefonhändelse - Förhindra krasch när backupfilen är stor - Nya översättningar (tyska) ================================================ FILE: fastlane/metadata/android/sv/changelogs/63.2.0.txt ================================================ - Använd akustisk ekoavstängning endast på inspelningssessionen - Gör inte backup av inspelningar - Nya översättningar till rumänska, portugisiska (Brasilien), tyska och tjeckiska ================================================ FILE: fastlane/metadata/android/sv/changelogs/63.2.1.txt ================================================ - Förbättrad inställning av ljudeffekter ================================================ FILE: fastlane/metadata/android/sv/changelogs/63.2.2.txt ================================================ - Installerade om spelaren AEC ================================================ FILE: fastlane/metadata/android/sv/changelogs/63.2.3.txt ================================================ - Tillåt användning av mjukvaru-AEC även om hårdvaru-AEC skulle vara tillgänglig ================================================ FILE: fastlane/metadata/android/sv/changelogs/63.2.5.txt ================================================ - Varning om AEC för hårdvara inte är tillgängligt när AEC för mjukvara är inaktiverat i ljudinställningarna - Nya översättningar (portugisiska Brasilien) ================================================ FILE: fastlane/metadata/android/sv/changelogs/63.3.0.txt ================================================ - Tillåt *- och #-tecken i telefonnummer - Nya översättningar (portugisiska (Brasilien)) ================================================ FILE: fastlane/metadata/android/sv/changelogs/64.0.0.txt ================================================ - Migrerat användargränssnitt till Jetpack Compose (kan innehålla buggar) - Försök alltid använda Acoustic Echo Canceler i hårdvara om det finns tillgängligt ================================================ FILE: fastlane/metadata/android/sv/changelogs/64.1.0.txt ================================================ - Flera korrigeringar och förbättringar relaterade till användargränssnittet - Nya översättningar (portugisiska (Brasilien)) ================================================ FILE: fastlane/metadata/android/sv/changelogs/64.2.0.txt ================================================ - Vertikala rullningslister har lagts till i aktiviteterna Inställningar, Konto och Ljud - Fixat hantering av URI-förslag för samtal och överföring - Förbättringar av dialogstil - Nya översättningar (portugisiska) ================================================ FILE: fastlane/metadata/android/sv/changelogs/64.3.0.txt ================================================ - Fixad hantering av DTMF backspace-tecken - Förbättringar i användargränssnittet för chatt och kontakt - Använd bakgrundsfärg även i systemfält ================================================ FILE: fastlane/metadata/android/sv/changelogs/64.3.1.txt ================================================ - Fixad hantering av DTMF backspace-tecken - Förbättringar i användargränssnittet för chatt och kontakt - Använd bakgrundsfärg även i systemfält - Fast krasch i kontoaktivitet när inga konton ================================================ FILE: fastlane/metadata/android/sv/changelogs/65.0.0.txt ================================================ - Slutförd konvertering av användargränssnittet till Jetpack Compose - Fixat inställningen Verifiera servercertifikat - Nya portugisiska översättningar ================================================ FILE: fastlane/metadata/android/sv/changelogs/65.1.0.txt ================================================ - Återställd svep vänster/höger för att växla mellan konton - Fixar och förbättringar i användargränssnittet för Konto och Inställningar ================================================ FILE: fastlane/metadata/android/sv/changelogs/65.2.0.txt ================================================ - Förbättrad utseende och implementering av pull-to-register - Se till att bluetoothreceiver är registrerad innan du avregistrerar den - Fixad back-press-åtgärd för API < 33-kompatibilitet ================================================ FILE: fastlane/metadata/android/sv/changelogs/65.2.1.txt ================================================ - Förbättrad utseende och implementering av pull-to-register - Se till att bluetoothreceiver är registrerad innan du avregistrerar den - Fixad back-press-åtgärd för API < 33-kompatibilitet - Fixad ZRTP-verifieringsfråga ================================================ FILE: fastlane/metadata/android/sv/changelogs/65.3.0.txt ================================================ - Lagt till inställning för ringsignal - Förbättrat utseende och implementering av pull-to-register - Se till att bluetoothreceiver är registrerad innan du avregistrerar den - Fixat back-tryck-åtgärd för API < 33-kompatibilitet - Fixat ZRTP-verifieringsfråga ================================================ FILE: fastlane/metadata/android/sv/changelogs/66.0.0.txt ================================================ - Använd switchar istället för kryssrutor i alla aktiviteter - Varning vid start om Acoustic Echo Cancellation för nätverk eller hårdvara inte är tillgänglig - Åtgärdat fel som orsakade trunkering av uppringt telefonnummer - Undvik krasch relaterad till att använda baresip som uppringningsapp ================================================ FILE: fastlane/metadata/android/sv/changelogs/66.1.0.txt ================================================ - Lagt till kontoinställningar för numeriskt tangentbord - Lagt till verktygstips till ikonerna i huvudaktivitetens övre appfält - Visa kontots fullständiga SIP URI på kontosidan - Nya översättningar (japanska och tamilska) ================================================ FILE: fastlane/metadata/android/sv/changelogs/66.1.1.txt ================================================ - Lagt till kontoinställningar för numeriskt tangentbord - Lagt till verktygstips till ikonerna i huvudaktivitetens övre appfält - Visa kontots fullständiga SIP URI på kontosidan - Återställer osänt nytt meddelande efter paus/återupptagning - Nya översättningar (japanska och tamilska) ================================================ FILE: fastlane/metadata/android/sv/changelogs/66.1.2.txt ================================================ - Lagt till kontoinställningar för numeriskt tangentbord - Lagt till verktygstips till ikonerna i huvudaktivitetens övre appfält - Visa kontots fullständiga SIP URI på kontosidan - Återställde osänt nytt meddelande efter paus/återupptagning - I chattaktivitet, fixat att lägga till chattkollega i kontakter - Nya översättningar (japanska och tamilska) ================================================ FILE: fastlane/metadata/android/sv/changelogs/66.1.3.txt ================================================ - Lagt till kontoinställningar för numeriskt tangentbord - Lagt till verktygstips till ikonerna i huvudaktivitetens övre appfält - Visa kontots fullständiga SIP URI på kontosidan - Återställde osänt nytt meddelande efter paus/återupptagning - I chattaktivitet, fixat att lägga till chattkollega till kontakt - Lagt till avbryt-knapp för att avbryta utgående samtal - Fixat visning av förslag på mål för överföring av samtal - Nya översättningar (japanska och tamilska) ================================================ FILE: fastlane/metadata/android/sv/changelogs/66.1.4.txt ================================================ - Flera inställningsrelaterade korrigeringar - Förbättrad layout för dialogrutan för överföring - Nya översättningar (tyska) ================================================ FILE: fastlane/metadata/android/sv/changelogs/66.1.5.txt ================================================ - Fixad initiering av konto från nätverk ================================================ FILE: fastlane/metadata/android/sv/changelogs/66.1.6.txt ================================================ - Använd volymknappar för ringsignalvolym när samtal kommer in och för samtalsvolym under samtal - Använd toasts för no network och no hw aec notiser ================================================ FILE: fastlane/metadata/android/sv/changelogs/67.0.0.txt ================================================ - Migrerade baresip-appen från aktiviteter till Jetpack Compose-navigering - Förbättrad hantering av samtal och meddelanden som kommer när baresip inte är synlig - Några andra mindre förbättringar av användargränssnittet - Uppströmsfix av Refer-To-rubrikernas Till/Från-taggar ================================================ FILE: fastlane/metadata/android/sv/changelogs/67.0.1.txt ================================================ - Stoppa samtalstimern kronometer när samtalet är avslutat - Lägg till mikrofon i statusmeddelande endast när inspelningstillstånd har beviljats - Nya översättningar (tamil) ================================================ FILE: fastlane/metadata/android/sv/changelogs/67.0.2.txt ================================================ - Buggfix för ljudinställning för standardvolym för samtal - Förbättring och buggfix för säkerhetskopiering/återställning - Nya översättningar (tyska) ================================================ FILE: fastlane/metadata/android/sv/changelogs/67.0.3.txt ================================================ - Fixar och förbättringar på skärmen för konto och inställningar ================================================ FILE: fastlane/metadata/android/sv/changelogs/67.1.0.txt ================================================ - Använd den blå nedåtpilen i samtalshistoriken för att ange att samtalet har besvarats någon annanstans - Lagt till hjälptexter vid klick på pilikonerna för samtalsinformation ================================================ FILE: fastlane/metadata/android/sv/changelogs/67.2.0.txt ================================================ - Lagt till Colorblind-inställning som för närvarande påverkar ikoner för registreringsstatus - Förbättrade namn på meddelandekanaler ================================================ FILE: fastlane/metadata/android/sv/changelogs/68.0.0.txt ================================================ - Riktar sig mot Android 16 API-nivå 36 - Lagt till inställning för närhetsavkänning - Fast inställning för kontakter Båda värdena - Nya översättningar (tjeckiska) ================================================ FILE: fastlane/metadata/android/sv/changelogs/68.1.0.txt ================================================ - Förbättrad visning av tid för samtalshistorik - Ändrad bakgrundsfärg på statusikoner för färgblinda - Fixad uppdatering av statusmeddelanden - Åtgärdat möjlig krasch relaterad till kontoinställning för registreringsintervall ================================================ FILE: fastlane/metadata/android/sv/changelogs/68.1.1.txt ================================================ - Förbättrad visning av tid för samtalshistorik - Ändrad bakgrundsfärg på statusikoner för färgblinda - Åtgärdat möjlig krasch relaterad till kontoinställning för registreringsintervall - Fixad uppdatering av statusmeddelanden ================================================ FILE: fastlane/metadata/android/sv/changelogs/68.1.2.txt ================================================ - Förbättrad visning av tid för samtalshistorik - Ändrad bakgrundsfärg på statusikoner för färgblinda - Åtgärdat möjlig krasch relaterad till kontoinställning för registreringsintervall - Fixad uppdatering av statusmeddelanden ================================================ FILE: fastlane/metadata/android/sv/changelogs/7.0.0.txt ================================================ - Lagt till stöd för IPv6 (ej testat på grund av bristande tillgång till IPv6-nätverk). - Konfigurationsalternativet "Prefer IPv6" har lagts till. - Lagt till konfigurationsalternativet "Reset to Factory Default". - Vid Avsluta, se till att även baresip-aktiviteten stoppas. ================================================ FILE: fastlane/metadata/android/sv/changelogs/7.1.0.txt ================================================ - Konfigurationsalternativet "Standardvolym för samtal" har lagts till. ================================================ FILE: fastlane/metadata/android/sv/changelogs/7.1.1.txt ================================================ - Uppdatera kontospinnaren även när huvudaktiviteten återupptas utan åtgärd. ================================================ FILE: fastlane/metadata/android/sv/changelogs/7.1.2.txt ================================================ - Försökte se till att även kontospinnarvyn uppdateras när kontospinnaren uppdateras. ================================================ FILE: fastlane/metadata/android/sv/changelogs/7.2.0.txt ================================================ - Lagt till ny konfigurationsvariabel Listen Address som gör det möjligt att ange en IP-adress adress (eller alla adresser) och en port som baresip lyssnar på för SIP-begäranden. ================================================ FILE: fastlane/metadata/android/sv/changelogs/7.3.0.txt ================================================ - NAT-protokollet för kontomedia kan nu väljas mellan STUN, ICE eller inget. - Sätt kontostatus till gult omedelbart när kontots registrering är avstängd. ================================================ FILE: fastlane/metadata/android/sv/changelogs/7.4.0.txt ================================================ - Anpassade varningar till riktlinjer för materialdesign. - Förbättrad hantering av språksträngar. - Finsk språköversättning har lagts till som ett exempel. ================================================ FILE: fastlane/metadata/android/sv/changelogs/8.0.0.txt ================================================ - Lagt till stöd för ljudkodek G.722.1. - Stöd för WebRTC-baserad ekosläckning för ljud har lagts till (gäller inte Opus-codec). - Mindre ändringar av samtalsmeddelanden. ================================================ FILE: fastlane/metadata/android/sv/changelogs/8.0.1.txt ================================================ - Återställning av volymen till ursprunglig nivå om standardvolymen för samtal används har åtgärdats. ================================================ FILE: fastlane/metadata/android/sv/changelogs/8.0.2.txt ================================================ - Fixat tillägg av nytt konto. ================================================ FILE: fastlane/metadata/android/sv/changelogs/8.1.0.txt ================================================ - Hämta DNS-serveradresser dynamiskt från systemet om DNS-servrar inte anges i Konfiguration. - Om det bara finns ett konto, lägg till kontots domän till kontakt-URI som anges utan hostpart. ================================================ FILE: fastlane/metadata/android/sv/changelogs/8.2.0.txt ================================================ - Förbättrat innehåll och utseende på About-sidan. - Visar alltid det aktiva samtalet (om något) när appen startas om. - Ändrad placering av OK-knappen i varningsdialogen. ================================================ FILE: fastlane/metadata/android/sv/changelogs/8.3.0.txt ================================================ - Lade till Internet Low Bitrate Codec (iLBC). - Förbättrat val av codec för kontot. ================================================ FILE: fastlane/metadata/android/sv/changelogs/8.3.1.txt ================================================ - Ställ inte in ljudet på kommunikationsläge när samtalet är etablerat, eftersom det på vissa enheter ger 3-4 sekunders fördröjning av uppspelningen av ljudet. - Gjorde akustisk ekoreducering konfigurerbar. - Aktivera knappen för samtalsinformation när den blir synlig. - Använd standardtema för enheten när spinners visas. ================================================ FILE: fastlane/metadata/android/sv/changelogs/8.3.2.txt ================================================ - Om kontot nås från AoR spinner, gå direkt tillbaka till Main Aktivitet. - Använd HtmlCompat för att konvertera About HTML till text (bör fungera på alla Android API-nivåer). - Använd ringsignalens loopingfunktion på Android P+. ================================================ FILE: fastlane/metadata/android/sv/changelogs/8.3.3.txt ================================================ - Hämta DNS-servrar dynamiskt även när baresip startas för första gången. - Undvik möjlig krasch när du hämtar kontots ljudkodek. ================================================ FILE: fastlane/metadata/android/sv/changelogs/8.3.5.txt ================================================ - Omge kontakt-URI:er med < och > endast när kontakterna sparas i en fil. ================================================ FILE: fastlane/metadata/android/sv/changelogs/8.3.6.txt ================================================ - Tillåt ändring av kontaktnamn även när endast bokstävernas versaler skiljer sig åt. - När du återställer kontakter behöver du inte kräva att URI:er omges av vinkelparenteser. ================================================ FILE: fastlane/metadata/android/sv/changelogs/8.4.0.txt ================================================ - Ekoavstängning tillämpas nu även på samtal som använder Opus-codec. ================================================ FILE: fastlane/metadata/android/sv/changelogs/8.4.1.txt ================================================ - Förbättring av akustisk ekoundertryckning uppströms. - Nya översättningar: el, nb_NO, ro. ================================================ FILE: fastlane/metadata/android/sv/changelogs/8.4.2.txt ================================================ - Uppdatera DNS-servrar och registrera UA:er alltid när nätverket blir tillgängligt. - Fixat och uppdaterat några texter. ================================================ FILE: fastlane/metadata/android/sv/changelogs/8.4.3.txt ================================================ - Fixat visning av kontostatus när standardkonto ändras. ================================================ FILE: fastlane/metadata/android/sv/changelogs/8.4.4.txt ================================================ - Uppströmsfix i opus codec-implementering. - Fixar för volymkontrollström. - Fixat automatiskt avvisande av nytt inkommande samtal. - Mindre förbättringar av strängar. ================================================ FILE: fastlane/metadata/android/sv/changelogs/8.5.0.txt ================================================ - Preliminär användning av närhetssensor för att stänga av pekskärmen under samtal. ================================================ FILE: fastlane/metadata/android/sv/changelogs/8.5.1.txt ================================================ - Flyttad hantering av närhetssensorer från MainActivity till BaresipService. - Fixade krasch när samtal eller meddelande kommer in och MainActivity inte körs. ================================================ FILE: fastlane/metadata/android/sv/changelogs/8.5.2.txt ================================================ - Korrigerad kontroll av DNS-servrar för konfiguration. ================================================ FILE: fastlane/metadata/android/sv/changelogs/8.6.0.txt ================================================ - Lagt till stöd för SIP-registratorer som inte hanterar återanvändning av nonce på rätt sätt. ================================================ FILE: fastlane/metadata/android/sv/changelogs/9.0.0.txt ================================================ - Lagt till möjlighet att baresip-konfigurera ett TLS-certifikat och CA-certifikat. ================================================ FILE: fastlane/metadata/android/sv/changelogs/9.1.0.txt ================================================ - Avvisa inkommande samtal automatiskt om telefonen inte är i viloläge. - Undertryck ringsignalen för inkommande samtal om enheten är i läget "Stör ej". ================================================ FILE: fastlane/metadata/android/sv/changelogs/9.2.0.txt ================================================ - Lagt till stöd för Opus Forward Error Correction (FEC) via konfigurationsvariabeln Opus Konfigurationsvariabeln Förväntad paketförlust. - Försök att uppdatera dynamisk DNS-information om registreringen misslyckas på grund av felet "Ogiltigt argument". - Använd översättningsbar sträng i alla konfigurationsrelaterade varningsmeddelanden. ================================================ FILE: fastlane/metadata/android/sv/changelogs/9.3.0.txt ================================================ - Lagt till konfigurationsalternativet Svarsläge (Manuell eller Automatisk) för kontot. - Lagt till norska bokmålssträngar och ändringsloggar från Weblate. ================================================ FILE: fastlane/metadata/android/sv/changelogs/9.4.0.txt ================================================ - Lagt till menyalternativet "Starta om" för huvudaktivitet. - När registreringen misslyckas på grund av fel i DNS-uppslagningen, försök registrera en gång till efter uppdatering av DNS-servrar. - Små förbättringar av loggning. ================================================ FILE: fastlane/metadata/android/sv/full_description.txt ================================================ Detta är en baresip-baserad SIP User Agent-applikation för Android. För närvarande stöder baresip-appen röstsamtal, textmeddelanden, röstbrevlåda Message Waiting Indication samt blinda och uppvaktade samtalsöverföringar. Röst kan kodas med Opus, AMR, Codec2, G.729, G.722, G.722.1, G.726 eller PCMU/PCMA-codecs. Säkerhet uppnås via TLS- eller WSS SIP-signaltransport och ZRTP- eller (DTLS) SRTP-mediekapsling. Utvecklingen av baresip-appen motiveras av behovet av en säker SIP-baserad VoIP-användaragent med öppen källkod för Android som inte är beroende av proprietära push-notifikationstjänster från tredje part. Om du behöver videosamtal och har en enhet med Android version 9 eller nyare som stöder Camera2 API på hårdvarusupportnivå LEVEL3 eller FULL, kan du istället för den här applikationen installera dess systerapplikation baresip+. Källkoden finns tillgänglig på GitHub, där även problem kan rapporteras. ================================================ FILE: fastlane/metadata/android/sv/short_description.txt ================================================ VoIP User Agent-app för Android baserad på baresip SIP-bibliotek ================================================ FILE: fastlane/metadata/android/sv/title.txt ================================================ baresip ================================================ FILE: fastlane/metadata/android/zh-CN/full_description.txt ================================================ 这是一款基于 baresip 的 Android SIP 用户代理应用程序。 目前,baresip 应用支持语音通话、语音电话会议、文本消息、语音信箱消息等待指示以及盲转和协商转接。语音编码支持 Opus、AMR、Codec2、G.729、G.722、G.722.1 或 PCMU/PCMA 编解码器。通过 TLS 或 WSS SIP 信令传输以及 ZRTP 或(DTLS)SRTP 媒体封装实现安全保障。 开发 baresip 应用的初衷是打造一款安全、开源的 Android SIP VoIP 用户代理,且不依赖任何专有的第三方推送通知服务。 此应用程序可以在 Android 9 或更高版本的设备上运行。 如果您需要视频通话,可以安装其姊妹应用 baresip+ 来代替此应用。 源代码可在 GitHub 项目 的 master 分支中找到,您也可以在此报告问题。 ================================================ FILE: fastlane/metadata/android/zh-CN/short_description.txt ================================================ 基于 baresip SIP 库的 Android VoIP 用户代理应用 ================================================ FILE: fastlane/metadata/android/zh-CN/title.txt ================================================ baresip ================================================ FILE: gradle/libs.versions.toml ================================================ [versions] activityCompose = "1.13.0" coilCompose = "2.7.0" composeBom = "2026.03.01" coreKtx = "1.18.0" exifinterface = "1.4.2" fragmentKtx = "1.8.9" gradleVersion = "9.1.1" kotlin = "2.3.20" kotlinStdlibJdk8 = "2.3.10" kotlinSerializationPlugin = "2.3.20" kotlinxCoroutinesAndroid = "1.10.2" kotlinxSerializationJson = "1.11.0" lifecycleProcess = "2.10.0" lifecycleViewmodelKtx = "2.10.0" material = "1.13.0" media = "1.7.1" navigationCompose = "2.9.7" preferenceKtx = "1.2.1" ui = "1.10.6" foundationAndroid = "1.10.6" runtimeLivedata = "1.10.6" composeMaterial3 = "1.4.0" navigationRuntimeAndroid = "2.9.7" composeMaterialIcons = "1.7.8" runtime = "1.10.6" [libraries] androidx-activity-compose = { module = "androidx.activity:activity-compose", version.ref = "activityCompose" } androidx-activity-ktx = { module = "androidx.activity:activity-ktx", version.ref = "activityCompose" } androidx-compose-bom = { module = "androidx.compose:compose-bom", version.ref = "composeBom" } androidx-core-ktx = { module = "androidx.core:core-ktx", version.ref = "coreKtx" } androidx-exifinterface = { module = "androidx.exifinterface:exifinterface", version.ref = "exifinterface" } androidx-fragment-ktx = { module = "androidx.fragment:fragment-ktx", version.ref = "fragmentKtx" } androidx-lifecycle-process = { group = "androidx.lifecycle", name = "lifecycle-process", version.ref = "lifecycleProcess" } androidx-lifecycle-viewmodel-ktx = { module = "androidx.lifecycle:lifecycle-viewmodel-ktx", version.ref = "lifecycleViewmodelKtx" } androidx-media = { module = "androidx.media:media", version.ref = "media" } androidx-navigation-compose = { module = "androidx.navigation:navigation-compose", version.ref = "navigationCompose" } androidx-preference-ktx = { module = "androidx.preference:preference-ktx", version.ref = "preferenceKtx" } androidx-ui = { module = "androidx.compose.ui:ui", version.ref = "ui" } coil-compose = { module = "io.coil-kt:coil-compose", version.ref = "coilCompose" } gradle = { module = "com.android.tools.build:gradle", version.ref = "gradleVersion" } kotlin-stdlib-jdk8 = { module = "org.jetbrains.kotlin:kotlin-stdlib-jdk8", version.ref = "kotlinStdlibJdk8" } kotlinx-coroutines-android = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-android", version.ref = "kotlinxCoroutinesAndroid" } kotlin-gradle-plugin = { module = "org.jetbrains.kotlin:kotlin-gradle-plugin", version.ref = "kotlin" } kotlinx-serialization-json = { module = "org.jetbrains.kotlinx:kotlinx-serialization-json", version.ref = "kotlinxSerializationJson" } material = { module = "com.google.android.material:material", version.ref = "material" } androidx-foundation-android = { group = "androidx.compose.foundation", name = "foundation-android", version.ref = "foundationAndroid" } androidx-runtime-livedata = { group = "androidx.compose.runtime", name = "runtime-livedata", version.ref = "runtimeLivedata" } androidx-compose-material3 = { group = "androidx.compose.material3", name = "material3", version.ref = "composeMaterial3" } androidx-navigation-runtime-android = { group = "androidx.navigation", name = "navigation-runtime-android", version.ref = "navigationRuntimeAndroid" } androidx-compose-material-icons-core = { group = "androidx.compose.material", name = "material-icons-core", version.ref = "composeMaterialIcons" } androidx-compose-material-icons-extended = { group = "androidx.compose.material", name = "material-icons-extended", version.ref = "composeMaterialIcons" } androidx-compose-runtime = { group = "androidx.compose.runtime", name = "runtime", version.ref = "runtime" } [plugins] compose-compiler = { id = "org.jetbrains.kotlin.plugin.compose", version.ref = "kotlin" } kotlin-serialization = { id = "org.jetbrains.kotlin.plugin.serialization", version.ref = "kotlinSerializationPlugin" } ================================================ FILE: gradle/wrapper/gradle-wrapper.properties ================================================ #Mon Jan 30 05:45:34 EET 2023 distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists distributionUrl=https\://services.gradle.org/distributions/gradle-9.4.0-bin.zip zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists ================================================ FILE: gradle.properties ================================================ android.enableJetifier=false android.nonFinalResIds=true android.useAndroidX=true android.nonTransitiveRClass=true android.uniquePackageNames=false android.dependency.useConstraints=true android.r8.strictFullModeForKeepRules=false android.generateSyncIssueWhenLibraryConstraintsAreEnabled=false kotlin.code.style=official org.gradle.jvmargs=-Xmx2G -Dfile.encoding=UTF-8 ================================================ FILE: gradlew ================================================ #!/usr/bin/env sh # # Copyright 2015 the original author or authors. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. # ############################################################################## ## ## Gradle start up script for UN*X ## ############################################################################## # Attempt to set APP_HOME # Resolve links: $0 may be a link PRG="$0" # Need this for relative symlinks. while [ -h "$PRG" ] ; do ls=`ls -ld "$PRG"` link=`expr "$ls" : '.*-> \(.*\)$'` if expr "$link" : '/.*' > /dev/null; then PRG="$link" else PRG=`dirname "$PRG"`"/$link" fi done SAVED="`pwd`" cd "`dirname \"$PRG\"`/" >/dev/null APP_HOME="`pwd -P`" cd "$SAVED" >/dev/null APP_NAME="Gradle" APP_BASE_NAME=`basename "$0"` # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' # Use the maximum available, or set MAX_FD != -1 to use that value. MAX_FD="maximum" warn () { echo "$*" } die () { echo echo "$*" echo exit 1 } # OS specific support (must be 'true' or 'false'). cygwin=false msys=false darwin=false nonstop=false case "`uname`" in CYGWIN* ) cygwin=true ;; Darwin* ) darwin=true ;; MINGW* ) msys=true ;; NONSTOP* ) nonstop=true ;; esac CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar # Determine the Java command to use to start the JVM. if [ -n "$JAVA_HOME" ] ; then if [ -x "$JAVA_HOME/jre/sh/java" ] ; then # IBM's JDK on AIX uses strange locations for the executables JAVACMD="$JAVA_HOME/jre/sh/java" else JAVACMD="$JAVA_HOME/bin/java" fi if [ ! -x "$JAVACMD" ] ; then die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME Please set the JAVA_HOME variable in your environment to match the location of your Java installation." fi else JAVACMD="java" which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. Please set the JAVA_HOME variable in your environment to match the location of your Java installation." fi # Increase the maximum file descriptors if we can. if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then MAX_FD_LIMIT=`ulimit -H -n` if [ $? -eq 0 ] ; then if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then MAX_FD="$MAX_FD_LIMIT" fi ulimit -n $MAX_FD if [ $? -ne 0 ] ; then warn "Could not set maximum file descriptor limit: $MAX_FD" fi else warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" fi fi # For Darwin, add options to specify how the application appears in the dock if $darwin; then GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" fi # For Cygwin, switch paths to Windows format before running java if $cygwin ; then APP_HOME=`cygpath --path --mixed "$APP_HOME"` CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` JAVACMD=`cygpath --unix "$JAVACMD"` # We build the pattern for arguments to be converted via cygpath ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` SEP="" for dir in $ROOTDIRSRAW ; do ROOTDIRS="$ROOTDIRS$SEP$dir" SEP="|" done OURCYGPATTERN="(^($ROOTDIRS))" # Add a user-defined pattern to the cygpath arguments if [ "$GRADLE_CYGPATTERN" != "" ] ; then OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" fi # Now convert the arguments - kludge to limit ourselves to /bin/sh i=0 for arg in "$@" ; do CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` else eval `echo args$i`="\"$arg\"" fi i=$((i+1)) done case $i in (0) set -- ;; (1) set -- "$args0" ;; (2) set -- "$args0" "$args1" ;; (3) set -- "$args0" "$args1" "$args2" ;; (4) set -- "$args0" "$args1" "$args2" "$args3" ;; (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; esac fi # Escape application args save () { for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done echo " " } APP_ARGS=$(save "$@") # Collect all arguments for the java command, following the shell quoting and substitution rules eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" # by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then cd "$(dirname "$0")" fi exec "$JAVACMD" "$@" ================================================ FILE: gradlew.bat ================================================ @rem @rem Copyright 2015 the original author or authors. @rem @rem Licensed under the Apache License, Version 2.0 (the "License"); @rem you may not use this file except in compliance with the License. @rem You may obtain a copy of the License at @rem @rem http://www.apache.org/licenses/LICENSE-2.0 @rem @rem Unless required by applicable law or agreed to in writing, software @rem distributed under the License is distributed on an "AS IS" BASIS, @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. @rem See the License for the specific language governing permissions and @rem limitations under the License. @rem @if "%DEBUG%" == "" @echo off @rem ########################################################################## @rem @rem Gradle startup script for Windows @rem @rem ########################################################################## @rem Set local scope for the variables with windows NT shell if "%OS%"=="Windows_NT" setlocal set DIRNAME=%~dp0 if "%DIRNAME%" == "" set DIRNAME=. set APP_BASE_NAME=%~n0 set APP_HOME=%DIRNAME% @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" @rem Find java.exe if defined JAVA_HOME goto findJavaFromJavaHome set JAVA_EXE=java.exe %JAVA_EXE% -version >NUL 2>&1 if "%ERRORLEVEL%" == "0" goto init echo. echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. echo. echo Please set the JAVA_HOME variable in your environment to match the echo location of your Java installation. goto fail :findJavaFromJavaHome set JAVA_HOME=%JAVA_HOME:"=% set JAVA_EXE=%JAVA_HOME%/bin/java.exe if exist "%JAVA_EXE%" goto init echo. echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% echo. echo Please set the JAVA_HOME variable in your environment to match the echo location of your Java installation. goto fail :init @rem Get command-line arguments, handling Windows variants if not "%OS%" == "Windows_NT" goto win9xME_args :win9xME_args @rem Slurp the command line arguments. set CMD_LINE_ARGS= set _SKIP=2 :win9xME_args_slurp if "x%~1" == "x" goto execute set CMD_LINE_ARGS=%* :execute @rem Setup the command line set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar @rem Execute Gradle "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% :end @rem End local scope for the variables with windows NT shell if "%ERRORLEVEL%"=="0" goto mainEnd :fail rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of rem the _cmd.exe /c_ return code! if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 exit /b 1 :mainEnd if "%OS%"=="Windows_NT" endlocal :omega ================================================ FILE: settings.gradle.kts ================================================ plugins { id("org.gradle.toolchains.foojay-resolver-convention") version "1.0.0" } include (":app")