From f4bd1a93edfcbbac5f107a5c21c3b75acaf69a73 Mon Sep 17 00:00:00 2001 From: Juha Heinanen Date: Wed, 23 May 2018 10:58:57 +0300 Subject: [PATCH] added graphical account configuration and many other improvements --- app/build.gradle | 2 +- app/src/main/AndroidManifest.xml | 14 +- app/src/main/assets/accounts | 1 - app/src/main/assets/config | 6 +- app/src/main/cpp/baresip.c | 391 +++++++++++++- .../main/kotlin/com/tutpro/baresip/Account.kt | 127 ++++- .../com/tutpro/baresip/AccountActivity.kt | 298 +++++++++++ .../com/tutpro/baresip/AccountListAdapter.kt | 61 +++ .../kotlin/com/tutpro/baresip/AccountRow.kt | 3 + .../com/tutpro/baresip/AccountsActivity.kt | 100 ++++ .../main/kotlin/com/tutpro/baresip/Call.kt | 38 +- .../main/kotlin/com/tutpro/baresip/Codec.kt | 3 + .../tutpro/baresip/EditAccountsActivity.kt | 103 ---- .../main/kotlin/com/tutpro/baresip/History.kt | 4 +- .../com/tutpro/baresip/HistoryActivity.kt | 13 +- .../kotlin/com/tutpro/baresip/MainActivity.kt | 486 ++++++++++-------- .../com/tutpro/baresip/UaSpinnerAdapter.kt | 37 ++ .../kotlin/com/tutpro/baresip/UserAgent.kt | 35 ++ .../main/kotlin/com/tutpro/baresip/Utils.kt | 94 ++-- app/src/main/res/drawable/action_add.png | Bin 0 -> 1028 bytes app/src/main/res/drawable/action_remove.png | Bin 0 -> 509 bytes app/src/main/res/drawable/speaker.png | Bin 0 -> 1650 bytes .../main/res/drawable/spinner_background.xml | 6 + app/src/main/res/layout/account_row.xml | 29 ++ app/src/main/res/layout/activity_account.xml | 152 ++++++ app/src/main/res/layout/activity_accounts.xml | 48 ++ app/src/main/res/layout/history_row.xml | 2 +- 27 files changed, 1635 insertions(+), 418 deletions(-) create mode 100644 app/src/main/kotlin/com/tutpro/baresip/AccountActivity.kt create mode 100644 app/src/main/kotlin/com/tutpro/baresip/AccountListAdapter.kt create mode 100644 app/src/main/kotlin/com/tutpro/baresip/AccountRow.kt create mode 100644 app/src/main/kotlin/com/tutpro/baresip/AccountsActivity.kt create mode 100644 app/src/main/kotlin/com/tutpro/baresip/Codec.kt delete mode 100644 app/src/main/kotlin/com/tutpro/baresip/EditAccountsActivity.kt create mode 100644 app/src/main/kotlin/com/tutpro/baresip/UaSpinnerAdapter.kt create mode 100644 app/src/main/kotlin/com/tutpro/baresip/UserAgent.kt create mode 100644 app/src/main/res/drawable/action_add.png create mode 100644 app/src/main/res/drawable/action_remove.png create mode 100644 app/src/main/res/drawable/speaker.png create mode 100644 app/src/main/res/drawable/spinner_background.xml create mode 100644 app/src/main/res/layout/account_row.xml create mode 100644 app/src/main/res/layout/activity_account.xml create mode 100644 app/src/main/res/layout/activity_accounts.xml diff --git a/app/build.gradle b/app/build.gradle index 7123bcfa..d0ea44bd 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -35,7 +35,7 @@ dependencies { implementation fileTree(include: ['*.jar'], dir: 'libs') implementation 'com.android.support:appcompat-v7:25.2.0' implementation 'com.android.support.constraint:constraint-layout:1.0.1' - implementation "org.jetbrains.kotlin:kotlin-stdlib-jre7:$kotlin_version" + implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" } repositories { mavenCentral() diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 8bb08492..647068e3 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -51,8 +51,18 @@ + + + + # # -;auth_user=foo;auth_pass=xxx;outbound="sip:192.168.43.98:5060;transport=tcp";ptime=20;audio_codecs=OPUS/48000/2,PCMU,PCMA;video_codecs="";regint=600;pubint=0;sipnat=outbound diff --git a/app/src/main/assets/config b/app/src/main/assets/config index 1a1f2ac6..ca6e50a5 100644 --- a/app/src/main/assets/config +++ b/app/src/main/assets/config @@ -128,9 +128,9 @@ module ice.so #module natpmp.so # Media encryption modules -#module srtp.so -#module dtls_srtp.so -#module zrtp.so +module srtp.so +module dtls_srtp.so +module zrtp.so #------------------------------------------------------------------------------ diff --git a/app/src/main/cpp/baresip.c b/app/src/main/cpp/baresip.c index abece685..e10e50a8 100644 --- a/app/src/main/cpp/baresip.c +++ b/app/src/main/cpp/baresip.c @@ -102,7 +102,12 @@ static void ua_event_handler(struct ua *ua, enum ua_event ev, re_snprintf(event_buf, sizeof event_buf, "%s", "call incoming"); break; case UA_EVENT_CALL_MENC: - re_snprintf(event_buf, sizeof event_buf, "%s", prm); + if (prm[0] == '1') + re_snprintf(event_buf, sizeof event_buf, "call verify,%s", prm+2); + else if (prm[0] == '2') + re_snprintf(event_buf, sizeof event_buf, "call verified,%s", prm+2); + else + re_snprintf(event_buf, sizeof event_buf, "%s", "unknown menc event"); break; case UA_EVENT_CALL_CLOSED: re_snprintf(event_buf, sizeof event_buf, "%s", "call closed"); @@ -317,26 +322,342 @@ Java_com_tutpro_baresip_MainActivity_baresipStop(JNIEnv *env, jobject thiz) } 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); +Java_com_tutpro_baresip_AccountKt_account_1display_1name(JNIEnv *env, jobject thiz, jstring javaAcc) { + const char *native_acc = (*env)->GetStringUTFChars(env, javaAcc, 0); + struct account *acc = (struct account *)strtoul(native_acc, NULL, 10); - if (strlen(native_ua) > 0) - return (*env)->NewStringUTF(env, ua_aor(ua)); + (*env)->ReleaseStringUTFChars(env, javaAcc, native_acc); + if (acc) { + const char *dn = account_display_name(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_AccountKt_account_1set_1display_1name(JNIEnv *env, jobject thiz, + jstring javaAcc, jstring javaDn) { + const char *native_acc = (*env)->GetStringUTFChars(env, javaAcc, 0); + struct account *acc = (struct account *)strtoul(native_acc, NULL, 10); + (*env)->ReleaseStringUTFChars(env, javaAcc, native_acc); + const char *dn = (*env)->GetStringUTFChars(env, javaDn, 0);\ + + int res; + if (strlen(dn) > 0) + res = account_set_display_name(acc, dn); + else + res = account_set_display_name(acc, NULL); + (*env)->ReleaseStringUTFChars(env, javaDn, dn); + return res; +} + +JNIEXPORT jstring JNICALL +Java_com_tutpro_baresip_AccountKt_account_1aor(JNIEnv *env, jobject thiz, jstring javaAcc) { + const char *native_acc = (*env)->GetStringUTFChars(env, javaAcc, 0); + struct account *acc = (struct account *)strtoul(native_acc, NULL, 10); + + (*env)->ReleaseStringUTFChars(env, javaAcc, native_acc); + if (acc) + return (*env)->NewStringUTF(env, account_aor(acc)); else return (*env)->NewStringUTF(env, ""); } JNIEXPORT jstring JNICALL -Java_com_tutpro_baresip_MainActivity_ua_1mediaenc(JNIEnv *env, jobject thiz, jstring javaUA) +Java_com_tutpro_baresip_AccountKt_account_1auth_1user(JNIEnv *env, jobject thiz, jstring javaAcc) { + const char *native_acc = (*env)->GetStringUTFChars(env, javaAcc, 0); + struct account *acc = (struct account *)strtoul(native_acc, NULL, 10); + (*env)->ReleaseStringUTFChars(env, javaAcc, native_acc); + + if (acc) { + const char *au = account_auth_user(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_AccountKt_account_1set_1auth_1user(JNIEnv *env, jobject thiz, + jstring javaAcc, jstring javaUser) { + const char *native_acc = (*env)->GetStringUTFChars(env, javaAcc, 0); + struct account *acc = (struct account *)strtoul(native_acc, NULL, 10); + (*env)->ReleaseStringUTFChars(env, javaAcc, native_acc); + const char *user = (*env)->GetStringUTFChars(env, javaUser, 0); + + int res; + if (strlen(user) > 0) + res = account_set_auth_user(acc, user); + else + res = account_set_auth_user(acc, NULL); + (*env)->ReleaseStringUTFChars(env, javaUser, user); + return res; +} + +JNIEXPORT jstring JNICALL +Java_com_tutpro_baresip_AccountKt_account_1auth_1pass(JNIEnv *env, jobject thiz, jstring javaAcc) { + const char *native_acc = (*env)->GetStringUTFChars(env, javaAcc, 0); + struct account *acc = (struct account *)strtoul(native_acc, NULL, 10); + (*env)->ReleaseStringUTFChars(env, javaAcc, native_acc); + + if (acc) { + const char *ap = account_auth_pass(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_AccountKt_account_1set_1auth_1pass(JNIEnv *env, jobject thiz, + jstring javaAcc, jstring javaPass) { + const char *native_acc = (*env)->GetStringUTFChars(env, javaAcc, 0); + struct account *acc = (struct account *)strtoul(native_acc, NULL, 10); + (*env)->ReleaseStringUTFChars(env, javaAcc, native_acc); + const char *pass = (*env)->GetStringUTFChars(env, javaPass, 0); + + int res; + if (strlen(pass) > 0) + res = account_set_auth_pass(acc, pass); + else + res = account_set_auth_pass(acc, NULL); + (*env)->ReleaseStringUTFChars(env, javaPass, pass); + return res; +} + +JNIEXPORT jstring JNICALL +Java_com_tutpro_baresip_AccountKt_account_1outbound(JNIEnv *env, jobject thiz, jstring javaAcc, + jint ix) { + const char *native_acc = (*env)->GetStringUTFChars(env, javaAcc, 0); + struct account *acc = (struct account *) strtoul(native_acc, NULL, 10); + const uint16_t native_ix = ix; + const char *outbound; + + (*env)->ReleaseStringUTFChars(env, javaAcc, native_acc); + if (acc) { + outbound = account_outbound(acc, native_ix); + if (outbound) + return (*env)->NewStringUTF(env, outbound); + else + return (*env)->NewStringUTF(env, ""); + } else { + return (*env)->NewStringUTF(env, ""); + } +} + +JNIEXPORT int JNICALL +Java_com_tutpro_baresip_AccountKt_account_1set_1outbound(JNIEnv *env, jobject thiz, jstring javaAcc, + jstring javaOb, jint javaIx) { + const char *native_acc = (*env)->GetStringUTFChars(env, javaAcc, 0); + struct account *acc = (struct account *) strtoul(native_acc, NULL, 10); + (*env)->ReleaseStringUTFChars(env, javaAcc, native_acc); + const char *ob = (*env)->GetStringUTFChars(env, javaOb, 0); + const uint16_t ix = javaIx; + + int res; + if (strlen(ob) > 0) + res = account_set_outbound(acc, ob, ix); + else + res = account_set_outbound(acc, NULL, ix); + (*env)->ReleaseStringUTFChars(env, javaOb, ob); + return res; +} + +JNIEXPORT jstring JNICALL +Java_com_tutpro_baresip_AccountKt_account_1audio_1codec(JNIEnv *env, jobject thiz, jstring javaAcc, + jint ix) { + const char *native_acc = (*env)->GetStringUTFChars(env, javaAcc, 0); + struct account *acc = (struct account *) strtoul(native_acc, NULL, 10); + (*env)->ReleaseStringUTFChars(env, javaAcc, native_acc); + const uint16_t native_ix = ix; + const struct list *codecl; + char codec_buf[32]; + struct le *le; + + if (acc) { + codecl = account_aucodecl(acc); + if (!list_isempty(codecl)) { + int i = -1; + for (le = list_head(codecl); le != NULL; le = le->next) { + i++; + if (i > ix) break; + if (i == ix) { + const struct aucodec *ac = le->data; + re_snprintf(codec_buf, sizeof codec_buf, "%s/%u/%u", ac->name, ac->srate, ac->ch); + break; + } + } + if (i == ix) { + return (*env)->NewStringUTF(env, codec_buf); + } + } + } + return (*env)->NewStringUTF(env, ""); +} + +JNIEXPORT jint JNICALL +Java_com_tutpro_baresip_AccountKt_account_1set_1audio_1codecs(JNIEnv *env, jobject thiz, + jstring javaAcc, jstring javaCodecs) { + const char *native_acc = (*env)->GetStringUTFChars(env, javaAcc, 0); + struct account *acc = (struct account *) strtoul(native_acc, NULL, 10); + (*env)->ReleaseStringUTFChars(env, javaAcc, native_acc); + const char *codecs = (*env)->GetStringUTFChars(env, javaCodecs, 0); + + LOGD("setting audio codecs '%s'\n", codecs); + int res = account_set_audio_codecs(acc, codecs); + (*env)->ReleaseStringUTFChars(env, javaCodecs, codecs); + return res; +} + +JNIEXPORT jint JNICALL +Java_com_tutpro_baresip_AccountKt_account_1regint(JNIEnv *env, jobject thiz, jstring javaAcc) { + const char *native_acc = (*env)->GetStringUTFChars(env, javaAcc, 0); + struct account *acc = (struct account *) strtoul(native_acc, NULL, 10); + + (*env)->ReleaseStringUTFChars(env, javaAcc, native_acc); + if (acc) + return account_regint(acc); + else + return 0; +} + +JNIEXPORT jint JNICALL +Java_com_tutpro_baresip_AccountKt_account_1set_1regint(JNIEnv *env, jobject thiz, + jstring javaAcc, jint javaRegint) { + const char *native_acc = (*env)->GetStringUTFChars(env, javaAcc, 0); + struct account *acc = (struct account *)strtoul(native_acc, NULL, 10); + (*env)->ReleaseStringUTFChars(env, javaAcc, native_acc); + const uint32_t regint = javaRegint; + + return account_set_regint(acc, regint); +} + +JNIEXPORT jstring JNICALL +Java_com_tutpro_baresip_AccountKt_account_1mediaenc(JNIEnv *env, jobject thiz, jstring javaAcc) +{ + const char *native_acc = (*env)->GetStringUTFChars(env, javaAcc, 0); + struct account *acc = (struct account *) strtoul(native_acc, NULL, 10); + + (*env)->ReleaseStringUTFChars(env, javaAcc, native_acc); + if (acc) { + const char *mediaenc = account_mediaenc(acc); + if (mediaenc) return (*env)->NewStringUTF(env, mediaenc); + } + return (*env)->NewStringUTF(env, ""); +} + +JNIEXPORT jint JNICALL +Java_com_tutpro_baresip_AccountKt_account_1set_1mediaenc(JNIEnv *env, jobject thiz, + jstring javaAcc, jstring javaMencid) { + const char *native_acc = (*env)->GetStringUTFChars(env, javaAcc, 0); + struct account *acc = (struct account *)strtoul(native_acc, NULL, 10); + (*env)->ReleaseStringUTFChars(env, javaAcc, native_acc); + const char *mencid = (*env)->GetStringUTFChars(env, javaMencid, 0); + + int res; + if (strlen(mencid) > 0) + res = account_set_mediaenc(acc, mencid); + else + res = account_set_mediaenc(acc, NULL); + (*env)->ReleaseStringUTFChars(env, javaMencid, mencid); + return res; +} + +JNIEXPORT jint JNICALL +Java_com_tutpro_baresip_UserAgent_00024Companion_ua_1alloc(JNIEnv *env, jobject thiz, + jstring javaSipUri) { + const char *sip_uri = (*env)->GetStringUTFChars(env, javaSipUri, 0); + struct ua *ua; + LOGD("allocating UA '%s'\n", sip_uri); + int res = ua_alloc(&ua, sip_uri); + (*env)->ReleaseStringUTFChars(env, javaSipUri, sip_uri); + if (res == 0) { + char ua_buf[64]; + UpdateContext *pctx = (UpdateContext*)(&g_ctx); + 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->mainActivityClz, "addAccount", + "(Ljava/lang/String;)V"); + (*env)->CallVoidMethod(env, pctx->mainActivityObj, accountId, javaUA); + (*env)->DeleteLocalRef(env, javaUA); + } + return res; +} + +JNIEXPORT jint JNICALL +Java_com_tutpro_baresip_UserAgent_00024Companion_ua_1register(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); - struct account *acc = ua_account(ua); - char *mediaenc = account_mediaenc(acc); - if (mediaenc) - return (*env)->NewStringUTF(env, mediaenc); + (*env)->ReleaseStringUTFChars(env, javaUA, native_ua); + return ua_register(ua); +} + +JNIEXPORT void JNICALL +Java_com_tutpro_baresip_UserAgent_00024Companion_ua_1unregister(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); + ua_unregister(ua); +} + +JNIEXPORT jboolean JNICALL +Java_com_tutpro_baresip_UserAgent_00024Companion_ua_1isregistered(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); + return ua_isregistered(ua); +} + +JNIEXPORT void JNICALL +Java_com_tutpro_baresip_UserAgent_00024Companion_ua_1destroy(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); + mem_deref(ua); +} + +JNIEXPORT jstring JNICALL +Java_com_tutpro_baresip_UserAgentKt_ua_1account(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); + struct account *acc; + char acc_buf[256]; + + (*env)->ReleaseStringUTFChars(env, javaUA, native_ua); + acc_buf[0] = '\0'; + if (ua) { + acc = ua_account(ua); + if (acc) sprintf(acc_buf, "%lu", (unsigned long) acc); + } + return (*env)->NewStringUTF(env, acc_buf); +} + +JNIEXPORT jstring JNICALL +Java_com_tutpro_baresip_UserAgentKt_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); + + (*env)->ReleaseStringUTFChars(env, javaUA, native_ua); + if (ua) + return (*env)->NewStringUTF(env, ua_aor(ua)); else return (*env)->NewStringUTF(env, ""); } @@ -423,7 +744,7 @@ Java_com_tutpro_baresip_MainActivity_ua_1answer(JNIEnv *env, jobject thiz, JNIEXPORT jint JNICALL Java_com_tutpro_baresip_MainActivity_call_1hold(JNIEnv *env, jobject thiz, - jstring javaCall) { + jstring javaCall) { const char *native_call = (*env)->GetStringUTFChars(env, javaCall, 0); LOGD("holding call %s\n", native_call); int res = call_hold(ua_call(uag_current()), true); @@ -433,7 +754,7 @@ Java_com_tutpro_baresip_MainActivity_call_1hold(JNIEnv *env, jobject thiz, JNIEXPORT jint JNICALL Java_com_tutpro_baresip_MainActivity_call_1unhold(JNIEnv *env, jobject thiz, - jstring javaCall) { + jstring javaCall) { const char *native_call = (*env)->GetStringUTFChars(env, javaCall, 0); LOGD("holding call %s\n", native_call); int res = call_hold(ua_call(uag_current()), false); @@ -514,10 +835,50 @@ Java_com_tutpro_baresip_MainActivity_reload_1config(JNIEnv *env, jobject thiz) { } JNIEXPORT jint JNICALL -Java_com_tutpro_baresip_MainActivity_cmd_1exec(JNIEnv *env, jobject thiz, jstring javaCmd) { +Java_com_tutpro_baresip_Utils_cmd_1exec(JNIEnv *env, jobject thiz, jstring javaCmd) { const char *native_cmd = (*env)->GetStringUTFChars(env, javaCmd, 0); LOGD("processing command '%s'\n", native_cmd); int res = cmd_process_long(baresip_commands(), native_cmd, strlen(native_cmd), &pf_null, NULL); (*env)->ReleaseStringUTFChars(env, javaCmd, native_cmd); return res; } + +JNIEXPORT jstring JNICALL +Java_com_tutpro_baresip_Utils_audio_1codecs(JNIEnv *env, jobject thiz) +{ + 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'; + break; + } + start = start + len; + left = left - len; + } + return (*env)->NewStringUTF(env, codec_buf); +} + +JNIEXPORT jboolean JNICALL +Java_com_tutpro_baresip_Utils_uri_1decode(JNIEnv *env, jobject thiz, jstring javaUri) { + const char *uri = (*env)->GetStringUTFChars(env, javaUri, 0); + struct pl pl; + struct uri uri2; + + LOGD("decoding uri '%s'\n", uri); + pl_set_str(&pl, uri); + int res = uri_decode(&uri2, &pl); + (*env)->ReleaseStringUTFChars(env, javaUri, uri); + return res == 0; +} \ No newline at end of file diff --git a/app/src/main/kotlin/com/tutpro/baresip/Account.kt b/app/src/main/kotlin/com/tutpro/baresip/Account.kt index 72fbd2fd..519f53cd 100644 --- a/app/src/main/kotlin/com/tutpro/baresip/Account.kt +++ b/app/src/main/kotlin/com/tutpro/baresip/Account.kt @@ -1,3 +1,128 @@ package com.tutpro.baresip -class Account(val ua: String, val aoR: String, val mediaenc: String, var status: String) {} +class Account(val accp: String) { + + var displayName = account_display_name(accp) + val aor = account_aor(accp) + var authUser = account_auth_user(accp) + var authPass = account_auth_pass(accp) + var outbound = ArrayList() + var audioCodec = ArrayList() + var regint = account_regint(accp) + var mediaenc = account_mediaenc(accp) + + init { + var i = 0 + while (true) { + val ob = account_outbound(accp, i) + if (ob != "") { + outbound.add(ob) + i++ + } else { + break + } + } + i = 0 + while (true) { + val ac = account_audio_codec(accp, i) + if (ac != "") { + audioCodec.add(ac) + i++ + } else { + break + } + } + } + + fun print() : String { + + var res: String + + if (displayName != "") + res = "\"${displayName}\" " + else + res = "" + + res = res + "<${aor}>" + + if (authUser != "") res = res + ";auth_user=\"${authUser}\"" + + if (authPass != "") res = res + ";auth_pass=\"${authPass}\"" + + if (outbound.size > 0) res = res + ";outbound=\"${outbound[0]}\"" + if (outbound.size > 1) res = res + ";outbound2=\"${outbound[1]}\"" + + if (audioCodec.size > 0) { + var first = true + res = res + ";audio_codecs=" + for (c in audioCodec) + if (first) { + res = res + c + first = false + } else { + res = res + ",$c" + } + } + + if (mediaenc != "") res = res + ";mediaenc=${mediaenc}" + + res = res + ";ptime=20;regint=${regint};regq=0.5;pubint=0;answermode=manual;sipnat=outbound" + + return res + } + + companion object { + + fun accounts(): ArrayList { + var res = ArrayList() + for (ua in MainActivity.uas) { + res.add(ua.account) + } + return res + } + + fun find(uas: ArrayList, accp: String): Account? { + for (ua in uas) { + if (ua.account.accp == accp) return ua.account + } + return null + } + + fun findUA(aor: String): UserAgent? { + for (ua in MainActivity.uas) { + if (ua.aor == aor) return ua + } + return null + } + + fun exists(uas: ArrayList, aor: String): Boolean { + for (ua in uas) { + if (ua.account.aor == aor) return true + } + return false + } + + fun printNew(aor: String) : String { + var res = "<$aor>;auth_user=\"\";auth_pass=\"\";audio_codecs=opus/48000/2,PCMU,PCMA" + res = res + ";ptime=20;regint=600;regq=0.5;pubint=0;answermode=manual;sipnat=outbound" + res = res + ";mediaenc=zrtp" + return res + } + } +} + +external fun account_set_display_name(acc: String, dn: String): Int +external fun account_display_name(acc: String): String +external fun account_aor(acc: String): String +external fun account_auth_user(acc: String): String +external fun account_set_auth_user(acc: String, user: String): Int +external fun account_auth_pass(acc: String): String +external fun account_set_auth_pass(acc: String, pass: String): Int +external fun account_outbound(acc: String, ix: Int): String +external fun account_set_outbound(acc: String, ob: String, ix: Int): Int +external fun account_audio_codec(acc: String, ix: Int): String +external fun account_regint(acc: String): Int +external fun account_set_regint(acc: String, regint: Int): Int +external fun account_mediaenc(acc: String): String +external fun account_set_mediaenc(acc: String, mencid: String): Int +external fun account_set_audio_codecs(acc: String, codecs: String): Int \ No newline at end of file diff --git a/app/src/main/kotlin/com/tutpro/baresip/AccountActivity.kt b/app/src/main/kotlin/com/tutpro/baresip/AccountActivity.kt new file mode 100644 index 00000000..55d8ca00 --- /dev/null +++ b/app/src/main/kotlin/com/tutpro/baresip/AccountActivity.kt @@ -0,0 +1,298 @@ +package com.tutpro.baresip + +import android.content.Intent +import android.os.Bundle +import android.support.v7.app.AppCompatActivity +import android.util.Log +import android.view.Menu +import android.view.MenuItem +import android.view.View +import android.widget.LinearLayout.LayoutParams +import android.widget.* + +class AccountActivity : AppCompatActivity() { + + internal lateinit var acc: Account + internal lateinit var displayName: EditText + internal lateinit var aor: String + internal lateinit var authUser: EditText + internal lateinit var authPass: EditText + internal lateinit var outbound1: EditText + internal lateinit var outbound2: EditText + internal lateinit var regint: EditText + internal lateinit var mediaEnc: String + + private var newCodecs = ArrayList() + private var save = false + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + setContentView(R.layout.activity_account) + + val b = intent.extras + acc = Account.find(MainActivity.uas, b.getString("accp"))!! + aor = acc.aor + setTitle(aor.replace("sip:", "")) + + displayName = findViewById(R.id.DisplayName) as EditText + displayName.setText(acc.displayName) + + authUser = findViewById(R.id.AuthUser) as EditText + authUser.setText(acc.authUser) + + authPass = findViewById(R.id.AuthPass) as EditText + authPass.setText(acc.authPass) + + outbound1 = findViewById(R.id.Outbound1) as EditText + outbound2 = findViewById(R.id.Outbound2) as EditText + if (acc.outbound.size > 0) { + outbound1.setText(acc.outbound[0]) + if (acc.outbound.size > 1) + outbound2.setText(acc.outbound[1]) + } + + regint = findViewById(R.id.RegInt) as EditText + regint.setText(acc.regint.toString()) + + val audioCodecs = ArrayList(Utils.audio_codecs().split(",")) + newCodecs.addAll(audioCodecs) + while (newCodecs.size < audioCodecs.size) newCodecs.add("") + + val layout = findViewById(R.id.CodecSpinners) as LinearLayout + for (i in audioCodecs.indices) { + val spinner = Spinner(applicationContext) + spinner.id = i + 100 + spinner.layoutParams = LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT) + spinner.setPopupBackgroundResource(R.drawable.spinner_background) + layout.addView(spinner) + + var spinnerList = ArrayList() + if (acc.audioCodec.size > i) { + val codec = acc.audioCodec[i] + spinnerList.add(codec) + for (c in audioCodecs) if (c != codec) spinnerList.add(c) + spinnerList.add("") + } else { + spinnerList = audioCodecs + spinnerList.add(0, "") + } + val codecSpinner = findViewById(spinner.id) as Spinner + val adapter = ArrayAdapter(this,android.R.layout.simple_spinner_item, + spinnerList) + adapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item) + codecSpinner.adapter = adapter + codecSpinner.onItemSelectedListener = object : AdapterView.OnItemSelectedListener { + override fun onItemSelected(parent: AdapterView<*>, view: View, position: Int, id: Long) { + newCodecs.set(parent.id - 100, parent.selectedItem.toString()) + } + override fun onNothingSelected(parent: AdapterView<*>) { + } + } + } + + mediaEnc = acc.mediaenc + val mediaEncSpinner = findViewById(R.id.mediaEncSpinner) as Spinner + val mediaEncKeys = arrayListOf("zrtp", "dtls_srtp", "srtp", "srtp-mand", "") + val mediaEncVals = arrayListOf("ZRTP", "DTLS SRTP", "SRTP", "Madatory SRTP", "") + val keyIx = mediaEncKeys.indexOf(acc.mediaenc) + val keyVal = mediaEncVals.elementAt(keyIx) + mediaEncKeys.removeAt(keyIx) + mediaEncVals.removeAt(keyIx) + mediaEncKeys.add(0, acc.mediaenc) + mediaEncVals.add(0, keyVal) + val mediaEncAdapter = ArrayAdapter(this,android.R.layout.simple_spinner_item, + mediaEncVals) + mediaEncAdapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item) + mediaEncSpinner.adapter = mediaEncAdapter + mediaEncSpinner.onItemSelectedListener = object : AdapterView.OnItemSelectedListener { + override fun onItemSelected(parent: AdapterView<*>, view: View, position: Int, id: Long) { + mediaEnc = mediaEncKeys[mediaEncVals.indexOf(parent.selectedItem.toString())] + } + override fun onNothingSelected(parent: AdapterView<*>) { + } + } + } + + override fun onCreateOptionsMenu(menu: Menu): Boolean { + super.onCreateOptionsMenu(menu) + val inflater = menuInflater + inflater.inflate(R.menu.edit_menu, menu) + return true + } + + override fun onOptionsItemSelected(item: MenuItem): Boolean { + val intent = Intent(this, MainActivity::class.java) + when (item.itemId) { + R.id.save -> { + val dn = displayName.text.toString().trim() + if (dn != acc.displayName) { + if (checkDisplayName(dn)) { + if (account_set_display_name(acc.accp, dn) == 0) { + acc.displayName = account_display_name(acc.accp); + Log.d("Baresip", "New display name is ${acc.displayName}") + save = true + } else { + Log.e("Baresip", "Setting of display name failed") + } + } else { + Utils.alertView(this, "Notice", "Invalid Display Name: $dn") + return false + } + } + + val au = authUser.text.toString().trim() + if (au != acc.authUser) { + if (checkAuthUser(au)) { + if (account_set_auth_user(acc.accp, au) == 0) { + acc.authUser = account_auth_user(acc.accp); + Log.d("Baresip", "New auth user is ${acc.authUser}") + save = true + } else { + Log.e("Baresip", "Setting of auth user failed") + } + } else { + Utils.alertView(this, "Notice", + "Invalid Authentication UserName: $au") + return false + } + } + + val ap = authPass.text.toString().trim() + if (ap != acc.authPass) { + if (Utils.checkPrintASCII(ap)) { + if (account_set_auth_pass(acc.accp, ap) == 0) { + acc.authPass = account_auth_pass(acc.accp) + Log.d("Baresip", "New auth password is ${acc.authPass}") + save = true + } else { + Log.e("Baresip", "Setting of auth pass failed") + } + } else { + Utils.alertView(this, "Notice", + "Invalid Authentication Password: $ap") + return false + } + } + + val ob = ArrayList() + if (outbound1.text.toString().trim() != "") + ob.add(outbound1.text.toString().trim().replace(" ", "")) + if (outbound2.text.toString().trim() != "") + ob.add(outbound2.text.toString().trim().replace(" ", "")) + if (ob != acc.outbound) { + val outbound = ArrayList() + for (i in ob.indices) { + if ((ob[i] == "") || Utils.uri_decode(ob[i])) { + if (account_set_outbound(acc.accp, ob[i], i) == 0) { + if (ob[i] != "") + outbound.add(account_outbound(acc.accp, i)) + } else { + Log.e("Baresip", "Setting of outbound proxy ${ob[i]} failed") + break + } + } else { + Utils.alertView(this, "Notice", + "Invalid Outbound Proxy: ${ob[i]}") + return false + } + } + Log.d("Baresip", "New outbound proxies are ${outbound}") + acc.outbound = outbound + save = true + } + + val ri = regint.text.toString().trim() + if (Utils.checkUint(ri)) { + if (ri.toInt() != acc.regint) { + if (account_set_regint(acc.accp, ri.toInt()) == 0) { + acc.regint = account_regint(acc.accp) + Log.d("Baresip", "New regint is ${acc.regint}") + save = true + } else { + Log.e("Baresip", "Setting of regint $ri failed") + } + } + } else { + Utils.alertView(this, "Notice", + "Invalid Registration Interval: $ri") + return false + } + + val ac = ArrayList(LinkedHashSet(newCodecs.filter{it != ""} as ArrayList)) + if (ac != acc.audioCodec) { + Log.d("Baresip", "New codecs ${newCodecs.filter { it != "" }}") + val acParam = ";audio_codecs=" + Utils.implode(ac, ",") + if (account_set_audio_codecs(acc.accp, acParam) == 0) { + var i = 0 + while (true) { + val codec = account_audio_codec(acc.accp, i) + if (codec != "") { + Log.d("Baresip", "Found audio codec $codec") + i++ + } else { + break + } + } + acc.audioCodec = ac + save = true + } else { + Log.e("Baresip", "Setting of audio codecs $acParam failed") + } + } + + if (mediaEnc != acc.mediaenc) { + if (account_set_mediaenc(acc.accp, mediaEnc) == 0) { + acc.mediaenc = account_mediaenc(acc.accp) + Log.d("Baresip", "New mediaenc is ${acc.mediaenc}") + save = true + } else { + Log.e("Baresip", "Setting of mediaenc $mediaEnc failed") + } + } + + if (save) { + AccountsActivity.saveAccounts() + if (acc.regint > 0) { + Log.d("Baresip", "Registering ${acc.aor}") + UserAgent.ua_register(Account.findUA(acc.aor)!!.uap) + } + } + setResult(RESULT_OK, intent) + finish() + return true + } + R.id.cancel, android.R.id.home -> { + intent.putExtra("action", "cancel") + if (acc.regint > 0) { + Log.d("Baresip", "Registering ${acc.aor}") + UserAgent.ua_register(Account.findUA(acc.aor)!!.uap) + } + setResult(RESULT_OK, intent) + finish() + return true + } + else -> return super.onOptionsItemSelected(item) + } + } + + private fun checkDisplayName(dn: String): Boolean { + if (dn == "") return true + val dnRegex = Regex("^[a-zA-Z]([ ._-]|[a-zA-Z0-9]){1,63}\$") + return dnRegex.matches(dn) + } + + private fun checkAuthUser(au: String): Boolean { + if (au == "") return true + val ud = au.split("@") + val userIDRegex = Regex("^[a-zA-Z]([._-]|[a-zA-Z0-9]){1,63}\$") + val telnoRegex = Regex("^[+]?[0-9]{1,16}\$") + if (ud.size == 1) { + return userIDRegex.matches(ud[0]) || telnoRegex.matches(ud[0]) + } else { + return (userIDRegex.matches(ud[0]) || telnoRegex.matches(ud[0])) && + Utils.checkDomain(ud[1]) + } + } + +} + diff --git a/app/src/main/kotlin/com/tutpro/baresip/AccountListAdapter.kt b/app/src/main/kotlin/com/tutpro/baresip/AccountListAdapter.kt new file mode 100644 index 00000000..e8388409 --- /dev/null +++ b/app/src/main/kotlin/com/tutpro/baresip/AccountListAdapter.kt @@ -0,0 +1,61 @@ +package com.tutpro.baresip + +import android.content.Context +import android.content.Intent +import android.os.Bundle +import android.support.v7.app.AlertDialog +import android.util.Log +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import android.widget.ArrayAdapter +import android.widget.ImageButton +import android.widget.TextView + +import java.util.* + +class AccountListAdapter(private val cxt: Context, private val rows: ArrayList) : + ArrayAdapter(cxt, R.layout.account_row, rows) { + + override fun getView(position: Int, convertView: View?, parent: ViewGroup): View { + val row = rows[position] + val inflater = context.getSystemService(Context.LAYOUT_INFLATER_SERVICE) as LayoutInflater + val rowView = inflater.inflate(R.layout.account_row, parent, false) + val aorView = rowView.findViewById(R.id.aor) as TextView + aorView.text = row.aor + aorView.textSize = 20f + aorView.setPadding(6, 6, 0, 6) + aorView.setOnClickListener { _ -> + val i = Intent(cxt, AccountActivity::class.java) + val b = Bundle() + b.putString("accp", MainActivity.uas[position].account.accp) + i.putExtras(b) + cxt.startActivity(i) + } + val actionView = rowView.findViewById(R.id.action) as ImageButton + actionView.setImageResource(row.action) + actionView.setOnClickListener { _ -> + Log.d("Baresip", "Delete button clicked") + val deleteDialog = AlertDialog.Builder(cxt) + deleteDialog.setMessage("Do you want to delete account ${MainActivity.uas[position].aor}?") + deleteDialog.setPositiveButton("Delete") { dialog, _ -> + if (UserAgent.ua_isregistered(MainActivity.uas[position].uap)) { + UserAgent.ua_unregister(MainActivity.uas[position].uap) + } + UserAgent.ua_destroy(MainActivity.uas[position].uap) + MainActivity.uas.remove(MainActivity.uas[position]) + MainActivity.images.removeAt(position) + AccountsActivity.saveAccounts() + AccountsActivity.generateAccounts() + this.notifyDataSetChanged() + dialog.dismiss() + } + deleteDialog.setNegativeButton("No") { dialog, _ -> + dialog.dismiss() + } + deleteDialog.create().show() + } + return rowView + } + +} diff --git a/app/src/main/kotlin/com/tutpro/baresip/AccountRow.kt b/app/src/main/kotlin/com/tutpro/baresip/AccountRow.kt new file mode 100644 index 00000000..3f306fd9 --- /dev/null +++ b/app/src/main/kotlin/com/tutpro/baresip/AccountRow.kt @@ -0,0 +1,3 @@ +package com.tutpro.baresip + +class AccountRow(val aor: String, val action: Int) diff --git a/app/src/main/kotlin/com/tutpro/baresip/AccountsActivity.kt b/app/src/main/kotlin/com/tutpro/baresip/AccountsActivity.kt new file mode 100644 index 00000000..3fc79bec --- /dev/null +++ b/app/src/main/kotlin/com/tutpro/baresip/AccountsActivity.kt @@ -0,0 +1,100 @@ +package com.tutpro.baresip + +import android.app.Activity +import android.content.Intent +import android.os.Bundle +import android.os.SystemClock +import android.support.v7.app.AppCompatActivity +import android.util.Log +import android.view.MenuItem +import android.widget.* +import java.io.File + +import java.util.ArrayList + +class AccountsActivity : AppCompatActivity() { + + internal var aor: String = "" + + public override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + setContentView(R.layout.activity_accounts) + + val listview = findViewById(R.id.accounts) as ListView + generateAccounts() + val alAdapter = AccountListAdapter(this, accounts) + listview.adapter = alAdapter + + val addAccountButton = findViewById(R.id.addAccount) as ImageButton + val newAorView = findViewById(R.id.newAor) as EditText + addAccountButton.setOnClickListener{ + var aor = newAorView.text.toString() + if (!aor.startsWith("sip:")) aor = "sip:$aor" + if (!checkSipUri(aor)) { + Log.e("Baresip", "Invalid SIP URI $aor") + Utils.alertView(this, "Notice", + "Invalid SIP URI: $aor") + } else if (Account.exists(MainActivity.uas, aor)) { + Log.e("Baresip", "Account $aor already exists") + } else { + if (UserAgent.ua_alloc("<$aor>;regq=0.5;pubint=0;regint=600") != 0) + Log.e("Baresip", "Failed to allocate UA $aor") + else { + SystemClock.sleep(200); + newAorView.setText("") + newAorView.setHint("SIP URI user@domain") + newAorView.clearFocus() + saveAccounts() + recreate() + } + } + } + } + + override fun onOptionsItemSelected(item: MenuItem): Boolean { + when (item.itemId) { + android.R.id.home -> { + Log.d("Baresip", "Back array was pressed at Accounts") + val i = Intent() + setResult(Activity.RESULT_CANCELED, i) + finish() + } + } + return true + } + + private fun checkSipUri(uri: String): Boolean { + if (!uri.startsWith("sip:")) return false + val userDomain = uri.replace("sip:", "").split("@") + if (userDomain.size != 2) return false + if (!Utils.checkUserID(userDomain[0]) && !Utils.checkTelNo(userDomain[0])) + return false + return Utils.checkDomain(userDomain[1]) || Utils.checkIP(userDomain[1]) + } + + companion object { + + var accounts = ArrayList() + var posAtAccounts = ArrayList() + + fun generateAccounts() { + accounts.clear() + posAtAccounts.clear() + var i = 0; + for (ua in MainActivity.uas) { + accounts.add(AccountRow(ua.aor.replace("sip:", ""), R.drawable.action_remove)) + posAtAccounts.add(i) + i++ + } + } + + fun saveAccounts() { + var accounts = "" + for (a in Account.accounts()) accounts = accounts + a.print() + "\n" + val path = MainActivity.filesPath + "/accounts" + Utils.putFileContents(File(path), accounts) + Log.d("Baresip", "Saved accounts '${accounts}' to '$path") + } + } + +} diff --git a/app/src/main/kotlin/com/tutpro/baresip/Call.kt b/app/src/main/kotlin/com/tutpro/baresip/Call.kt index e58a5df1..f89d3863 100644 --- a/app/src/main/kotlin/com/tutpro/baresip/Call.kt +++ b/app/src/main/kotlin/com/tutpro/baresip/Call.kt @@ -3,31 +3,47 @@ package com.tutpro.baresip import android.text.TextWatcher import java.util.ArrayList -class Call(val ua: String, val call: String, val peerURI: String, var status: String, - val dtmfWatcher: TextWatcher?) { +class Call(val callp: String, val ua: UserAgent, val peerURI: String, val dir: String, + var status: String, val dtmfWatcher: TextWatcher?) { - var hold: Boolean = false - var security: Int = 0 - var zid: String = "" + var hold = false + var security = 0 + var zid = "" + var hasHistory = false companion object { - fun uaCalls(calls: ArrayList, ua: String): ArrayList { + fun calls(calls: ArrayList, dir: String): ArrayList { val result = ArrayList() for (i in calls.indices) { - if (calls[i].ua == ua) result.add(calls[i]) + if (calls[i].dir == dir) result.add(calls[i]) } return result } - fun callIndex(calls: ArrayList, ua: String, call: String): Int { + fun uaCalls(calls: ArrayList, ua: UserAgent, dir: String): ArrayList { + val result = ArrayList() for (i in calls.indices) { - if (calls[i].ua.equals(ua) && calls[i].call == call) - return i + if ((calls[i].ua == ua) && (calls[i].dir == dir)) result.add(calls[i]) } - return -1 + return result } + fun find(calls: ArrayList, callp: String): Call? { + for (c in calls) { + if (c.callp == callp) return c + } + return null + } + + fun index(calls: ArrayList, call: Call, dir: String): Int { + var res = 0 + for (c in calls) { + if (c == call) return res + if ((dir == "") || (call.dir == dir)) res++ + } + return res + } } } diff --git a/app/src/main/kotlin/com/tutpro/baresip/Codec.kt b/app/src/main/kotlin/com/tutpro/baresip/Codec.kt new file mode 100644 index 00000000..1a8157b6 --- /dev/null +++ b/app/src/main/kotlin/com/tutpro/baresip/Codec.kt @@ -0,0 +1,3 @@ +package com.tutpro.baresip + +class Codec(val name: String, val rate: Int, val channels: Int) {} \ No newline at end of file diff --git a/app/src/main/kotlin/com/tutpro/baresip/EditAccountsActivity.kt b/app/src/main/kotlin/com/tutpro/baresip/EditAccountsActivity.kt deleted file mode 100644 index 7fb58473..00000000 --- a/app/src/main/kotlin/com/tutpro/baresip/EditAccountsActivity.kt +++ /dev/null @@ -1,103 +0,0 @@ -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 - -class EditAccountsActivity : AppCompatActivity() { - - private var editText: EditText? = null - private var path: String? = null - private var file: File? = null - - override fun onCreate(savedInstanceState: Bundle?) { - super.onCreate(savedInstanceState) - setContentView(R.layout.activity_edit_accounts) - - editText = findViewById(R.id.editAccounts) as EditText - path = applicationContext.filesDir.absolutePath + "/accounts" - file = File(path!!) - var content: String - - if (!file!!.exists()) { - Log.e("Baresip", "Failed to find accounts file") - content = "No accounts" - } else { - Log.e("Baresip", "Found accounts file") - val length = file!!.length().toInt() - val bytes = ByteArray(length) - try { - val `in` = FileInputStream(file!!) - try { - `in`.read(bytes) - } finally { - `in`.close() - } - content = String(bytes) - } catch (e: java.io.IOException) { - 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, - resources.getDimension(R.dimen.textsize)) - editText!!.setText(content, TextView.BufferType.EDITABLE) - } - - override fun onCreateOptionsMenu(menu: Menu): Boolean { - super.onCreateOptionsMenu(menu) - val inflater = menuInflater - inflater.inflate(R.menu.edit_menu, menu) - return true - } - - override fun onOptionsItemSelected(item: MenuItem): Boolean { - val i = Intent() - when (item.itemId) { - R.id.save -> { - try { - val fOut = FileOutputStream(file!!.absoluteFile, false) - val fWriter = OutputStreamWriter(fOut) - val res = editText!!.text.toString() - try { - fWriter.write(res) - fWriter.close() - fOut.close() - } catch (e: java.io.IOException) { - Log.e("Baresip", "Failed to write accounts file: " + e.toString()) - } - - } catch (e: java.io.FileNotFoundException) { - Log.e("Baresip", "Failed to find accounts file: " + e.toString()) - } - - Log.d("Baresip", "Updated accounts file") - setResult(RESULT_OK, i) - finish() - return true - } - R.id.cancel, android.R.id.home -> { - setResult(RESULT_CANCELED, i) - finish() - return true - } - else -> return super.onOptionsItemSelected(item) - } - } -} - diff --git a/app/src/main/kotlin/com/tutpro/baresip/History.kt b/app/src/main/kotlin/com/tutpro/baresip/History.kt index f1fc8c49..ccc95d4a 100644 --- a/app/src/main/kotlin/com/tutpro/baresip/History.kt +++ b/app/src/main/kotlin/com/tutpro/baresip/History.kt @@ -3,8 +3,8 @@ package com.tutpro.baresip import java.io.Serializable import java.util.GregorianCalendar -class History(val ua: String, val call: String, val aor: String, val peerURI: String, - val direction: String, val connected: Boolean) : Serializable { +class History(val aor: String, val peerURI: String, val direction: String, + val connected: Boolean) : Serializable { val time: GregorianCalendar = GregorianCalendar() diff --git a/app/src/main/kotlin/com/tutpro/baresip/HistoryActivity.kt b/app/src/main/kotlin/com/tutpro/baresip/HistoryActivity.kt index 1840bf5c..034d70ec 100644 --- a/app/src/main/kotlin/com/tutpro/baresip/HistoryActivity.kt +++ b/app/src/main/kotlin/com/tutpro/baresip/HistoryActivity.kt @@ -30,7 +30,7 @@ class HistoryActivity : AppCompatActivity() { val b = intent.extras aor = b.getString("aor") - generate_ua_history(aor) + aorGenerateHistory(aor) val adapter = HistoryListAdapter(this, uaHistory) listview.adapter = adapter @@ -46,7 +46,7 @@ class HistoryActivity : AppCompatActivity() { when (which) { DialogInterface.BUTTON_POSITIVE -> { History.removeAt(posAtHistory[pos]) - generate_ua_history(aor) + aorGenerateHistory(aor) if (uaHistory.size == 0) { val i = Intent() setResult(Activity.RESULT_CANCELED, i) @@ -82,7 +82,7 @@ class HistoryActivity : AppCompatActivity() { return true } - private fun generate_ua_history(aor: String) { + private fun aorGenerateHistory(aor: String) { uaHistory.clear() posAtHistory.clear() for (i in History.indices.reversed()) { @@ -137,13 +137,6 @@ class HistoryActivity : AppCompatActivity() { return size } - fun callHasHistory(ua: String, call: String): Boolean { - for (h in History) { - if (h.ua == ua && h.call == call) return true - } - return false - } - fun aorRemoveHistory(aor: String) { for (h in History) { if (h.aor == aor) { diff --git a/app/src/main/kotlin/com/tutpro/baresip/MainActivity.kt b/app/src/main/kotlin/com/tutpro/baresip/MainActivity.kt index efc58250..154f62ed 100644 --- a/app/src/main/kotlin/com/tutpro/baresip/MainActivity.kt +++ b/app/src/main/kotlin/com/tutpro/baresip/MainActivity.kt @@ -1,7 +1,6 @@ package com.tutpro.baresip import android.Manifest -import android.app.Notification import android.app.Notification.VISIBILITY_PUBLIC import android.app.NotificationManager import android.app.PendingIntent @@ -35,14 +34,14 @@ import java.io.* class MainActivity : AppCompatActivity() { - internal lateinit var appContext: Context + lateinit var appContext: Context internal lateinit var layout: RelativeLayout internal lateinit var callee: AutoCompleteTextView internal lateinit var securityButton: ImageButton internal lateinit var callButton: Button internal lateinit var holdButton: Button internal lateinit var dtmf: EditText - internal lateinit var accountAdapter: AccountSpinnerAdapter + internal lateinit var uaAdapter: UaSpinnerAdapter internal lateinit var aorSpinner: Spinner internal lateinit var am: AudioManager internal lateinit var imm: InputMethodManager @@ -61,6 +60,7 @@ class MainActivity : AppCompatActivity() { callButton = findViewById(R.id.callButton) as Button holdButton = findViewById(R.id.holdButton) as Button dtmf = findViewById(R.id.dtmf) as EditText + filesPath = applicationContext.filesDir.absolutePath am = getSystemService(Context.AUDIO_SERVICE) as AudioManager imm = getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager @@ -78,15 +78,16 @@ class MainActivity : AppCompatActivity() { .setContent(RemoteViews(getPackageName(), R.layout.notification)) aorSpinner = findViewById(R.id.AoRList) as Spinner - accountAdapter = AccountSpinnerAdapter(applicationContext, accounts, images) - aorSpinner.adapter = accountAdapter + uaAdapter = UaSpinnerAdapter(applicationContext, uas, images) + aorSpinner.adapter = uaAdapter aorSpinner.onItemSelectedListener = object : AdapterView.OnItemSelectedListener { override fun onItemSelected(parent: AdapterView<*>, view: View, position: Int, id: Long) { - val aor = accounts[position].aoR - val ua = accounts[position].ua + val acc = uas[position].account + val aor = acc.aor + val ua = uas[position] Log.i("Baresip", "Setting $aor current") - val out = Call.uaCalls(callsOut, ua) - if (out.size == 0) { + val callsOut = Call.uaCalls(calls, ua, "out") + if (callsOut.size == 0) { callee.text.clear() callee.hint = "Callee" callButton.text = "Call" @@ -99,18 +100,18 @@ class MainActivity : AppCompatActivity() { securityButton.visibility = View.INVISIBLE dtmf.visibility = View.INVISIBLE } else { - callee.setText(out[0].peerURI) - callButton.text = out[0].status - if (out[0].status == "Hangup") { - if (out[0].hold) { + callee.setText(callsOut[0].peerURI) + callButton.text = callsOut[0].status + if (callsOut[0].status == "Hangup") { + if (callsOut[0].hold) { holdButton.text = "Unhold" } else { holdButton.text = "Hold" } holdButton.visibility = View.VISIBLE - securityButton.setImageResource(out[0].security) - Utils.setSecurityButtonTag(securityButton, out[0].security) - if (ua_mediaenc(ua) == "") + securityButton.setImageResource(callsOut[0].security) + setSecurityButtonTag(securityButton, callsOut[0].security) + if (acc.mediaenc == "") securityButton.visibility = View.INVISIBLE else securityButton.visibility = View.VISIBLE @@ -122,7 +123,7 @@ class MainActivity : AppCompatActivity() { dtmf.visibility = View.INVISIBLE } } - val `in` = Call.uaCalls(callsIn, ua) + val `in` = Call.uaCalls(calls, ua, "in") val view_count = layout.childCount Log.d("Baresip", "View count is $view_count") if (view_count > 7) { @@ -130,7 +131,7 @@ class MainActivity : AppCompatActivity() { layout.removeViews(7, view_count - 7) } for (c in `in`) { - for (call_index in callsIn.indices) { + for (call_index in Call.calls(calls, "in").indices) { addCallViews(ua, c, (call_index + 1) * 10) } } @@ -141,6 +142,23 @@ class MainActivity : AppCompatActivity() { } } + aorSpinner.setOnTouchListener { view, event -> + if (event.action == MotionEvent.ACTION_DOWN) { + if ((event.x - view.left) < 100) { + val i = Intent(this, AccountActivity::class.java) + val b = Bundle() + b.putString("accp", uas[aorSpinner.selectedItemPosition].account.accp) + i.putExtras(b) + startActivityForResult(i, ACCOUNT_CODE) + true + } else { + false + } + } else { + false + } + } + val assets = arrayOf("accounts", "contacts", "config", "busy.wav", "callwaiting.wav", "error.wav", "message.wav", "notfound.wav", "ring.wav", "ringback.wav") val path = applicationContext.filesDir.path @@ -159,7 +177,7 @@ class MainActivity : AppCompatActivity() { file = File("$path/$a") if (!file.exists()) { Log.d("Baresip", "Copying asset $a") - Utils.copyAssetToFile(applicationContext, a, "$path/$a") + copyAssetToFile(applicationContext, a, "$path/$a") } else { Log.d("Baresip", "Asset $a already copied") } @@ -207,11 +225,11 @@ class MainActivity : AppCompatActivity() { unverifyDialog.setMessage("This call is SECURE and peer is VERIFIED! " + "Do you want to unverify the peer?") unverifyDialog.setPositiveButton("Unverify") { dialog, _ -> - val out = Call.uaCalls(callsOut, accounts[aorSpinner.selectedItemPosition].ua) - if (out.size > 0) { - if (cmd_exec("zrtp_unverify " + out[0].zid) != 0) { + val outCalls = Call.uaCalls(calls, uas[aorSpinner.selectedItemPosition], "out") + if (outCalls.size > 0) { + if (Utils.cmd_exec("zrtp_unverify " + outCalls[0].zid) != 0) { Log.w("Baresip", - "Command 'zrtp_unverify ${out[0].zid}' failed") + "Command 'zrtp_unverify ${outCalls[0].zid}' failed") } else { securityButton.setImageResource(R.drawable.box_yellow) securityButton.tag = "yellow" @@ -229,8 +247,8 @@ class MainActivity : AppCompatActivity() { callButton.text = "Call" callButton.setOnClickListener { - val aor = accounts[aorSpinner.selectedItemPosition].aoR - val ua = accounts[aorSpinner.selectedItemPosition].ua + val ua = uas[aorSpinner.selectedItemPosition] + val aor = ua.aor when (callButton.text.toString()) { "Call" -> { val callee = (findViewById(R.id.callee) as EditText).text.toString() @@ -245,14 +263,16 @@ class MainActivity : AppCompatActivity() { } } "Cancel" -> { - Log.i("Baresip", "Canceling AoR $aor call " + - callsOut[0].call + " to " + (findViewById(R.id.callee) as EditText).text) - ua_hangup(ua, callsOut[0].call, 486, "Rejected") + val callp = Call.calls(calls, "out")[0].callp + Log.i("Baresip", "Canceling AoR $aor call $callp to " + + (findViewById(R.id.callee) as EditText).text) + ua_hangup(ua.uap, callp, 486, "Rejected") } "Hangup" -> { - Log.i("Baresip", "Hanging up AoR $aor call " + - callsOut[0].call + " to " + (findViewById(R.id.callee) as EditText).text) - ua_hangup(ua, callsOut[0].call,0, "") + val callp = Call.calls(calls, "out")[0].callp + Log.i("Baresip", "Hanging up AoR $aor call $callp to " + + (findViewById(R.id.callee) as EditText).text) + ua_hangup(ua.uap, callp,0, "") } } } @@ -262,20 +282,22 @@ class MainActivity : AppCompatActivity() { when (holdButton.text.toString()) { "Hold" -> { Log.i("Baresip", "Holding up " + (findViewById(R.id.callee) as EditText).text) - call_hold(callsOut[0].call) - callsOut[0].hold = true + val call = Call.calls(calls, "out")[0] + call_hold(call.callp) + call.hold = true holdButton.text = "Unhold" } "Unhold" -> { Log.i("Baresip", "Unholding " + (findViewById(R.id.callee) as EditText).text) - call_unhold(callsOut[0].call) - callsOut[0].hold = false + val call = Call.calls(calls, "out")[0] + call_unhold(call.callp) + call.hold = false holdButton.text = "Hold" } "History" -> { val i = Intent(this@MainActivity, HistoryActivity::class.java) val b = Bundle() - b.putString("aor", accounts[aorSpinner.selectedItemPosition].aoR) + b.putString("aor", uas[aorSpinner.selectedItemPosition].aor) i.putExtras(b) startActivityForResult(i, HISTORY_CODE) } @@ -337,8 +359,8 @@ class MainActivity : AppCompatActivity() { return true } R.id.accounts -> { - i = Intent(this, EditAccountsActivity::class.java) - startActivityForResult(i, EDIT_ACCOUNTS_CODE) + i = Intent(this, AccountsActivity::class.java) + startActivityForResult(i, ACCOUNTS_CODE) return true } R.id.contacts -> { @@ -372,7 +394,6 @@ class MainActivity : AppCompatActivity() { Log.w("Baresip", "OutputStream exception: " + e.toString()) e.printStackTrace() } - baresipStop() nm.cancel(10) running = false @@ -389,14 +410,11 @@ class MainActivity : AppCompatActivity() { when (requestCode) { - EDIT_ACCOUNTS_CODE -> { - if (resultCode == RESULT_OK) { - Utils.alertView(this, "Notice", - "You need to restart baresip in order to activate saved accounts!") - } - if (resultCode == RESULT_CANCELED) { + ACCOUNTS_CODE -> { + if (resultCode == RESULT_OK) + Log.d("Baresip", "Edit accounts OK") + if (resultCode == RESULT_CANCELED) Log.d("Baresip", "Edit accounts canceled") - } } EDIT_CONFIG_CODE -> { @@ -418,7 +436,7 @@ class MainActivity : AppCompatActivity() { } if (resultCode == RESULT_CANCELED) { Log.d("Baresip", "History canceled") - if (HistoryActivity.aorHistory(accounts[aorSpinner.selectedItemPosition].aoR) == 0) { + if (HistoryActivity.aorHistory(uas[aorSpinner.selectedItemPosition].aor) == 0) { (findViewById(R.id.holdButton) as Button).visibility = View.INVISIBLE } } @@ -430,12 +448,12 @@ class MainActivity : AppCompatActivity() { } } - private fun call(ua: String, uri: String) { + private fun call(ua: UserAgent, uri: String) { (findViewById(R.id.callee) as EditText).setText(uri) - Log.i("Baresip", "Calling $ua / $uri") - val call = ua_connect(ua, uri) + Log.i("Baresip", "Calling $ua.uap / $uri") + val call = ua_connect(ua.uap, uri) if (call != "") { - Log.i("Baresip", "Adding outgoing call $ua / $call / $uri") + Log.i("Baresip", "Adding outgoing call $ua.uap / $call / $uri") val dtmfWatcher = object: TextWatcher { override fun beforeTextChanged(sequence: CharSequence, start: Int, count: Int, after: Int) {} override fun onTextChanged(sequence: CharSequence, start: Int, before: Int, count: Int) { @@ -447,15 +465,16 @@ class MainActivity : AppCompatActivity() { call_send_digit(call, 4.toChar()) } } - callsOut.add(Call(ua, call, uri, "Cancel", dtmfWatcher)) + calls.add(Call(call, ua, uri, "out","Cancel", dtmfWatcher)) (findViewById(R.id.callButton) as Button).text = "Cancel" (findViewById(R.id.holdButton) as Button).visibility = View.INVISIBLE } } - private fun addCallViews(ua: String, call: Call, id: Int) { + private fun addCallViews(ua: UserAgent, call: Call, id: Int) { Log.d("Baresip", "Creating new Incoming textview at $id") + val acc = ua.account val caller_heading = TextView(appContext) caller_heading.text = "Incoming call from ..." caller_heading.setTextColor(Color.BLACK) @@ -480,7 +499,7 @@ class MainActivity : AppCompatActivity() { caller_row.layoutParams = caller_row_params val caller_uri = TextView(appContext) - caller_uri.text = callsIn[callsIn.size - 1].peerURI + caller_uri.text = call.peerURI caller_uri.setTextColor(Color.BLUE) caller_uri.textSize = 20f caller_uri.setPadding(10, 10, 0, 10) @@ -497,14 +516,42 @@ class MainActivity : AppCompatActivity() { val security_button_params = LinearLayout.LayoutParams(dp24px, dp24px, 0.0f) security_button_params.gravity = Gravity.CENTER_VERTICAL security_button.layoutParams = security_button_params - if ((call.security != 0) && (ua_mediaenc(ua) != "")) { + if ((call.security != 0) && (acc.mediaenc != "")) { security_button.setImageResource(call.security) - Utils.setSecurityButtonTag(securityButton, call.security) + setSecurityButtonTag(securityButton, call.security) security_button.visibility = View.VISIBLE } else { security_button.visibility = View.INVISIBLE } - Utils.setSecurityButtonOnClickListener(this, security_button, call) + security_button.setOnClickListener { + when (security_button.tag) { + "red" -> { + Utils.alertView(this, "Alert", "This call is NOT secure!") + } + "yellow" -> { + Utils.alertView(this, "Alert", + "This call is SECURE, but peer is NOT verified!") + } + "green" -> { + val unverifyDialog = AlertDialog.Builder(this) + unverifyDialog.setMessage("This call is SECURE and peer is VERIFIED! " + + "Do you want to unverify the peer?") + unverifyDialog.setPositiveButton("Unverify") { dialog, _ -> + if (Utils.cmd_exec("zrtp_unverify " + call.zid) != 0) { + Log.w("Baresip", "Command 'zrtp_unverify ${call.zid}' failed") + } else { + security_button.setImageResource(R.drawable.box_yellow) + security_button.tag = "yellow" + } + dialog.dismiss() + } + unverifyDialog.setNegativeButton("No") { dialog, _ -> + dialog.dismiss() + } + unverifyDialog.create().show() + } + } + } caller_row.addView(security_button) layout.addView(caller_row) @@ -516,20 +563,16 @@ class MainActivity : AppCompatActivity() { Log.d("Baresip", "Creating new answer button at " + (id + 4)) answer_button.id = id + 4 answer_button.setOnClickListener { v -> - val call_ua = call.ua - val call_call = call.call when ((v as Button).text.toString()) { "Answer" -> { - Log.i("Baresip", "UA " + call_ua + " accepting incoming call " + - call_call) - ua_answer(call_ua, call_call) - val final_in_index = Call.callIndex(callsIn, call_ua, call_call) - if (final_in_index >= 0) { + Log.i("Baresip", "UA ${call.ua.uap} accepting incoming call ${call.callp}") + ua_answer(call.ua.uap, call.callp) + if (call.dir == "in") { Log.d("Baresip", "Updating Hangup and Hold") - callsIn[final_in_index].status = "Hangup" - callsIn[final_in_index].hold = false + call.status = "Hangup" + call.hold = false this@MainActivity.runOnUiThread { - val answer_id = (final_in_index + 1) * 10 + 4 + val answer_id = (Call.index(calls, call,"in") + 1) * 10 + 4 val answer_but = layout.findViewById(answer_id) as Button answer_but.text = "Hangup" val reject_button = layout.findViewById(answer_id + 1) as Button @@ -538,9 +581,8 @@ class MainActivity : AppCompatActivity() { } } "Hangup" -> { - Log.i("Baresip", "UA " + call_ua + " hanging up call " + - call_call) - ua_hangup(call_ua, call_call, 200, "OK") + Log.i("Baresip", "UA ${call.ua.uap} hanging up call ${call.callp}") + ua_hangup(call.ua.uap, call.callp, 200, "OK") } else -> Log.e("Baresip", "Invalid answer button text: " + v.text.toString()) } @@ -569,16 +611,16 @@ class MainActivity : AppCompatActivity() { when ((v as Button).text.toString()) { "Reject" -> { Log.i("Baresip", "UA " + call.ua + - " rejecting incoming call " + call.call) - ua_hangup(call.ua, call.call, 486, "Rejected") + " rejecting incoming call " + call.callp) + ua_hangup(call.ua.uap, call.callp, 486, "Rejected") } "Hold" -> { - call_hold(call.call) + call_hold(call.callp) v.text = "Unhold" call.hold = true } "Unhold" -> { - call_unhold(call.call) + call_unhold(call.callp) v.text = "Hold" call.hold = false } @@ -596,14 +638,14 @@ class MainActivity : AppCompatActivity() { val contentView = RemoteViews(getPackageName(), R.layout.notification) for (i: Int in 0 .. 3) { val resID = resources.getIdentifier("status$i", "id", packageName); - if (i < accounts.size) { + if (i < images.size) { contentView.setImageViewResource(resID, images[i]) contentView.setViewVisibility(resID, View.VISIBLE) } else { contentView.setViewVisibility(resID, View.INVISIBLE) } } - if (accounts.size > 4) + if (images.size > 4) contentView.setViewVisibility(R.id.etc, View.VISIBLE) else contentView.setViewVisibility(R.id.etc, View.INVISIBLE) @@ -613,63 +655,70 @@ class MainActivity : AppCompatActivity() { @Keep fun addAccount(ua: String) { - val aor = ua_aor(ua) - Log.d("Baresip", "Adding account $ua with AoR $aor") - accounts.add(Account(ua, aor, ua_mediaenc(ua),"")) + val userAgent = UserAgent(ua) + uas.add(userAgent) images.add(R.drawable.dot_yellow) this@MainActivity.runOnUiThread { - accountAdapter.notifyDataSetChanged() + uaAdapter.notifyDataSetChanged() updateNotification() } } @Keep - private fun updateStatus(event: String, ua: String, call: String) { + private fun updateStatus(event: String, uap: String, callp: String) { - val aor = ua_aor(ua) + val ua = UserAgent.find(uas, uap) + if (ua == null) { + Log.e("Baresip", "Update status did not find ua $uap") + return + } + val aor = ua.aor + val acc = ua.account val ev = event.split(",") - Log.d("Baresip", "Handling event ${ev[0]} for $ua/$call/$aor") + Log.d("Baresip", "Handling event ${ev[0]} for $uap/$callp/$aor") - for (account_index in accounts.indices) { - if (accounts[account_index].aoR == aor) { + for (account_index in uas.indices) { + if (uas[account_index].aor == aor) { Log.d("Baresip", "Found AoR at index $account_index") when (ev[0]) { "registering", "unregistering" -> { } "registered" -> { Log.d("Baresip", "Setting status to green") - accounts[account_index].status = "OK" images[account_index] = R.drawable.dot_green this@MainActivity.runOnUiThread { - accountAdapter.notifyDataSetChanged() + uaAdapter.notifyDataSetChanged() updateNotification() } } "registering failed" -> { Log.d("Baresip", "Setting status to red") - accounts[account_index].status = "FAIL" images[account_index] = R.drawable.dot_red this@MainActivity.runOnUiThread { - accountAdapter.notifyDataSetChanged() + uaAdapter.notifyDataSetChanged() updateNotification() } } "call ringing" -> { } "call established" -> { - val out_index = Call.callIndex(callsOut, ua, call) - if (out_index != -1) { - Log.d("Baresip", "Outbound call $call established") - callsOut[out_index].status = "Hangup" - callsOut[out_index].hold = false - callsOut[out_index].security = R.drawable.box_red + val call = Call.find(calls, callp) + if (call == null) { + Log.e("Baresip", "Established call $callp not found") + return + } + if (call.dir == "out") { + Log.d("Baresip", "Outbound call $callp established") + call.status = "Hangup" + call.hold = false + call.security = R.drawable.box_red this@MainActivity.runOnUiThread { - if (ua == accounts[aorSpinner.selectedItemPosition].ua) { + if (ua == uas[aorSpinner.selectedItemPosition]) { callButton.text = "Hangup" holdButton.text = "Hold" holdButton.visibility = View.VISIBLE - if (ua_mediaenc(ua) == "") { + if (acc.mediaenc == "") { securityButton.visibility = View.INVISIBLE } else { securityButton.setImageResource(R.drawable.box_red) @@ -680,36 +729,34 @@ class MainActivity : AppCompatActivity() { dtmf.hint = "DTMF" dtmf.visibility = View.VISIBLE dtmf.requestFocus() - dtmf.addTextChangedListener(callsOut[out_index].dtmfWatcher) + dtmf.addTextChangedListener(call.dtmfWatcher) } } - HistoryActivity.History.add(History(ua, call, aor, call_peeruri(call), - "out", true)) + HistoryActivity.History.add(History(aor, call.peerURI, "out", + true)) + call.hasHistory = true } else { - val in_index = Call.callIndex(callsIn, ua, call) - if (in_index != -1) { - Log.d("Baresip", "Inbound call $call established") - callsIn[in_index].status = "Answer" - callsIn[in_index].security = R.drawable.box_red - this@MainActivity.runOnUiThread { - if (ua == accounts[aorSpinner.selectedItemPosition].ua) { - if (ua_mediaenc(ua) != "") { - var view_id = (in_index + 1) * 10 + 3 - val securityButton = findViewById(view_id) as ImageButton - securityButton.setImageResource(R.drawable.box_red) - securityButton.tag = "red" - securityButton.visibility = View.VISIBLE - view_id = (in_index + 1) * 10 + 5 - val rejectButton = findViewById(view_id) as Button - rejectButton.text = "Hold" - } + Log.d("Baresip", "Inbound call $callp established") + call.status = "Answer" + call.security = R.drawable.box_red + this@MainActivity.runOnUiThread { + if (ua == uas[aorSpinner.selectedItemPosition]) { + if (acc.mediaenc != "") { + val inIndex = Call.index(calls, call, "in") + var view_id = (inIndex + 1) * 10 + 3 + val securityButton = findViewById(view_id) as ImageButton + securityButton.setImageResource(R.drawable.box_red) + securityButton.tag = "red" + securityButton.visibility = View.VISIBLE + view_id = (inIndex + 1) * 10 + 5 + val rejectButton = findViewById(view_id) as Button + rejectButton.text = "Hold" } } - HistoryActivity.History.add(History(ua, call, aor, call_peeruri(call), - "in", true)) - } else { - Log.e("Baresip", "Unknown call $ua/$call established") } + HistoryActivity.History.add(History(aor, call.peerURI, "in", + true)) + call.hasHistory = true } am.mode = AudioManager.MODE_IN_COMMUNICATION am.isSpeakerphoneOn = false @@ -720,77 +767,67 @@ class MainActivity : AppCompatActivity() { HistoryActivity.aorRemoveHistory(aor) } "call incoming" -> { - val peer_uri = call_peeruri(call) - Log.d("Baresip", "Incoming call " + ua + "/" + call + "/" + - peer_uri) - val new_call = Call(ua, call, peer_uri, "Answer", null) - callsIn.add(new_call) + val peer_uri = call_peeruri(callp) + Log.d("Baresip", "Incoming call $uap/$callp/$peer_uri") + val new_call = Call(callp, ua, peer_uri, "in", "Answer", null) + calls.add(new_call) this@MainActivity.runOnUiThread { - if (ua == accounts[aorSpinner.selectedItemPosition].ua) { - addCallViews(ua, new_call, callsIn.size * 10) + if (ua == uas[aorSpinner.selectedItemPosition]) { + addCallViews(ua, new_call, Call.calls(calls, "in").size * 10) } } am.mode = AudioManager.MODE_RINGTONE // am.isSpeakerphoneOn = true } "call verify" -> { + val call = Call.find(calls, callp) + if (call == null) { + Log.e("Baresip", "Call $callp to be verified is not found") + return + } this@MainActivity.runOnUiThread { val verifyDialog = AlertDialog.Builder(this) verifyDialog.setTitle("Verify") verifyDialog.setMessage("Do you want to verify SAS <${ev[1]}> <${ev[2]}>?") verifyDialog.setPositiveButton("Yes") { dialog, _ -> val security: Int - if (cmd_exec("zrtp_verify ${ev[3]}") != 0) { + if (Utils.cmd_exec("zrtp_verify ${ev[3]}") != 0) { Log.w("Baresip", "Command 'zrtp_verify ${ev[3]}' failed") security = R.drawable.box_yellow } else { security = R.drawable.box_green } - var index = Call.callIndex(callsOut, ua, call) - if (index != -1) { - callsOut[index].security = security - callsOut[index].zid = ev[3] + call.security = security + call.zid = ev[3] + if (call.dir == "out") { securityButton.setImageResource(security) - Utils.setSecurityButtonTag(securityButton, security) + setSecurityButtonTag(securityButton, security) securityButton.visibility = View.VISIBLE } else { - index = Call.callIndex(callsIn, ua, call) - if (index != -1) { - callsIn[index].security = security - callsIn[index].zid = ev[3] - if (ua == accounts[aorSpinner.selectedItemPosition].ua) { - val view_id = (index + 1) * 10 + 3 - val securityButton = layout.findViewById(view_id) as ImageButton - securityButton.setImageResource(security) - Utils.setSecurityButtonTag(securityButton, security) - securityButton.visibility = View.VISIBLE - } + if (ua == uas[aorSpinner.selectedItemPosition]) { + val view_id = (Call.index(calls, call, "in") + 1) * 10 + 3 + val securityButton = layout.findViewById(view_id) as ImageButton + securityButton.setImageResource(security) + setSecurityButtonTag(securityButton, security) + securityButton.visibility = View.VISIBLE } } dialog.dismiss() } verifyDialog.setNegativeButton("No") { dialog, _ -> - var index = Call.callIndex(callsOut, ua, call) - if (index >= 0) { - callsOut[index].security = R.drawable.box_yellow - callsOut[index].zid = ev[3] + call.security = R.drawable.box_yellow + call.zid = ev[3] + if (call.dir == "out") { securityButton.setImageResource(R.drawable.box_yellow) securityButton.tag = "yellow" securityButton.visibility = View.VISIBLE } else { - index = Call.callIndex(callsIn, ua, call) - if (index != -1) { - callsIn[index].security = R.drawable.box_yellow - callsIn[index].zid = ev[3] - if (ua == accounts[aorSpinner.selectedItemPosition].ua) { - val view_id = (index + 1) * 10 + 3 - val securityButton = layout.findViewById(view_id) as ImageButton - securityButton.setImageResource(R.drawable.box_yellow) - securityButton.tag = "yellow" - securityButton.visibility = View.VISIBLE - } - } else { - Log.e("Baresip", "Unknown call $ua/$call to verify") + if (ua == uas[aorSpinner.selectedItemPosition]) { + val view_id = (Call.index(calls, call, "in") + 1) * 10 + 3 + val securityButton = layout.findViewById(view_id) as ImageButton + securityButton.setImageResource(R.drawable.box_yellow) + securityButton.tag = "yellow" + securityButton.visibility = View.VISIBLE } } dialog.dismiss() @@ -799,46 +836,48 @@ class MainActivity : AppCompatActivity() { } } "call verified" -> { - val out_index = Call.callIndex(callsOut, ua, call) - if (out_index != -1) { - callsOut[out_index].security = R.drawable.box_green - callsOut[out_index].zid = ev[1] + val call = Call.find(calls, callp) + if (call == null) { + Log.e("Baresip", "Call $callp that is verified is not found") + return + } + call.security = R.drawable.box_green + call.zid = ev[1] + if (call.dir == "out") { this@MainActivity.runOnUiThread { - if (ua == accounts[aorSpinner.selectedItemPosition].ua) { + if (ua == uas[aorSpinner.selectedItemPosition]) { securityButton.setImageResource(R.drawable.box_green) securityButton.tag = "green" securityButton.visibility = View.VISIBLE } } } else { - val in_index = Call.callIndex(callsIn, ua, call) - if (in_index != -1) { - callsIn[in_index].security = R.drawable.box_green - callsIn[in_index].zid = ev[1] - this@MainActivity.runOnUiThread { - if (ua == accounts[aorSpinner.selectedItemPosition].ua) { - val view_id = (in_index + 1) * 10 + 3 - val securityButton = layout.findViewById(view_id) as ImageButton - securityButton.setImageResource(R.drawable.box_green) - securityButton.tag = "green" - securityButton.visibility = View.VISIBLE - } + this@MainActivity.runOnUiThread { + if (ua == uas[aorSpinner.selectedItemPosition]) { + val view_id = (Call.index(calls, call, "in") + 1) * 10 + 3 + val securityButton = layout.findViewById(view_id) as ImageButton + securityButton.setImageResource(R.drawable.box_green) + securityButton.tag = "green" + securityButton.visibility = View.VISIBLE } - } else { - Log.e("Baresip", "Unknown call $ua/$call verified") } } } "call closed" -> { - val in_index = Call.callIndex(callsIn, ua, call) - if (in_index != -1) { - Log.d("Baresip", "Removing inbound call " + ua + "/" + - call + "/" + callsIn[in_index].peerURI) - val view_id = (in_index + 1) * 10 - val remove_count = callsIn.size - in_index - callsIn.removeAt(in_index) + val call = Call.find(calls, callp) + if (call == null) { + Log.e("Baresip", "Call $callp that is closed is not found") + return + } + if (call.dir == "in") { + Log.d("Baresip", "Removing inbound call $uap / $callp / " + + call.peerURI) + val inIndex = Call.index(calls, call, "in") + val view_id = (inIndex + 1) * 10 + val remove_count = Call.calls(calls, "in").size - inIndex + calls.removeAt(Call.index(calls, call, "")) this@MainActivity.runOnUiThread { - if (ua == accounts[aorSpinner.selectedItemPosition].ua) { + if (ua == uas[aorSpinner.selectedItemPosition]) { if (callButton.text == "Call") { holdButton.text = "History" holdButton.visibility = View.VISIBLE @@ -847,24 +886,23 @@ class MainActivity : AppCompatActivity() { val caller_heading = layout.findViewById(view_id) val view_index = layout.indexOfChild(caller_heading) layout.removeViews(view_index, 4 * remove_count) - for (i in in_index until callsIn.size) { + val callsIn = Call.calls(calls, "in") + for (i in inIndex until callsIn.size) { this@MainActivity.addCallViews(ua, callsIn[i], (i + 1) * 10) } } } - if (!HistoryActivity.callHasHistory(ua, call)) { + if (!call.hasHistory) { if (HistoryActivity.aorHistory(aor) > HISTORY_SIZE) HistoryActivity.aorRemoveHistory(aor) - HistoryActivity.History.add(History(ua, call, aor, call_peeruri(call), + HistoryActivity.History.add(History(aor, call.peerURI, "in", false)) } } else { - val out_index = Call.callIndex(callsOut, ua, call) - if (out_index != -1) { - Log.d("Baresip", "Removing outgoing call " + ua + "/" + - call + "/" + callsOut[out_index].peerURI) + Log.d("Baresip", "Removing outgoing call $uap / $callp / " + + call.peerURI) this@MainActivity.runOnUiThread { - if (ua == accounts[aorSpinner.selectedItemPosition].ua) { + if (ua == uas[aorSpinner.selectedItemPosition]) { callButton.text = "Call" callButton.isEnabled = true callee.setText("") @@ -875,24 +913,19 @@ class MainActivity : AppCompatActivity() { imm.hideSoftInputFromWindow(dtmf.getWindowToken(), 0) } securityButton.visibility = View.INVISIBLE - dtmf.removeTextChangedListener(callsOut[out_index].dtmfWatcher) + dtmf.removeTextChangedListener(call.dtmfWatcher) dtmf.visibility = View.INVISIBLE } - callsOut.removeAt(out_index) + calls.removeAt(Call.index(calls, call, "")) } - if (!HistoryActivity.callHasHistory(ua, call)) { + if (!call.hasHistory) { if (HistoryActivity.aorHistory(aor) > HISTORY_SIZE) HistoryActivity.aorRemoveHistory(aor) - HistoryActivity.History.add(History(ua, call, aor, call_peeruri(call), + HistoryActivity.History.add(History(aor, call.peerURI, "out", false)) } - } else { - Log.e("Baresip", "Unknown call " + ua + "/" + call + - " closed") - } } - if ((callsIn.size == 0) && (callsOut.size == 0)) - am.mode = AudioManager.MODE_NORMAL + if (calls.size == 0) am.mode = AudioManager.MODE_NORMAL } else -> Log.d("Baresip", "Unknown event '${ev[0]}'") } @@ -900,33 +933,56 @@ class MainActivity : AppCompatActivity() { } } + private 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) + } + } catch (e: IOException) { + Log.e("Baresip", "Failed to read asset " + asset + ": " + + e.toString()) + } + + } + + private fun setSecurityButtonTag(button: ImageButton, security: Int) { + when (security) { + R.drawable.box_red -> { button.tag = "red" } + R.drawable.box_yellow -> { button.tag = "yellow" } + R.drawable.box_green -> { button.tag = "green" } + } + } + external fun baresipStart(path: String) external fun baresipStop() - external fun call_peeruri(call: String): String - external fun ua_aor(ua: String): String - external fun ua_mediaenc(ua: String): String external fun ua_connect(ua: String, peer_uri: String): String external fun ua_answer(ua: String, call: String) external fun ua_hangup(ua: String, call: String, code: Int, reason: String) + external fun call_peeruri(call: String): String external fun call_hold(call: String): Int external fun call_unhold(call: String): Int external fun call_send_digit(call: String, digit: Char): Int - external fun cmd_exec(cmd: String): Int external fun reload_config(): Int companion object { internal var running: Boolean = false - internal var accounts = ArrayList() + var uas = ArrayList() internal var images = ArrayList() - internal var callsIn = ArrayList() - internal var callsOut = ArrayList() + internal var calls = ArrayList() + var filesPath = "" - const val EDIT_ACCOUNTS_CODE = 1 + const val ACCOUNTS_CODE = 1 const val EDIT_CONTACTS_CODE = 2 const val EDIT_CONFIG_CODE = 3 const val HISTORY_CODE = 4 const val ABOUT_CODE = 5 + const val ACCOUNT_CODE = 6 const val RECORD_AUDIO_PERMISSION = 1 const val HISTORY_SIZE = 100 diff --git a/app/src/main/kotlin/com/tutpro/baresip/UaSpinnerAdapter.kt b/app/src/main/kotlin/com/tutpro/baresip/UaSpinnerAdapter.kt new file mode 100644 index 00000000..018c8fe5 --- /dev/null +++ b/app/src/main/kotlin/com/tutpro/baresip/UaSpinnerAdapter.kt @@ -0,0 +1,37 @@ +package com.tutpro.baresip + +import android.content.Context +import android.util.Log +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 + +class UaSpinnerAdapter(private val cxt: Context, private val uas: ArrayList, + private val images: ArrayList) : + ArrayAdapter(cxt, android.R.layout.simple_spinner_item, images) { + + override fun getDropDownView(position: Int, convertView: View?, parent: ViewGroup): View { + return getImageForPosition(position, parent) + } + + override fun getView(position: Int, convertView: View?, parent: ViewGroup): View { + return getImageForPosition(position, parent) + } + + private fun getImageForPosition(position: Int, parent: ViewGroup): View { + val inflater = cxt.getSystemService(Context.LAYOUT_INFLATER_SERVICE) as LayoutInflater + val row = inflater.inflate(R.layout.account_spinner, parent, false) + val textView = row.findViewById(R.id.spinnerText) as TextView + textView.text = uas[position].aor.replace("sip:", "") + textView.textSize = 17f + val imageView = row.findViewById(R.id.spinnerImage) as ImageView + imageView.setImageResource(images[position]) + return row + } + +} diff --git a/app/src/main/kotlin/com/tutpro/baresip/UserAgent.kt b/app/src/main/kotlin/com/tutpro/baresip/UserAgent.kt new file mode 100644 index 00000000..d22b3e85 --- /dev/null +++ b/app/src/main/kotlin/com/tutpro/baresip/UserAgent.kt @@ -0,0 +1,35 @@ +package com.tutpro.baresip + +class UserAgent (val uap: String) { + + val account = Account(ua_account(uap)) + val aor = ua_aor(uap) + + companion object { + + external fun ua_alloc(uri: String): Int + external fun ua_destroy(uri: String) + external fun ua_register(ua: String): Int + external fun ua_isregistered(ua: String): Boolean + external fun ua_unregister(ua: String) + + fun userAgents(): ArrayList { + val res = ArrayList() + for (ua in MainActivity.uas) { + res.add(ua) + } + return res + } + + fun find(uas: ArrayList, uap: String): UserAgent? { + for (u in uas) { + if (u.uap == uap) return u + } + return null + } + } +} + +external fun ua_account(ua: String): String +external fun ua_aor(ua: String): String + diff --git a/app/src/main/kotlin/com/tutpro/baresip/Utils.kt b/app/src/main/kotlin/com/tutpro/baresip/Utils.kt index a7b13bab..2c56b32e 100644 --- a/app/src/main/kotlin/com/tutpro/baresip/Utils.kt +++ b/app/src/main/kotlin/com/tutpro/baresip/Utils.kt @@ -3,28 +3,11 @@ package com.tutpro.baresip import android.content.Context import android.support.v7.app.AlertDialog import android.util.Log -import android.widget.ImageButton + import java.io.* object Utils { - 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) - } - } catch (e: IOException) { - Log.e("Baresip", "Failed to read asset " + asset + ": " + - e.toString()) - } - - } - fun getFileContents(file: File): String { if (!file.exists()) { Log.e("Baresip", "Failed to find file: " + file.path) @@ -89,46 +72,51 @@ object Utils { return uri.substringAfter(":").substringBefore("@") } - fun setSecurityButtonTag(button: ImageButton, security: Int) { - when (security) { - R.drawable.box_red -> { button.tag = "red" } - R.drawable.box_yellow -> { button.tag = "yellow" } - R.drawable.box_green -> { button.tag = "green" } - } + fun checkUserID(id: String): Boolean { + return Regex("^[a-zA-Z]([._-]|[a-zA-Z0-9]){1,49}\$").matches(id) } - fun setSecurityButtonOnClickListener(context: Context, button: ImageButton, call: Call) { - button.setOnClickListener { - when (button.tag) { - "red" -> { - Utils.alertView(context, "Alert", "This call is NOT secure!") - } - "yellow" -> { - Utils.alertView(context, "Alert", - "This call is SECURE, but peer is NOT verified!") - } - "green" -> { - val unverifyDialog = AlertDialog.Builder(context) - unverifyDialog.setMessage("This call is SECURE and peer is VERIFIED! " + - "Do you want to unverify the peer?") - unverifyDialog.setPositiveButton("Unverify") { dialog, _ -> - if (cmd_exec("zrtp_unverify " + call.zid) != 0) { - Log.w("Baresip", "Command 'zrtp_unverify ${call.zid}' failed") - } else { - button.setImageResource(R.drawable.box_yellow) - button.tag = "yellow" - } - dialog.dismiss() - } - unverifyDialog.setNegativeButton("No") { dialog, _ -> - dialog.dismiss() - } - unverifyDialog.create().show() - } - } + fun checkTelNo(no: String): Boolean { + return Regex("^[+]?[0-9]{1,16}\$").matches(no) + } + + fun checkIP(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) + } + + fun checkDomain(domain: String): Boolean { + val parts = domain.split(".") + for (p in parts) { + if (p.length == 0 || p.endsWith("-") || !Regex("^[a-zA-z]([-]|[a-zA-Z0-9])+\$").matches(p)) + return false } + return true + } + + fun checkPrintASCII(s: String): Boolean { + if (s == "") return true + val printASCIIRegex = Regex("^[ -~]*\$") + return printASCIIRegex.matches(s) + } + + fun checkUint(s: String): Boolean { + val uintRegex = Regex("^[0-9]+\$") + return uintRegex.matches(s) + } + + fun implode(list: List, sep: String): String { + var res = "" + for (s in list) { + if (res == "") + res = s + else + res = res + sep + s + } + return res } external fun cmd_exec(cmd: String): Int + external fun uri_decode(uri: String): Boolean + external fun audio_codecs(): String } diff --git a/app/src/main/res/drawable/action_add.png b/app/src/main/res/drawable/action_add.png new file mode 100644 index 0000000000000000000000000000000000000000..326d0502881034dc45c11e2c3c2fedd9e6570a04 GIT binary patch literal 1028 zcmV+f1pE7mP)*&6Z@?vTkXmvsSCp(P^R3t|AlBQYTKP_=2uw*n-6miZBOi zEt^zC9Ozc(HZ~`dY473(^@BQ*x=KaV%{#lv94%&RSDW{9d~$CM=%>P+C!FNu<~h&r z|NPH$LinEtEhVUPt}uJs z1|v#I;tDLpD|=rsF6>S?2dvoQ6QS0i`FQIyQE3^(?@D3!J1-gMhp%jMZqb9+iIsOO zHy>_!x@b`WGP3VAV`^yDc?D~Fmka+*Uh{!lAGH9S+_LDc_ueqhy*#%xK=a$N2Deu< z`F-UOq(LM5hg>F$>ECB`3MF5?xACEhf^uc)mw5NY-pO!y^|06DRhnB^w&fM?QF+WR z%prL-`{j>M{dSf@17+k1xlZ+U4m5joK}1Svtdl>jHL>gS`OB2$#^=-|VVhJY0?=4a zo{4$M$)%#4lq$>XMEVMi9^dy-VpnQ2Mf#WNrD5NAcU#YTO_Rja0%#-|z`Hmlc+K{( zK54fpTa>pHODic)Wb=}Estm8wIZ0#3;_;c`WCun2%k;|X(D=6YZ46Mvi5DwifL2JF z$4q95P|(T%wRjIIM1)P$iF0xV4EU!4_UURiN&62Th))ltIw;)d)t6P%_=cw7jUKmx z%LNFXE70w8VC8U$D17dffYqXQn=YhHNSUyLq-7D1%>n6*LW<{SpCsZJhGYj-+)ui{ zQGL6%7L6VkhyhA)a!unR_yBk>%7cEY3EXB|wAe*wE`E*7LfVS4>Xe{d4$6Y0L6cu$ z?2p&cMJ4T^^hI>LD$&BhRBSC>)>y8J7fs^m$%%76AI_XWG0Yyyo+>wu zcsVk!Eix#oxFOD8IzUV#U z91wZLBd+h%%{$iyqa`Kwv-x@8$k?>;%X2B`7Wp@kC~sF2>uOQ&_1G08Q^4s@aVRl@ yEsg<&p@*?vYN#$g;6rwKFp>EcQ`ZFGWc&@z@#H93SJX`a0000`9BDX=?{V)aw4Qj zstbgOS19L2xt*E3`LU<5G^B{!EZ@Ah%kKBR_q|!rKVBFKj0FA@frEt?i*Zt(9eqKNgVy z1R23Z99<4i3XaA`%WE?$z!947f)KRkU$iX`fOl!~02*Hs+E}?d_XbM(}gV*=?)LY>Jps&M7bq9EXB;6>~;w5U*FVi@gbz(O#= zBApu?|5WX7#1=IoikCsHdWq_iiKXz>he7*@aDeWdqGH%?u%ROrPSvL?+BuW7>OOf! zG}|XiaQFP)V+CEBB0Bv_boN00004R>004l5008;`004mK004C`008P>0026e000+ooVrmw00002 zVoOIv0RM-N%)bBt010qNS#tmY4c7nw4c7reD4Tcy000McNliru;sp#09xaXPx-S3# z1!YM@K~!ko&6-_o6-5-s|FgXmF<7l1K!FGFqbdmkS^@^4piPJYLcxco!8>~kN_?Q0 zKrGQfT0Uw5N+K2>_l!ivQfU&^c@u7EjXQ4p5 zcb4sW`k$HZ+21`gJ9}oj41t??9@EikZ7aJO{Js1EmW%(Slrr&E*d$$AZC|ljIu?J{ zUlb!a!1y4mfzNaOtbzXGM1leo1=z`l>WCCj7R6gQpa(FJEH3Nw^m;yDUp(!IG(hg) z6Lm(^Jka!<{6z(%qjaKIA&NI-GtXB5$r>v!WExf5Kvx;VKSTv*4qgh2&2>N`8V^^#E#_i=qh49>85U#&d0e8nP&spqghn zcoHkj<&gRSGeq3fFMAW8b|DRa&oo`ZEOV7!Kmh4E&c|5wHO?DLd7|k&&UG_XtE`9} z(~JiIXONeMETZLDdK)4(npFYZwkNiT8hF37eqZb{4+4SiqkC@VK~_SkPR$L}R2d;7 ziuXjDxF{;sHohAx99=MMHW1)e|6Z1u$kzY>s&Lu-#BEPjQ!yXYso4_j2r-MSz>y!( zt8ROF3X5#b3usiB1KmvNC5uO7jl0AE=Fjx2tz~J5h~|D&+nSHa;hxTfqo#)XxSXz} zeU9_C+9SWVGmIDFkA#R=Wud}x#?b(`Cw2kHI~6zBh)~x>^AH`eQRDFDdIj*t zS#r@Kl&Jq>O-wD=r4fUG0SXmTO&B)FB~_p z5~K7U-kC>g@@Z(Kb~@`c@}+I;&t(lXTZOxdoUfT|#E~0ssGjXKKu1R)Ki{001R)MObuXVRU6W zV{&C-bY%cCFflkSFf}bPGE^}#Ix{djG&L(QGdeIZUBIT=0000bbVXQnWMOn=I&E)c wX=Zr + + + \ No newline at end of file diff --git a/app/src/main/res/layout/account_row.xml b/app/src/main/res/layout/account_row.xml new file mode 100644 index 00000000..a26b93a5 --- /dev/null +++ b/app/src/main/res/layout/account_row.xml @@ -0,0 +1,29 @@ + + + + + + + + + + diff --git a/app/src/main/res/layout/activity_account.xml b/app/src/main/res/layout/activity_account.xml new file mode 100644 index 00000000..f2c5c39e --- /dev/null +++ b/app/src/main/res/layout/activity_account.xml @@ -0,0 +1,152 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/layout/activity_accounts.xml b/app/src/main/res/layout/activity_accounts.xml new file mode 100644 index 00000000..a405cac6 --- /dev/null +++ b/app/src/main/res/layout/activity_accounts.xml @@ -0,0 +1,48 @@ + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/layout/history_row.xml b/app/src/main/res/layout/history_row.xml index f03231c2..00a9f25e 100644 --- a/app/src/main/res/layout/history_row.xml +++ b/app/src/main/res/layout/history_row.xml @@ -1,6 +1,6 @@