commit ce52d1dbff995737784bb748df199f77c9848b5b Author: Juha Heinanen Date: Tue Feb 20 05:01:26 2018 +0200 first commit diff --git a/.gitignore b/.gitignore new file mode 100644 index 00000000..589dd22c --- /dev/null +++ b/.gitignore @@ -0,0 +1,13 @@ +/.idea +local.properties +.DS_Store +*~ +*.iml +.gradle +/local.properties +/.idea/workspace.xml +/.idea/libraries +.DS_Store +build +/captures +.externalNativeBuild diff --git a/.google/packaging.yaml b/.google/packaging.yaml new file mode 100644 index 00000000..c3f85aeb --- /dev/null +++ b/.google/packaging.yaml @@ -0,0 +1,7 @@ +status: PUBLISHED +technologies: [Android, NDK] +categories: [NDK] +languages: [C++, Java] +solutions: [Mobile] +github: googlesamples/android-ndk +license: apache2 diff --git a/.svnignore b/.svnignore new file mode 100644 index 00000000..efa9236c --- /dev/null +++ b/.svnignore @@ -0,0 +1,13 @@ +.idea +local.properties +.DS_Store +*~ +*.iml +.gradle +local.properties +.idea/workspace.xml +.idea/libraries +.DS_Store +build +captures +.externalNativeBuild diff --git a/README.md b/README.md new file mode 100644 index 00000000..2ff3c431 --- /dev/null +++ b/README.md @@ -0,0 +1,7 @@ +This is very basic Android Studio baresip project. + +Includes PCMU/PCMA, speex, and opus codecs as well as ZRTP media +encapsulation. + +The static libraries and include files in distribution directory have +been produced using https://github.com/alfredh/baresip-android. diff --git a/app/build.gradle b/app/build.gradle new file mode 100644 index 00000000..34e72448 --- /dev/null +++ b/app/build.gradle @@ -0,0 +1,34 @@ +apply plugin: 'com.android.application' + +android { + compileSdkVersion = 25 + + defaultConfig { + applicationId = 'com.tutpro.baresip' + minSdkVersion 21 + targetSdkVersion 23 + versionCode = 1 + versionName = '1.0' + ndk { + abiFilters 'armeabi-v7a' + } + } + buildTypes { + release { + minifyEnabled false + proguardFiles getDefaultProguardFile('proguard-android.txt'), + 'proguard-rules.pro' + } + } + externalNativeBuild { + cmake { + path 'src/main/cpp/CMakeLists.txt' + } + } +} + +dependencies { + compile fileTree(include: ['*.jar'], dir: 'libs') + compile 'com.android.support:appcompat-v7:25.2.0' + compile 'com.android.support.constraint:constraint-layout:1.0.1' +} diff --git a/app/proguard-rules.pro b/app/proguard-rules.pro new file mode 100644 index 00000000..b7420a70 --- /dev/null +++ b/app/proguard-rules.pro @@ -0,0 +1,17 @@ +# 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: + +# 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 *; +#} diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml new file mode 100644 index 00000000..30576d9c --- /dev/null +++ b/app/src/main/AndroidManifest.xml @@ -0,0 +1,108 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/assets/accounts b/app/src/main/assets/accounts new file mode 100644 index 00000000..49528ec9 --- /dev/null +++ b/app/src/main/assets/accounts @@ -0,0 +1,34 @@ +# +# SIP accounts - one account per line +# +# Displayname ;addr-params +# +# uri-params: +# ;transport={udp,tcp,tls} +# +# addr-params: +# ;answermode={manual,early,auto} +# ;audio_codecs=speex/16000,pcma,... +# ;auth_user=username +# ;mediaenc={srtp,srtp-mand,srtp-mandf,dtls_srtp,zrtp} +# ;medianat={stun,turn,ice} +# ;outbound="sip:primary.example.com;transport=tcp" +# ;outbound2=sip:secondary.example.com +# ;ptime={10,20,30,40,...} +# ;regint=3600 +# ;pubint=0 (publishing off) +# ;regq=0.5 +# ;rtpkeep={zero,stun,dyna,rtcp} +# ;sipnat={outbound} +# ;stunuser=STUN/TURN/ICE-username +# ;stunpass=STUN/TURN/ICE-password +# ;stunserver=stun:[user:pass]@host[:port] +# ;video_codecs=h264,h263,... +# +# Examples: +# +# +# +# +# +;auth_user=foo;outbound="sip:192.168.43.98:5060;transport=tcp";ptime=20;audio_codecs=OPUS/48000/2,speex/16000,speex/8000;video_codecs="";regint=600;pubint=0;sipnat=outbound diff --git a/app/src/main/assets/busy.wav b/app/src/main/assets/busy.wav new file mode 100644 index 00000000..9d0ffec0 Binary files /dev/null and b/app/src/main/assets/busy.wav differ diff --git a/app/src/main/assets/callwaiting.wav b/app/src/main/assets/callwaiting.wav new file mode 100644 index 00000000..c9bf9b20 Binary files /dev/null and b/app/src/main/assets/callwaiting.wav differ diff --git a/app/src/main/assets/config b/app/src/main/assets/config new file mode 100644 index 00000000..f501c607 --- /dev/null +++ b/app/src/main/assets/config @@ -0,0 +1,200 @@ +# +# baresip configuration +# + +#------------------------------------------------------------------------------ + +# Core +poll_method epoll # poll, select, epoll .. + +# SIP +sip_trans_bsize 128 +#sip_listen 0.0.0.0:5060 +#sip_certificate cert.pem + +# Call +call_local_timeout 120 +call_max_calls 4 + +# Audio +audio_player opensles,nil +audio_source opensles,nil +audio_alert opensles,nil +audio_srate 8000-48000 +audio_channels 1-2 +#ausrc_srate 48000 +#auplay_srate 48000 +#ausrc_channels 0 +#auplay_channels 0 +audio_level no + +# Video +#video_source v4l2,/dev/video0 +#video_display x11,nil +video_size 352x288 +video_bitrate 500000 +video_fps 25 +video_fullscreen yes + +# AVT - Audio/Video Transport +rtp_tos 184 +#rtp_ports 10000-20000 +#rtp_bandwidth 512-1024 # [kbit/s] +rtcp_enable yes +rtcp_mux no +jitter_buffer_delay 5-10 # frames +rtp_stats no +#rtp_timeout 60 + +# Network +#dns_server 10.0.0.1:53 +#net_interface ^ + +# BFCP +#bfcp_proto udp + +#------------------------------------------------------------------------------ +# Modules + +# UI Modules +#module stdio.so +#module cons.so +#module evdev.so +#module httpd.so + +# Audio codec Modules (in order) +module opus.so +#module silk.so +#module amr.so +#module g7221.so +#module g722.so +#module g726.so +module g711.so +#module gsm.so +#module l16.so +module speex.so +#module bv32.so +#module mpa.so +#module codec2.so +#module ilbc.so +#module isac.so + +# Audio filter Modules (in encoding order) +#module vumeter.so +#module sndfile.so +#module speex_aec.so +#module speex_pp.so +#module plc.so + +# Audio driver Modules +module opensles.so +#module jack.so +#module portaudio.so +#module aubridge.so +#module aufile.so + +# Video codec Modules (in order) +#module avcodec.so +#module vp8.so +#module vp9.so +#module h265.so + +# Video filter Modules (in encoding order) +#module selfview.so +#module snapshot.so +#module swscale.so +#module vidinfo.so + +# Video source modules +#module v4l.so +#module v4l2.so +#module v4l2_codec.so +#module x11grab.so +#module cairo.so +#module vidbridge.so + +# Video display modules +#module directfb.so +#module x11.so +#module sdl2.so +#module fakevideo.so + +# Audio/Video source modules +#module rst.so +#module gst1.so +#module gst_video1.so + +# Media NAT modules +module stun.so +module turn.so +module ice.so +#module natpmp.so + +# Media encryption modules +#module srtp.so +#module dtls_srtp.so +#module zrtp.so + + +#------------------------------------------------------------------------------ +# Temporary Modules (loaded then unloaded) + +module_tmp uuid.so +module_tmp account.so + + +#------------------------------------------------------------------------------ +# Application Modules + +module_app auloop.so +module_app contact.so +module_app debug_cmd.so +#module_app dtmfio.so +#module_app echo.so +#module_app gtk.so +module_app menu.so +#module_app mwi.so +#module_app natbd.so +#module_app presence.so +#module_app syslog.so +module_app vidloop.so + + +#------------------------------------------------------------------------------ +# Module parameters + + +cons_listen 0.0.0.0:5555 + +http_listen 0.0.0.0:8000 + +evdev_device /dev/input/event0 + +# Speex codec parameters +speex_quality 7 # 0-10 +speex_complexity 7 # 0-10 +speex_enhancement 0 # 0-1 +speex_mode_nb 3 # 1-6 +speex_mode_wb 6 # 1-6 +speex_vbr 0 # Variable Bit Rate 0-1 +speex_vad 0 # Voice Activity Detection 0-1 +speex_agc_level 8000 + +# Opus codec parameters +opus_bitrate 28000 # 6000-510000 +opus_cbr no +opus_inbandfec yes + +# Selfview +#video_selfview window # {window,pip} +#selfview_size 64x64 + +# ICE +ice_turn no +ice_debug no +ice_nomination regular # {regular,aggressive} +ice_mode full # {full,lite} + +# Menu +#redial_attempts 3 # Num or +#redial_delay 5 # Delay in seconds diff --git a/app/src/main/assets/contacts b/app/src/main/assets/contacts new file mode 100644 index 00000000..d884f5f1 --- /dev/null +++ b/app/src/main/assets/contacts @@ -0,0 +1,17 @@ +# +# SIP contacts +# +# Displayname ;addr-params +# +# addr-params: +# ;presence={none,p2p} +# ;access={allow,block} +# + +"Echo Server" +#"user" ;presence=p2p + + +# Access rules +#"Catch All" ;access=block +#"Good Friend" ;access=allow diff --git a/app/src/main/assets/error.wav b/app/src/main/assets/error.wav new file mode 100644 index 00000000..483daca4 Binary files /dev/null and b/app/src/main/assets/error.wav differ diff --git a/app/src/main/assets/message.wav b/app/src/main/assets/message.wav new file mode 100644 index 00000000..bd91094b Binary files /dev/null and b/app/src/main/assets/message.wav differ diff --git a/app/src/main/assets/notfound.wav b/app/src/main/assets/notfound.wav new file mode 100644 index 00000000..285c61da Binary files /dev/null and b/app/src/main/assets/notfound.wav differ diff --git a/app/src/main/assets/ring.wav b/app/src/main/assets/ring.wav new file mode 100644 index 00000000..09ec1bd8 Binary files /dev/null and b/app/src/main/assets/ring.wav differ diff --git a/app/src/main/assets/ringback.wav b/app/src/main/assets/ringback.wav new file mode 100644 index 00000000..d21e20ec Binary files /dev/null and b/app/src/main/assets/ringback.wav differ diff --git a/app/src/main/cpp/CMakeLists.txt b/app/src/main/cpp/CMakeLists.txt new file mode 100644 index 00000000..09479956 --- /dev/null +++ b/app/src/main/cpp/CMakeLists.txt @@ -0,0 +1,80 @@ + +# +# Copyright (C) The Android Open Source Project +# +# 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. +# + +cmake_minimum_required(VERSION 3.4.1) + +# configure import libs +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_rem STATIC IMPORTED) +set_target_properties(lib_rem PROPERTIES IMPORTED_LOCATION + ${distribution_DIR}/rem/lib/${ANDROID_ABI}/librem.a) + +add_library(lib_speex STATIC IMPORTED) +set_target_properties(lib_speex PROPERTIES IMPORTED_LOCATION + ${distribution_DIR}/speex/lib/${ANDROID_ABI}/libspeex.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_bn STATIC IMPORTED) +set_target_properties(lib_bn PROPERTIES IMPORTED_LOCATION + ${distribution_DIR}/bn/lib/${ANDROID_ABI}/libbn.a) + +add_library(lib_zrtp STATIC IMPORTED) +set_target_properties(lib_zrtp PROPERTIES IMPORTED_LOCATION + ${distribution_DIR}/zrtp/lib/${ANDROID_ABI}/libzrtp.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 baresip.c) + +target_include_directories(baresip PRIVATE + ${distribution_DIR}/openssl/include + ${distribution_DIR}/re/include + ${distribution_DIR}/rem/include + ${distribution_DIR}/baresip/include) + +target_link_libraries(baresip + android + OpenSLES + lib_baresip + lib_rem + lib_re + lib_ssl + lib_crypto + lib_speex + lib_opus + lib_zrtp + lib_bn + z + log) diff --git a/app/src/main/cpp/baresip.c b/app/src/main/cpp/baresip.c new file mode 100644 index 00000000..fae31b7b --- /dev/null +++ b/app/src/main/cpp/baresip.c @@ -0,0 +1,458 @@ +#include +#include +#include +#include +#include +#include +#include + +#define LOGD(...) \ + ((void)__android_log_print(ANDROID_LOG_DEBUG, "Baresip", __VA_ARGS__)) + +#define LOGI(...) \ + ((void)__android_log_print(ANDROID_LOG_INFO, "Baresip", __VA_ARGS__)) + +#define LOGW(...) \ + ((void)__android_log_print(ANDROID_LOG_WARN, "Baresip", __VA_ARGS__)) + +#define LOGE(...) \ + ((void)__android_log_print(ANDROID_LOG_ERROR, "Baresip", __VA_ARGS__)) + +typedef struct tick_context { + JavaVM *javaVM; + jclass jniHelperClz; + jobject jniHelperObj; + jclass mainActivityClz; + jobject mainActivityObj; + pthread_mutex_t lock; + int done; +} TickContext; + +TickContext g_ctx; + +static void signal_handler(int sig) +{ + static bool term = false; + + if (term) { + 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 *ua_event_reg_str(enum ua_event ev) +{ + switch (ev) { + case UA_EVENT_REGISTERING: return "registering"; + case UA_EVENT_REGISTER_OK: return "registered"; + case UA_EVENT_REGISTER_FAIL: return "registering failed"; + case UA_EVENT_UNREGISTERING: return "unregistering"; + default: return "?"; + } +} + +static void ua_event_handler(struct ua *ua, enum ua_event ev, + struct call *call, const char *prm, void *arg) +{ + const char *event; + char event_buf[256]; + char ua_buf[256]; + char call_buf[256]; + + LOGD("ua event (%s)\n", uag_event_str(ev)); + + switch (ev) { + case UA_EVENT_REGISTERING: + case UA_EVENT_UNREGISTERING: + case UA_EVENT_REGISTER_OK: + case UA_EVENT_REGISTER_FAIL: + re_snprintf(event_buf, sizeof event_buf, "%s", ua_event_reg_str(ev)); + break; + case UA_EVENT_CALL_RINGING: + re_snprintf(event_buf, sizeof event_buf, "%s", "call ringing"); + break; + case UA_EVENT_CALL_PROGRESS: + re_snprintf(event_buf, sizeof event_buf, "%s", "call progress"); + break; + case UA_EVENT_CALL_ESTABLISHED: + re_snprintf(event_buf, sizeof event_buf, "%s", "call established"); + break; + case UA_EVENT_CALL_INCOMING: + re_snprintf(event_buf, sizeof event_buf, "%s", "call incoming"); + break; + case UA_EVENT_CALL_CLOSED: + re_snprintf(event_buf, sizeof event_buf, "%s", "call closed"); + break; + case UA_EVENT_EXIT: + re_snprintf(event_buf, sizeof event_buf, "%s", "exit"); + break; + default: + re_snprintf(event_buf, sizeof event_buf, "%s", "unknown event"); + } + event = event_buf; + + TickContext *pctx = (TickContext*)(&g_ctx); + JavaVM *javaVM = pctx->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", res); + return; + } + } + jmethodID statusId = (*env)->GetMethodID(env, pctx->jniHelperClz, + "updateStatus", + "(Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;)V"); + sprintf(ua_buf, "%lu", (unsigned long)ua); + jstring javaUA = (*env)->NewStringUTF(env, ua_buf); + sprintf(call_buf, "%lu", (unsigned long)call); + jstring javaCall = (*env)->NewStringUTF(env, call_buf); + jstring javaEvent = (*env)->NewStringUTF(env, event); + LOGD("sending ua/call %s/%s event %s\n", ua_buf, call_buf, event); + (*env)->CallVoidMethod(env, pctx->jniHelperObj, statusId, javaEvent, javaUA, javaCall); + (*env)->DeleteLocalRef(env, javaUA); + (*env)->DeleteLocalRef(env, javaCall); + (*env)->DeleteLocalRef(env, javaEvent); +} + +#include + +static int pfd[2]; +static pthread_t loggingThread; + +static void *loggingFunction() { + 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); + + if (pthread_create(&loggingThread, 0, loggingFunction, 0) == -1) { + return -1; + } + + pthread_detach(loggingThread); + + return 0; +} + +JNIEXPORT jint JNICALL JNI_OnLoad(JavaVM* vm, void* reserved) { + JNIEnv* env; + + LOGD("Executing JNI_OnLoad\n"); + + memset(&g_ctx, 0, sizeof(g_ctx)); + + g_ctx.javaVM = vm; + if ((*vm)->GetEnv(vm, (void**)&env, JNI_VERSION_1_6) != JNI_OK) { + return JNI_ERR; // JNI version not supported. + } + + jclass clz = (*env)->FindClass(env, "com/tutpro/baresip/MainActivity"); + g_ctx.jniHelperClz = (*env)->NewGlobalRef(env, clz); + + jmethodID jniHelperCtor = (*env)->GetMethodID(env, g_ctx.jniHelperClz, + "", "()V"); + jobject handler = (*env)->NewObject(env, g_ctx.jniHelperClz, + jniHelperCtor); + g_ctx.jniHelperObj = (*env)->NewGlobalRef(env, handler); + + g_ctx.done = 0; + g_ctx.mainActivityObj = NULL; + return JNI_VERSION_1_6; +} + +JNIEXPORT void JNICALL +Java_com_tutpro_baresip_MainActivity_baresipStart(JNIEnv *env, jobject thiz, jstring javaPath) +{ + int err; + const char *path = (*env)->GetStringUTFChars(env, javaPath, 0); + struct le *le; + + runLoggingThread(); + + err = libre_init(); + if (err) + goto out; + + conf_path_set(path); + + err = conf_configure(); + if (err) { + LOGW("conf_configure() failed: (%d)\n", err); + goto out; + } + + err = baresip_init(conf_config(), false); + if (err) { + LOGW("baresip_init() failed (%d)\n", err); + goto out; + } + + play_set_path(baresip_player(), path); + + err = ua_init("baresip v" BARESIP_VERSION " (" ARCH "/" OS ")", + true, true, true, false); + if (err) { + LOGE("ua_init() failed (%d)\n", err); + goto out; + } + + uag_set_exit_handler(ua_exit_handler, NULL); + uag_event_register(ua_event_handler, NULL); + + err = conf_modules(); + if (err) { + LOGW("conf_modules() failed (%d)\n", err); + goto out; + } + + TickContext *pctx = (TickContext*)(&g_ctx); + JavaVM *javaVM = pctx->javaVM; + 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", res); + return; + } + } + + LOGD("Adding %u accounts", list_count(uag_list())); + char ua_buf[256]; + struct ua *ua; + for (le = list_head(uag_list()); le; le = le->next) { + ua = le->data; + sprintf(ua_buf, "%lu", (unsigned long)ua); + jstring javaUA = (*env)->NewStringUTF(env, ua_buf); + LOGD("adding account %s/%s\n", ua_aor(ua), ua_buf); + jmethodID accountId = (*env)->GetMethodID(env, pctx->jniHelperClz, "addAccount", + "(Ljava/lang/String;)V"); + (*env)->CallVoidMethod(env, pctx->jniHelperObj, accountId, javaUA); + (*env)->DeleteLocalRef(env, javaUA); + } + + LOGI("Running main loop\n"); + err = re_main(signal_handler); + +out: + + if (err) { + LOGE("error: (%d)\n", err); + ua_stop_all(true); + } + + LOGD("closing upon main loop exit"); + ua_close(); + conf_close(); + baresip_close(); + mod_close(); + libre_close(); + + // tmr_debug(); + // mem_debug(); + + return; +} + +JNIEXPORT void JNICALL +Java_com_tutpro_baresip_MainActivity_baresipStop(JNIEnv *env, jobject thiz) +{ + LOGD("closing upon stop"); + ua_stop_all(false); + ua_close(); + // conf_close(); + // baresip_close(); + // mod_close(); + // libre_close(); + + // tmr_debug(); + // mem_debug(); + + return; +} + +JNIEXPORT jstring JNICALL +Java_com_tutpro_baresip_MainActivity_ua_1aor(JNIEnv *env, jobject thiz, jstring javaUA) +{ + const char *native_ua = (*env)->GetStringUTFChars(env, javaUA, 0); + struct ua *ua = (struct ua *)strtoul(native_ua, NULL, 10); + + if (strlen(native_ua) > 0) + return (*env)->NewStringUTF(env, ua_aor(ua)); + else + return (*env)->NewStringUTF(env, ""); +} + +JNIEXPORT jboolean JNICALL +Java_com_tutpro_baresip_MainActivity_ua_1isregistered(JNIEnv *env, jobject thiz, jlong ua_ptr) +{ + struct ua *ua = (struct ua *)ua_ptr; + bool result; + + result = ua_isregistered(ua); + if (ua == NULL) { + LOGD("ua_ptr is null\n"); + } else { + LOGD("ua_ptr is NOT null\n"); + } + return result; +} + +JNIEXPORT void JNICALL +Java_com_tutpro_baresip_MainActivity_ua_1current_1set(JNIEnv *env, jobject thiz, + jstring javaAoR) +{ + struct ua *new_current_ua; + const char *native_aor = (*env)->GetStringUTFChars(env, javaAoR, 0); + + LOGD("running ua_current_set on %s\n", native_aor); + new_current_ua = uag_find_aor(native_aor); + uag_current_set(new_current_ua); + (*env)->ReleaseStringUTFChars(env, javaAoR, native_aor); + return; +} + +JNIEXPORT jstring JNICALL +Java_com_tutpro_baresip_MainActivity_ua_1current(JNIEnv *env, jobject thiz) +{ + struct ua *current_ua = uag_current(); + char ua_buf[256]; + + if (current_ua == NULL) + ua_buf[0] = '\0'; + else + sprintf(ua_buf, "%lu", (unsigned long)current_ua); + return (*env)->NewStringUTF(env, ua_buf); +} + +JNIEXPORT jstring JNICALL +Java_com_tutpro_baresip_MainActivity_ua_1prev_1call(JNIEnv *env, jobject thiz, jstring javaUA) +{ + const char *native_ua = (*env)->GetStringUTFChars(env, javaUA, 0); + struct ua *ua = (struct ua *)strtoul(native_ua, NULL, 10); + (*env)->ReleaseStringUTFChars(env, javaUA, native_ua); + + struct call *call = ua_prev_call(ua); + char call_buf[256]; + if (call == NULL) + call_buf[0] = '\0'; + else + sprintf(call_buf, "%lu", (unsigned long)call); + return (*env)->NewStringUTF(env, call_buf); +} + +JNIEXPORT jstring JNICALL +Java_com_tutpro_baresip_MainActivity_ua_1call(JNIEnv *env, jobject thiz, jstring javaUA) +{ + const char *native_ua = (*env)->GetStringUTFChars(env, javaUA, 0); + struct ua *ua = (struct ua *)strtoul(native_ua, NULL, 10); + (*env)->ReleaseStringUTFChars(env, javaUA, native_ua); + + struct call *call = ua_call(ua); + char call_buf[256]; + sprintf(call_buf, "%lu", (unsigned long)call); + return (*env)->NewStringUTF(env, call_buf); +} + +JNIEXPORT jstring JNICALL +Java_com_tutpro_baresip_MainActivity_call_1peeruri(JNIEnv *env, jobject thiz, jstring javaCall) +{ + const char *native_call = (*env)->GetStringUTFChars(env, javaCall, 0); + struct call *call; + + call = (struct call *)strtoul(native_call, NULL, 10); + (*env)->ReleaseStringUTFChars(env, javaCall, native_call); + return (*env)->NewStringUTF(env, call_peeruri(call)); +} + +JNIEXPORT jstring JNICALL +Java_com_tutpro_baresip_MainActivity_ua_1connect(JNIEnv *env, jobject thiz, + jstring uri) { + struct call *call; + struct ua *ua; + int err; + const char *native_uri = (*env)->GetStringUTFChars(env, uri, 0); + char call_buf[256]; + + LOGD("connecting to %s\n", native_uri); + ua = uag_current(); + if (ua != NULL) { + err = ua_connect(ua, &call, NULL, native_uri, NULL, VIDMODE_ON); + if (err) { + LOGW("connecting to %s failed with error %d\n", native_uri, err); + call_buf[0] = '\0'; + } else { + sprintf(call_buf, "%lu", (unsigned long)call); + } + } else { + LOGE("no current ua\n"); + call_buf[0] = '\0'; + } + (*env)->ReleaseStringUTFChars(env, uri, native_uri); + return (*env)->NewStringUTF(env, call_buf); +} + + +JNIEXPORT void JNICALL +Java_com_tutpro_baresip_MainActivity_ua_1answer(JNIEnv *env, jobject thiz, + jstring javaUA, jstring javaCall) { + const char *native_ua = (*env)->GetStringUTFChars(env, javaUA, 0); + const char *native_call = (*env)->GetStringUTFChars(env, javaCall, 0); + LOGD("answering call %s/%s\n", native_ua, native_call); + struct ua *ua = (struct ua *)strtoul(native_ua, NULL, 10); + struct call *call = (struct call *)strtoul(native_call, NULL, 10); + ua_answer(ua, call); + (*env)->ReleaseStringUTFChars(env, javaUA, native_ua); + (*env)->ReleaseStringUTFChars(env, javaCall, native_call); + + return; +} + +JNIEXPORT void JNICALL +Java_com_tutpro_baresip_MainActivity_ua_1hangup(JNIEnv *env, jobject thiz, + jstring javaUA, jstring javaCall, jint code, + jstring reason) { + const char *native_ua = (*env)->GetStringUTFChars(env, javaUA, 0); + const char *native_call = (*env)->GetStringUTFChars(env, javaCall, 0); + struct ua *ua = (struct ua *)strtoul(native_ua, NULL, 10); + struct call *call = (struct call *)strtoul(native_call, NULL, 10); + const uint16_t native_code = code; + const char *native_reason = (*env)->GetStringUTFChars(env, reason, 0); + LOGD("hanging up call %s/%s\n", native_ua, native_call); + // ua_hangup(ua, call, native_code, native_reason); + ua_hangup(uag_current(), NULL, 0, NULL); + (*env)->ReleaseStringUTFChars(env, javaUA, native_ua); + (*env)->ReleaseStringUTFChars(env, javaCall, native_call); + (*env)->ReleaseStringUTFChars(env, reason, native_reason); + return; +} diff --git a/app/src/main/java/com/tutpro/baresip/AboutActivity.java b/app/src/main/java/com/tutpro/baresip/AboutActivity.java new file mode 100644 index 00000000..dc667ec9 --- /dev/null +++ b/app/src/main/java/com/tutpro/baresip/AboutActivity.java @@ -0,0 +1,21 @@ +package com.tutpro.baresip; + +import android.os.Bundle; +import android.support.v7.app.AppCompatActivity; +import android.widget.TextView; + +public class AboutActivity extends AppCompatActivity { + + private TextView aboutView; + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setContentView(R.layout.activity_about); + aboutView = (TextView)findViewById(R.id.aboutText); + aboutView.setEnabled(false); + aboutView.setText(getString(R.string.aboutText)); + } + +} + diff --git a/app/src/main/java/com/tutpro/baresip/Account.java b/app/src/main/java/com/tutpro/baresip/Account.java new file mode 100644 index 00000000..4030da99 --- /dev/null +++ b/app/src/main/java/com/tutpro/baresip/Account.java @@ -0,0 +1,34 @@ +package com.tutpro.baresip; + +public class Account { + + private String ua, aor, status; + private int status_image; + + public Account(String ua, String aor) { + this.ua = ua; + this.aor = aor; + this.status = ""; + this.status_image = 0; + } + + public void setStatusImage(int status_image) { + this.status_image = status_image; + } + public int getStatusImage() { + return status_image; + } + + public String getUA() { + return ua; + } + public String getAoR() { + return aor; + } + + public void setStatus(String status) { this.status = status; } + public String getStatus() { + return status; + } + +} diff --git a/app/src/main/java/com/tutpro/baresip/AccountSpinnerAdapter.java b/app/src/main/java/com/tutpro/baresip/AccountSpinnerAdapter.java new file mode 100644 index 00000000..7a5d8436 --- /dev/null +++ b/app/src/main/java/com/tutpro/baresip/AccountSpinnerAdapter.java @@ -0,0 +1,46 @@ +package com.tutpro.baresip; + +import android.content.Context; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.ArrayAdapter; +import android.widget.ImageView; +import android.widget.TextView; + +import java.util.ArrayList; + +public class AccountSpinnerAdapter extends ArrayAdapter { + + private ArrayList images; + private ArrayList aors; + private Context context; + + public AccountSpinnerAdapter(Context context, ArrayList aors, ArrayList images) { + super(context, android.R.layout.simple_spinner_item, images); + this.images = images; + this.aors = aors; + this.context = context; + } + + @Override + public View getDropDownView(int position, View convertView, ViewGroup parent) { + return getImageForPosition(position, convertView, parent); + } + + @Override + public View getView(int position, View convertView, ViewGroup parent) { + return getImageForPosition(position, convertView, parent); + } + + private View getImageForPosition(int position, View convertView, ViewGroup parent) { + LayoutInflater inflater = (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE); + View row = inflater.inflate(R.layout.account_spinner, parent, false); + TextView textView = (TextView) row.findViewById(R.id.spinnerText); + textView.setText(aors.get(position)); + ImageView imageView = (ImageView) row.findViewById(R.id.spinnerImage); + imageView.setImageResource(images.get(position)); + return row; + } +} + diff --git a/app/src/main/java/com/tutpro/baresip/Call.java b/app/src/main/java/com/tutpro/baresip/Call.java new file mode 100644 index 00000000..9c05f368 --- /dev/null +++ b/app/src/main/java/com/tutpro/baresip/Call.java @@ -0,0 +1,32 @@ +package com.tutpro.baresip; + +public class Call { + + private String ua, call, peer_uri, status; + + public Call(String ua, String call, String peer_uri, String status) { + this.ua = ua; + this.call = call; + this.peer_uri = peer_uri; + this.status = status; + } + + public String getUA() { + return ua; + } + + public String getCall() { + return call; + } + + public String getPeerURI() { + return peer_uri; + } + + public void setStatus(String status) { + this.status = status; + } + public String getStatus() { + return status; + } +} diff --git a/app/src/main/java/com/tutpro/baresip/EditAccountsActivity.java b/app/src/main/java/com/tutpro/baresip/EditAccountsActivity.java new file mode 100644 index 00000000..4a872cd0 --- /dev/null +++ b/app/src/main/java/com/tutpro/baresip/EditAccountsActivity.java @@ -0,0 +1,105 @@ +package com.tutpro.baresip; + +import android.content.Intent; +import android.os.Bundle; +import android.support.v7.app.AppCompatActivity; +import android.util.Log; +import android.util.TypedValue; +import android.view.Menu; +import android.view.MenuInflater; +import android.view.MenuItem; +import android.widget.EditText; +import android.widget.TextView; +import java.io.File; +import java.io.FileInputStream; +import java.io.FileOutputStream; +import java.io.OutputStreamWriter; + +public class EditAccountsActivity extends AppCompatActivity { + + private EditText editText; + private String path; + private File file; + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setContentView(R.layout.activity_edit_accounts); + + editText = (EditText)findViewById(R.id.editAccounts); + path = getApplicationContext().getFilesDir().getAbsolutePath() + "/accounts"; + file = new File(path); + String content; + + if (!file.exists()) { + Log.e("Baresip", "Failed to find accounts file"); + content = "No accounts"; + } else { + Log.e("Baresip", "Found accounts file"); + int length = (int)file.length(); + byte[] bytes = new byte[length]; + try { + FileInputStream in = new FileInputStream(file); + try { + in.read(bytes); + } finally { + in.close(); + } + content = new String(bytes); + } catch (java.io.IOException e) { + Log.e("Baresip", "Failed to read accounts file: " + e.toString()); + content = "Failed to read account file"; + } + } + + Log.d("Baresip", "Content length is: " + content.length()); + + editText.setTextSize(TypedValue.COMPLEX_UNIT_PX, + getResources().getDimension(R.dimen.textsize)); + editText.setText(content, TextView.BufferType.EDITABLE); + } + + public boolean onCreateOptionsMenu(Menu menu) { + super.onCreateOptionsMenu(menu); + MenuInflater inflater = getMenuInflater(); + inflater.inflate(R.menu.edit_menu, menu); + return true; + } + + public boolean onOptionsItemSelected(MenuItem item) { + Intent i = new Intent(this, MainActivity.class); + switch (item.getItemId()) { + case R.id.save: + try { + FileOutputStream fOut = + new FileOutputStream(file.getAbsoluteFile(), false); + OutputStreamWriter fWriter = new OutputStreamWriter(fOut); + String res = editText.getText().toString(); + try { + fWriter.write(res); + fWriter.close(); + fOut.close(); + } catch (java.io.IOException e) { + Log.e("Baresip", "Failed to write accounts file: " + + e.toString()); + } + } catch (java.io.FileNotFoundException e) { + Log.e("Baresip", "Failed to find accounts file: " + + e.toString()); + } + Log.d("Baresip", "Updated accounts file"); + i.putExtra("action", "save"); + setResult(RESULT_OK, i); + finish(); + return true; + case R.id.cancel: + i.putExtra("action", "cancel"); + setResult(RESULT_OK, i); + finish(); + return true; + default: + return super.onOptionsItemSelected(item); + } + } +} + diff --git a/app/src/main/java/com/tutpro/baresip/EditConfigActivity.java b/app/src/main/java/com/tutpro/baresip/EditConfigActivity.java new file mode 100644 index 00000000..9b41610f --- /dev/null +++ b/app/src/main/java/com/tutpro/baresip/EditConfigActivity.java @@ -0,0 +1,105 @@ +package com.tutpro.baresip; + +import android.content.Intent; +import android.os.Bundle; +import android.support.v7.app.AppCompatActivity; +import android.util.Log; +import android.util.TypedValue; +import android.view.Menu; +import android.view.MenuInflater; +import android.view.MenuItem; +import android.widget.EditText; +import android.widget.TextView; +import java.io.File; +import java.io.FileInputStream; +import java.io.FileOutputStream; +import java.io.OutputStreamWriter; + +public class EditConfigActivity extends AppCompatActivity { + + private EditText editText; + private String path; + private File file; + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setContentView(R.layout.activity_edit_config); + + editText = (EditText)findViewById(R.id.editConfig); + path = getApplicationContext().getFilesDir().getAbsolutePath() + "/config"; + // path = "/sdcard/baresip/config"; + file = new File(path); + String content; + + if (!file.exists()) { + Log.e("Baresip", "Failed to find config file"); + content = "No config"; + } else { + Log.e("Baresip", "Found config file"); + int length = (int)file.length(); + byte[] bytes = new byte[length]; + try { + FileInputStream in = new FileInputStream(file); + try { + in.read(bytes); + } finally { + in.close(); + } + content = new String(bytes); + } catch (java.io.IOException e) { + Log.e("Baresip", "Failed to read config file: " + e.toString()); + content = "Failed to read account file"; + } + } + + Log.d("Baresip", "Content length is: " + content.length()); + + editText.setTextSize(TypedValue.COMPLEX_UNIT_PX, + getResources().getDimension(R.dimen.textsize)); + editText.setText(content, TextView.BufferType.EDITABLE); + } + + public boolean onCreateOptionsMenu(Menu menu) { + super.onCreateOptionsMenu(menu); + MenuInflater inflater = getMenuInflater(); + inflater.inflate(R.menu.edit_menu, menu); + return true; + } + + public boolean onOptionsItemSelected(MenuItem item) { + Intent i = new Intent(this, MainActivity.class); + switch (item.getItemId()) { + case R.id.save: + try { + FileOutputStream fOut = + new FileOutputStream(file.getAbsoluteFile(), false); + OutputStreamWriter fWriter = new OutputStreamWriter(fOut); + String res = editText.getText().toString(); + try { + fWriter.write(res); + fWriter.close(); + fOut.close(); + } catch (java.io.IOException e) { + Log.e("Baresip", "Failed to write config file: " + + e.toString()); + } + } catch (java.io.FileNotFoundException e) { + Log.e("Baresip", "Failed to find config file: " + + e.toString()); + } + Log.d("Baresip", "Updated config file"); + i.putExtra("action", "save"); + setResult(RESULT_OK, i); + finish(); + return true; + case R.id.cancel: + i.putExtra("action", "cancel"); + setResult(RESULT_OK, i); + finish(); + return true; + default: + return super.onOptionsItemSelected(item); + } + } +} diff --git a/app/src/main/java/com/tutpro/baresip/EditContactsActivity.java b/app/src/main/java/com/tutpro/baresip/EditContactsActivity.java new file mode 100644 index 00000000..f9868e91 --- /dev/null +++ b/app/src/main/java/com/tutpro/baresip/EditContactsActivity.java @@ -0,0 +1,104 @@ +package com.tutpro.baresip; + +import android.content.Intent; +import android.os.Bundle; +import android.support.v7.app.AppCompatActivity; +import android.util.Log; +import android.util.TypedValue; +import android.view.Menu; +import android.view.MenuInflater; +import android.view.MenuItem; +import android.widget.EditText; +import android.widget.TextView; +import java.io.File; +import java.io.FileInputStream; +import java.io.FileOutputStream; +import java.io.OutputStreamWriter; + +public class EditContactsActivity extends AppCompatActivity { + + private EditText editText; + private String path; + private File file; + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setContentView(R.layout.activity_edit_contacts); + + editText = (EditText)findViewById(R.id.editText); + path = getApplicationContext().getFilesDir().getAbsolutePath() + + "/contacts"; + file = new File(path); + String content; + + if (!file.exists()) { + Log.e("Baresip", "Failed to find contacts file"); + content = "No contacts"; + } else { + Log.e("Baresip", "Found contacts file"); + int length = (int)file.length(); + byte[] bytes = new byte[length]; + try { + FileInputStream in = new FileInputStream(file); + try { + in.read(bytes); + } finally { + in.close(); + } + content = new String(bytes); + } catch (java.io.IOException e) { + Log.e("Baresip", "Failed to read contacts file: " + e.toString()); + content = "Failed to read account file"; + } + } + + Log.d("Baresip", "Content length is: " + content.length()); + + editText.setTextSize(TypedValue.COMPLEX_UNIT_PX, + getResources().getDimension(R.dimen.textsize)); + editText.setText(content, TextView.BufferType.EDITABLE); + } + + public boolean onCreateOptionsMenu(Menu menu) { + super.onCreateOptionsMenu(menu); + MenuInflater inflater = getMenuInflater(); + inflater.inflate(R.menu.edit_menu, menu); + return true; + } + + public boolean onOptionsItemSelected(MenuItem item) { + Intent i = new Intent(this, MainActivity.class); + switch (item.getItemId()) { + case R.id.save: + try { + FileOutputStream fOut = + new FileOutputStream(file.getAbsoluteFile(), false); + OutputStreamWriter fWriter = new OutputStreamWriter(fOut); + String res = editText.getText().toString(); + try { + fWriter.write(res); + fWriter.close(); + fOut.close(); + } catch (java.io.IOException e) { + Log.e("Baresip", "Failed to write contacts file: " + + e.toString()); + } + } catch (java.io.FileNotFoundException e) { + Log.e("Baresip", "Failed to find contacts file: " + + e.toString()); + } + Log.d("Baresip", "Updated contacts file"); + i.putExtra("action", "save"); + startActivity(i); + return true; + case R.id.cancel: + i.putExtra("action", "cancel"); + startActivity(i); + return true; + default: + return super.onOptionsItemSelected(item); + } + } +} + diff --git a/app/src/main/java/com/tutpro/baresip/MainActivity.java b/app/src/main/java/com/tutpro/baresip/MainActivity.java new file mode 100644 index 00000000..b3a1d5e6 --- /dev/null +++ b/app/src/main/java/com/tutpro/baresip/MainActivity.java @@ -0,0 +1,499 @@ +package com.tutpro.baresip; + +import android.content.Intent; +import android.content.res.AssetManager; +import android.content.res.Configuration; +import android.graphics.Color; +import android.support.v7.app.AppCompatActivity; +import android.os.Bundle; +import android.widget.*; +import android.widget.RelativeLayout.LayoutParams; +import android.view.*; +import android.util.Log; +import java.util.*; +import java.io.*; +import android.content.Context; + +public class MainActivity extends AppCompatActivity { + + static Context mainActivityContext; + static Boolean running = false; + static List accountList = new ArrayList<>(); + static EditText callee; + static RelativeLayout layout; + static Button callButton; + static ArrayList Accounts = new ArrayList<>(); + static ArrayList AoRs = new ArrayList<>(); + static ArrayList Images = new ArrayList<>(); + static AccountSpinnerAdapter AccountAdapter = null; + static ArrayList In = new ArrayList<>(); + static ArrayList Out = new ArrayList<>(); + + @Override + protected void onCreate(Bundle savedInstanceState) { + + super.onCreate(savedInstanceState); + setContentView(R.layout.activity_main); + + mainActivityContext = getApplicationContext(); + + Spinner AoRSpinner = (Spinner) findViewById(R.id.AoRList); + Log.i("Baresip", "Setting AccountAdapter"); + AccountAdapter = new AccountSpinnerAdapter(getApplicationContext(), AoRs, Images); + AoRSpinner.setAdapter(AccountAdapter); + AoRSpinner.setOnItemSelectedListener(new AdapterView.OnItemSelectedListener() { + @Override + public void onItemSelected(AdapterView parent, View view, int position, long id) { + String aor = AoRs.get(position); + Log.i("Baresip", "Setting " + aor + " current"); + ua_current_set(aor); + ArrayList out = uaCalls(Out, ua_current()); + if (out.size() == 0) { + callee.setText(""); + callButton.setText("Call"); + } else { + callee.setText(out.get(0).getPeerURI()); + callButton.setText(out.get(0).getStatus()); + } + ArrayList in = uaCalls(In, ua_current()); + int view_count = layout.getChildCount(); + Log.d("Baresip", "View count is " + view_count); + if (view_count > 4) { + layout.removeViews(4, view_count - 4); + } + for (Call c: in) { + for (int call_index = 0; call_index < In.size(); call_index++) { + addCallViews(c,(call_index + 1) * 10); + } + } + } + @Override + public void onNothingSelected(AdapterView parent) { + Log.i("Baresip", "Nothing selected"); + } + }); + + String[] assets = {"config", "accounts", "busy.wav", "callwaiting.wav", "error.wav", + "message.wav", "notfound.wav", "ring.wav", "ringback.wav"}; + final String path = mainActivityContext.getFilesDir().getPath(); + Log.d("Baresip", "path is: " + path); + File file; + file = new File(path); + if (!file.exists()) { + Log.d("Baresip", "Creating baresip directory"); + try { + new File(path).mkdirs(); + } catch (Error e) { + Log.e("Baresip", "Failed to create directory: " + + e.toString()); + } + } + for (String a : assets) { + file = new File(path + "/" + a); + if (!file.exists()) { + Log.d("Baresip", "Copying asset " + a); + copyAsset(a, path + "/" + a); + } + } + + if (!running) { + Log.i("Baresip", "Starting Baresip with path " + path); + new Thread(new Runnable() { + public void run() { + baresipStart(path); + } + }).start(); + running = true; + } + + layout = (RelativeLayout) findViewById(R.id.mainActivityLayout); + + callee = (EditText)findViewById(R.id.callee); + + callButton = (Button)findViewById(R.id.callButton); + callButton.setText("Call"); + callButton.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + String ua = ua_current(); + if (callButton.getText().toString().equals("Call")) { + String uri = ((EditText) findViewById(R.id.callee)).getText().toString(); + if (!uri.startsWith("sip:")) uri = "sip:" + uri; + if (!uri.contains("@")) { + String aor = ua_aor(ua); + String host = aor.substring(aor.indexOf("@") + 1); + uri = uri + "@" + host; + } + ((EditText) findViewById(R.id.callee)).setText(uri); + Log.i("Baresip", "Calling " + uri); + String call = ua_connect(uri); + if (!call.equals("")) { + Log.i("Baresip", "Adding outgoing call " + ua + "/" + call + + "/" + uri); + Out.add(new Call(ua, call, uri,"Cancel")); + callButton.setText("Cancel"); + } + } else { + Log.i("Baresip", "Hanging up " + + ((EditText) findViewById(R.id.callee)).getText()); + ua_hangup(ua, "", 486, "Rejected"); + } + } + }); + } + + @Override + public void onConfigurationChanged(Configuration newConfig) { + super.onConfigurationChanged(newConfig); + if (newConfig.orientation == Configuration.ORIENTATION_LANDSCAPE) { + Log.d("Baresip", "Screen orientation change to landscape"); + } else if (newConfig.orientation == Configuration.ORIENTATION_PORTRAIT){ + Log.d("Baresip", "Screen orientation change to portrait"); + } + } + + public boolean onCreateOptionsMenu(Menu menu) { + super.onCreateOptionsMenu(menu); + MenuInflater inflater = getMenuInflater(); + inflater.inflate(R.menu.main_menu, menu); + return true; + } + + public boolean onOptionsItemSelected(MenuItem item) { + Intent i = new Intent(this, MainActivity.class); + switch (item.getItemId()) { + case R.id.accounts: + startActivity(new Intent(MainActivity.this, + EditAccountsActivity.class)); + return true; + case R.id.config: + startActivity(new Intent(MainActivity.this, + EditConfigActivity.class)); + return true; + //case R.id.contacts: + // startActivity(new Intent(MainActivity.this, + // EditContactsActivity.class)); + //return true; + case R.id.about: + startActivity(new Intent(MainActivity.this, + AboutActivity.class)); + return true; + case R.id.quit: + if (running) { + Log.d("Baresip", "Stopping"); + baresipStop(); + Accounts.clear(); + AoRs.clear(); + Images.clear(); + running = false; + } + finish(); + System.exit(0); + return true; + default: + return super.onOptionsItemSelected(item); + } + } + + public ArrayList aorStatusList() { + ArrayList res = new ArrayList<>(); + for (Account a : Accounts) { + res.add(a.getAoR() + " (" + a.getStatus() + ")"); + } + return res; + } + + public void addAccount(String ua) { + String aor = ua_aor(ua); + Log.d("Baresip", "Adding account " + ua + " with AoR " + aor); + Accounts.add(new Account(ua, aor)); + AoRs.add(aor + " ()"); + Images.add(R.drawable.yellow); + runOnUiThread(new Runnable() { + @Override + public void run() { + AccountAdapter.notifyDataSetChanged(); + } + }); + } + + private void copyAsset(String asset, String path) { + try { + AssetManager assetManager = getAssets(); + InputStream is = assetManager.open(asset); + OutputStream os = new FileOutputStream(path); + byte [] buffer = new byte[512]; + int byteRead; + while ((byteRead = is.read(buffer)) != -1) { + os.write(buffer, 0, byteRead); + } + } catch (IOException e) { + Log.e("Baresip", "Failed to read asset " + asset + ": " + + e.toString()); + } + } + + private void addCallViews(final Call call, int id) { + Log.d("Baresip", "Creating new Incoming textview at " + id); + TextView caller_heading = new TextView(mainActivityContext); + caller_heading.setText("Incoming call from ..."); + caller_heading.setTextColor(Color.BLACK); + caller_heading.setTextSize(20); + caller_heading.setPadding(10, 20, 0, 0); + caller_heading.setId(id); + LayoutParams heading_params = new LayoutParams(LayoutParams.WRAP_CONTENT, + LayoutParams.WRAP_CONTENT); + if (id == 10) + heading_params.addRule(RelativeLayout.BELOW, callButton.getId()); + else + heading_params.addRule(RelativeLayout.BELOW,id - 10 + 3); + caller_heading.setLayoutParams(heading_params); + layout.addView(caller_heading); + + TextView caller_uri = new TextView(mainActivityContext); + caller_uri.setText(In.get(In.size() - 1).getPeerURI()); + caller_uri.setTextColor(Color.GREEN); + caller_uri.setTextSize(20); + caller_uri.setPadding(10, 10, 0, 10); + Log.d("Baresip", "Creating new caller textview at " + (id + 1)); + caller_uri.setId(id + 1); + LayoutParams caller_uri_params = new LayoutParams(LayoutParams.WRAP_CONTENT, + LayoutParams.WRAP_CONTENT); + caller_uri_params.addRule(RelativeLayout.BELOW, id); + caller_uri.setLayoutParams(caller_uri_params); + layout.addView(caller_uri); + + Button answer_button = new Button(mainActivityContext); + answer_button.setText("Answer"); + answer_button.setBackgroundResource(android.R.drawable.btn_default); + answer_button.setTextColor(Color.BLACK); + Log.d("Baresip", "Creating new answer button at " + (id + 2)); + answer_button.setId(id + 2); + answer_button.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + String call_ua = call.getUA(); + String call_call = call.getCall(); + switch (((Button)v).getText().toString()) { + case "Answer": + Log.i("Baresip", "UA " + call_ua + " accepting incoming call " + + call_call); + ua_answer(call_ua, call_call); + final int final_in_index = callIndex(In, call_ua, call_call); + if (final_in_index >= 0) { + Log.d("Baresip", "Changing answer button text to Hangup"); + runOnUiThread(new Runnable() { + @Override + public void run() { + final int answer_id = (final_in_index + 1) * 10 + 2; + Button answer_button = (Button)layout.findViewById(answer_id); + answer_button.setText("Hangup"); + Button reject_button = (Button)layout.findViewById(answer_id + 1); + reject_button.setEnabled(false); + } + }); + } + break; + case "Hangup": + Log.i("Baresip", "UA " + call_ua + " hanging up call " + + call_call); + ua_hangup(call_ua, call_call,200, "OK"); + break; + default: + Log.e("Baresip", "Invalid answer button text: " + + ((Button)v).getText().toString()); + break; + } + } + }); + LayoutParams answer_button_params = new LayoutParams(200, + LayoutParams.WRAP_CONTENT); + answer_button_params.addRule(RelativeLayout.BELOW, id + 1); + answer_button_params.setMargins(3, 10, 0, 0); + answer_button.setLayoutParams(answer_button_params); + layout.addView(answer_button); + + Button reject_button = new Button(mainActivityContext); + reject_button.setText("Reject"); + reject_button.setBackgroundResource(android.R.drawable.btn_default); + reject_button.setTextColor(Color.BLACK); + Log.d("Baresip", "Creating new reject button at " + (id + 3)); + reject_button.setId(id + 3); + reject_button.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + Log.i("Baresip", "UA " + call.getUA() + + " rejecting incoming call " + call.getCall()); + ua_hangup(call.getUA(), call.getCall(), 486, "Rejected"); + } + }); + LayoutParams reject_button_params = new LayoutParams(200, + LayoutParams.WRAP_CONTENT); + reject_button_params.addRule(RelativeLayout.BELOW, id + 1); + reject_button_params.setMargins(225, 10, 0, 0); + reject_button.setLayoutParams(reject_button_params); + layout.addView(reject_button); + } + + private int callIndex(ArrayList calls, String ua, String call) { + for (int i = 0; i < calls.size(); i++) { + if (calls.get(i).getUA().equals(ua) && calls.get(i).getCall().equals(call)) + return i; + } + return -1; + } + + private ArrayList uaCalls(ArrayList calls, String ua) { + ArrayList result = new ArrayList<>(); + for (int i = 0; i < calls.size(); i++) { + if (calls.get(i).getUA().equals(ua)) result.add(calls.get(i)); + } + return result; + } + + private void updateStatus(String event, final String ua, final String call) { + String aor = ua_aor(ua); + int index, call_index; + + Log.d("Baresip", "Handling event " + event + " for " + ua + "/" + call + "/" + + aor); + for (index = 0; index < Accounts.size(); index++) { + if (Accounts.get(index).getAoR().equals(aor)) { + Log.d("Baresip", "Found AoR at index " + index); + switch (event) { + case "registering": + case "unregistering": + // Log.d("Baresip", "Setting status to yellow"); + break; + case "registered": + Log.d("Baresip", "Setting status to green"); + Accounts.get(index).setStatus("OK"); + AoRs.set(index, aor); + Images.set(index, R.drawable.green); + runOnUiThread(new Runnable() { + @Override + public void run() { + AccountAdapter.notifyDataSetChanged(); + } + }); + break; + case "registering failed": + Log.d("Baresip", "Setting status to red"); + Accounts.get(index).setStatus("FAIL"); + AoRs.set(index, aor); + Images.set(index, R.drawable.red); + runOnUiThread(new Runnable() { + @Override + public void run() { + AccountAdapter.notifyDataSetChanged(); + } + }); + break; + case "call ringing": + break; + case "call established": + Log.d("Baresip", "Out call number is " + Out.size()); + int out_index = callIndex(Out, ua, call); + if (out_index >= 0) { + Log.d("Baresip", "Changing call button text to Hangup"); + Out.get(out_index).setStatus("Hangup"); + if (ua.equals(ua_current())) { + runOnUiThread(new Runnable() { + @Override + public void run() { + callButton.setText("Hangup"); + } + }); + } + break; + } + Log.e("Baresip", "Unknown call " + ua + "/" + call + + " established"); + break; + case "call incoming": + final String peer_uri = call_peeruri(call); + Log.d("Baresip", "Incoming call " + ua + "/" + call + "/" + + peer_uri); + final Call new_call = new Call(ua, call, peer_uri, "Answer"); + In.add(new_call); + if (ua.equals(ua_current())) { + runOnUiThread(new Runnable() { + @Override + public void run() { + addCallViews(new_call, In.size() * 10); + } + }); + } + break; + case "call closed": + call_index = callIndex(In, ua, call); + if (call_index != -1) { + Log.d("Baresip", "Removing inbound call " + ua + "/" + + call + "/" + In.get(index).getPeerURI()); + final int view_id = (call_index + 1) * 10; + final int remove_count = In.size() - call_index; + final int final_call_index = call_index; + In.remove(call_index); + Log.d("Baresip", "Current UA is " + ua_current()); + if (ua.equals(ua_current())) { + runOnUiThread(new Runnable() { + @Override + public void run() { + View caller_heading = layout.findViewById(view_id); + int view_index = layout.indexOfChild(caller_heading); + Log.d("Baresip", "Index of caller heading is " + + view_index); + layout.removeViews(view_index, 4 * remove_count); + for (int i = final_call_index; i < In.size(); i++) { + addCallViews(In.get(i), (i + 1) * 10); + } + } + }); + } + break; + } + call_index = callIndex(Out, ua, call); + if (call_index != -1) { + Log.d("Baresip", "Removing called call " + ua + "/" + + call + "/" + Out.get(index).getPeerURI()); + Out.remove(call_index); + if (ua.equals(ua_current())) { + runOnUiThread(new Runnable() { + @Override + public void run() { + callButton.setText("Call"); + callButton.setEnabled(true); + } + }); + } + break; + } + Log.e("Baresip", "Unknown call " + ua + "/" + call + + " closed"); + break; + default: + Log.d("Baresip", "Unknown event '" + event + "'"); + break; + } + } + } + } + + public native void baresipStart(String path); + public native void baresipStop(); + public native String ua_aor(String ua); + public native Boolean ua_isregistered(long ua_ptr); + public native String ua_call(String ua); + public native String ua_prev_call(String ua); + public native String call_peeruri(String call); + public native String ua_current(); + public static native void ua_current_set(String s); + public native String ua_connect(String s); + public native void ua_answer(String ua, String call); + public native void ua_hangup(String ua, String call, int code, String reason); + + static { + System.loadLibrary("baresip"); + } + +} diff --git a/app/src/main/res/drawable/edit_aor.xml b/app/src/main/res/drawable/edit_aor.xml new file mode 100644 index 00000000..e5fddbc8 --- /dev/null +++ b/app/src/main/res/drawable/edit_aor.xml @@ -0,0 +1,5 @@ + + + + diff --git a/app/src/main/res/drawable/green.png b/app/src/main/res/drawable/green.png new file mode 100644 index 00000000..fbea0162 Binary files /dev/null and b/app/src/main/res/drawable/green.png differ diff --git a/app/src/main/res/drawable/red.png b/app/src/main/res/drawable/red.png new file mode 100644 index 00000000..b9b15184 Binary files /dev/null and b/app/src/main/res/drawable/red.png differ diff --git a/app/src/main/res/drawable/yellow.png b/app/src/main/res/drawable/yellow.png new file mode 100644 index 00000000..c6f99fec Binary files /dev/null and b/app/src/main/res/drawable/yellow.png differ diff --git a/app/src/main/res/layout/account_spinner.xml b/app/src/main/res/layout/account_spinner.xml new file mode 100644 index 00000000..e4c68a81 --- /dev/null +++ b/app/src/main/res/layout/account_spinner.xml @@ -0,0 +1,26 @@ + + + + + + + + + + + + diff --git a/app/src/main/res/layout/activity_about.xml b/app/src/main/res/layout/activity_about.xml new file mode 100644 index 00000000..9153cded --- /dev/null +++ b/app/src/main/res/layout/activity_about.xml @@ -0,0 +1,17 @@ + + + + + + diff --git a/app/src/main/res/layout/activity_edit_accounts.xml b/app/src/main/res/layout/activity_edit_accounts.xml new file mode 100644 index 00000000..96dd5793 --- /dev/null +++ b/app/src/main/res/layout/activity_edit_accounts.xml @@ -0,0 +1,18 @@ + + + + + + diff --git a/app/src/main/res/layout/activity_edit_config.xml b/app/src/main/res/layout/activity_edit_config.xml new file mode 100644 index 00000000..95a95a68 --- /dev/null +++ b/app/src/main/res/layout/activity_edit_config.xml @@ -0,0 +1,18 @@ + + + + + + diff --git a/app/src/main/res/layout/activity_edit_contacts.xml b/app/src/main/res/layout/activity_edit_contacts.xml new file mode 100644 index 00000000..a556e598 --- /dev/null +++ b/app/src/main/res/layout/activity_edit_contacts.xml @@ -0,0 +1,18 @@ + + + + + + diff --git a/app/src/main/res/layout/activity_main.xml b/app/src/main/res/layout/activity_main.xml new file mode 100644 index 00000000..6eb0b84c --- /dev/null +++ b/app/src/main/res/layout/activity_main.xml @@ -0,0 +1,60 @@ + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/menu/accounts_menu.xml b/app/src/main/res/menu/accounts_menu.xml new file mode 100644 index 00000000..53b5fd18 --- /dev/null +++ b/app/src/main/res/menu/accounts_menu.xml @@ -0,0 +1,12 @@ + + + + + + + + + diff --git a/app/src/main/res/menu/edit_menu.xml b/app/src/main/res/menu/edit_menu.xml new file mode 100644 index 00000000..264f9e9b --- /dev/null +++ b/app/src/main/res/menu/edit_menu.xml @@ -0,0 +1,10 @@ + + + + + + + + diff --git a/app/src/main/res/menu/main_menu.xml b/app/src/main/res/menu/main_menu.xml new file mode 100644 index 00000000..3de6c80f --- /dev/null +++ b/app/src/main/res/menu/main_menu.xml @@ -0,0 +1,18 @@ + + + + + + + + + + + + + diff --git a/app/src/main/res/mipmap-hdpi/ic_launcher.png b/app/src/main/res/mipmap-hdpi/ic_launcher.png new file mode 100644 index 00000000..cde69bcc Binary files /dev/null and b/app/src/main/res/mipmap-hdpi/ic_launcher.png differ diff --git a/app/src/main/res/mipmap-mdpi/ic_launcher.png b/app/src/main/res/mipmap-mdpi/ic_launcher.png new file mode 100644 index 00000000..c133a0cb Binary files /dev/null and b/app/src/main/res/mipmap-mdpi/ic_launcher.png differ diff --git a/app/src/main/res/mipmap-xhdpi/ic_launcher.png b/app/src/main/res/mipmap-xhdpi/ic_launcher.png new file mode 100644 index 00000000..bfa42f0e Binary files /dev/null and b/app/src/main/res/mipmap-xhdpi/ic_launcher.png differ diff --git a/app/src/main/res/mipmap-xxhdpi/ic_launcher.png b/app/src/main/res/mipmap-xxhdpi/ic_launcher.png new file mode 100644 index 00000000..324e72cd Binary files /dev/null and b/app/src/main/res/mipmap-xxhdpi/ic_launcher.png differ diff --git a/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png b/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png new file mode 100644 index 00000000..aee44e13 Binary files /dev/null and b/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png differ diff --git a/app/src/main/res/values-w820dp/dimens.xml b/app/src/main/res/values-w820dp/dimens.xml new file mode 100644 index 00000000..63fc8164 --- /dev/null +++ b/app/src/main/res/values-w820dp/dimens.xml @@ -0,0 +1,6 @@ + + + 64dp + diff --git a/app/src/main/res/values/colors.xml b/app/src/main/res/values/colors.xml new file mode 100644 index 00000000..3ab3e9cb --- /dev/null +++ b/app/src/main/res/values/colors.xml @@ -0,0 +1,6 @@ + + + #3F51B5 + #303F9F + #FF4081 + diff --git a/app/src/main/res/values/dimens.xml b/app/src/main/res/values/dimens.xml new file mode 100644 index 00000000..812cb7be --- /dev/null +++ b/app/src/main/res/values/dimens.xml @@ -0,0 +1,6 @@ + + + 16dp + 16dp + 16dp + diff --git a/app/src/main/res/values/dimensions.xml b/app/src/main/res/values/dimensions.xml new file mode 100644 index 00000000..56b7e1c4 --- /dev/null +++ b/app/src/main/res/values/dimensions.xml @@ -0,0 +1,4 @@ + + + 12sp + \ No newline at end of file diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml new file mode 100644 index 00000000..109a9fc5 --- /dev/null +++ b/app/src/main/res/values/strings.xml @@ -0,0 +1,13 @@ + + + baresip + EditActivity + EditAccounts + + Baresip Android application\n\n + Work in progress\n\n + Juha Heinanen jh@tutpro.com\n\n + + No accounts + + diff --git a/app/src/main/res/values/styles.xml b/app/src/main/res/values/styles.xml new file mode 100644 index 00000000..545b9c6d --- /dev/null +++ b/app/src/main/res/values/styles.xml @@ -0,0 +1,20 @@ + + + + + + + +