diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 1f668d14..5ed8cf75 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -9,14 +9,18 @@ + + + + @@ -36,9 +40,10 @@ android:roundIcon="@mipmap/ic_launcher" android:supportsRtl="true" android:theme="@style/AppTheme"> - + android:launchMode="singleTask"> @@ -47,7 +52,7 @@ + android:parentActivityName=".MainActivity"> @@ -64,8 +69,8 @@ + android:parentActivityName=".MainActivity" + android:windowSoftInputMode="adjustPan"> @@ -74,7 +79,7 @@ android:name=".ContactActivity" android:configChanges="orientation|keyboardHidden|screenSize" android:label="Contact" - android:parentActivityName=".MainActivity" > + android:parentActivityName=".MainActivity"> @@ -95,6 +100,22 @@ android:name="android.support.PARENT_ACTIVITY" android:value="com.tutpro.baresip.MainActivity" /> + + + + + + - - + - diff --git a/app/src/main/cpp/baresip.c b/app/src/main/cpp/baresip.c index a3d03158..43ee9e25 100644 --- a/app/src/main/cpp/baresip.c +++ b/app/src/main/cpp/baresip.c @@ -166,7 +166,7 @@ static void ua_event_handler(struct ua *ua, enum ua_event ev, } } jmethodID statusId = (*env)->GetMethodID(env, pctx->mainActivityClz, - "updateStatus", + "uaEvent", "(Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;)V"); sprintf(ua_buf, "%lu", (unsigned long)ua); jstring javaUA = (*env)->NewStringUTF(env, ua_buf); @@ -180,16 +180,71 @@ static void ua_event_handler(struct ua *ua, enum ua_event ev, (*env)->DeleteLocalRef(env, javaEvent); } -static void message_handler(const struct pl *peer, const struct pl *ctype, +static void message_handler(struct ua *ua, const struct pl *peer, const struct pl *ctype, struct mbuf *body, void *arg) { (void)ctype; (void)arg; + char ua_buf[32]; + char peer_buf[256]; + char msg_buf[1024]; + char utf_buf[2048]; + char *in, *out; + int size; LOGD("got message '%.*s' from peer '%.*s'", mbuf_get_left(body), mbuf_buf(body), peer->l, peer->p); - (void)play_file(NULL, baresip_player(), "message.wav", 0); + UpdateContext *pctx = (UpdateContext*)(&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 methodId = (*env)->GetMethodID(env, pctx->mainActivityClz, + "messageEvent", + "(Ljava/lang/String;Ljava/lang/String;[B)V"); + sprintf(ua_buf, "%lu", (unsigned long)ua); + jstring javaUA = (*env)->NewStringUTF(env, ua_buf); + sprintf(peer_buf, "%.*s", peer->l, peer->p); + jstring javaPeer = (*env)->NewStringUTF(env, peer_buf); + jbyteArray javaMsg; + size = mbuf_get_left(body); + javaMsg = (*env)->NewByteArray(env, size); + if ((*env)->GetArrayLength(env, javaMsg) != size) { + (*env)->DeleteLocalRef(env, javaMsg); + javaMsg = (*env)->NewByteArray(env, size); + } + void *temp = (*env)->GetPrimitiveArrayCritical(env, (jarray)javaMsg, 0); + memcpy(temp, mbuf_buf(body), size); + (*env)->ReleasePrimitiveArrayCritical(env, javaMsg, temp, 0); + LOGD("sending message %s/%s/%.*s\n", ua_buf, peer_buf, size, mbuf_buf(body)); + (*env)->CallVoidMethod(env, pctx->mainActivityObj, methodId, javaUA, javaPeer, javaMsg); + (*env)->DeleteLocalRef(env, javaUA); + (*env)->DeleteLocalRef(env, javaPeer); + (*env)->DeleteLocalRef(env, javaMsg); +} + +static void send_resp_handler(int err, const struct sip_msg *msg, void *arg) +{ + (void)arg; + + LOGD("received message response %u\n", msg->scode); + + if (err) { + (void)re_fprintf(stderr, " \x1b[31m%m\x1b[;m\n", err); + return; + } + + if (msg->scode >= 300) { + (void)re_fprintf(stderr, " \x1b[31m%u %r\x1b[;m\n", + msg->scode, &msg->reason); + } } #include @@ -332,7 +387,7 @@ Java_com_tutpro_baresip_BaresipService_baresipStart(JNIEnv *env, jobject instanc sprintf(ua_buf, "%lu", (unsigned long) ua); jstring javaUA = (*env)->NewStringUTF(env, ua_buf); LOGD("adding UA for AoR %s/%s\n", ua_aor(ua), ua_buf); - jmethodID accountId = (*env)->GetMethodID(env, pctx->mainActivityClz, "addUA", + jmethodID accountId = (*env)->GetMethodID(env, pctx->mainActivityClz, "uaAdd", "(Ljava/lang/String;)V"); (*env)->CallVoidMethod(env, pctx->mainActivityObj, accountId, javaUA); (*env)->DeleteLocalRef(env, javaUA); @@ -909,6 +964,27 @@ Java_com_tutpro_baresip_MainActivity_call_1send_1digit(JNIEnv *env, jobject thiz return res; } +JNIEXPORT jint JNICALL +Java_com_tutpro_baresip_MessageActivity_message_1send(JNIEnv *env, jobject thiz, jstring javaUA, + jstring javaPeer, jstring javaMsg) { + struct ua *ua; + const char *native_ua = (*env)->GetStringUTFChars(env, javaUA, 0); + const char *native_peer = (*env)->GetStringUTFChars(env, javaPeer, 0); + const char *native_msg = (*env)->GetStringUTFChars(env, javaMsg, 0); + + LOGD("sending message from ua %s to %s\n", native_ua, native_peer); + ua = (struct ua *)strtoul(native_ua, NULL, 10); + int err = message_send(ua, native_peer, native_msg, send_resp_handler, NULL); + if (err) { + LOGW("message_send failed with error %d\n", err); + } + + (*env)->ReleaseStringUTFChars(env, javaUA, native_ua); + (*env)->ReleaseStringUTFChars(env, javaPeer, native_peer); + (*env)->ReleaseStringUTFChars(env, javaMsg, native_msg); + return err; +} + JNIEXPORT void JNICALL Java_com_tutpro_baresip_ContactsActivity_00024Companion_contacts_1remove(JNIEnv *env, jobject thiz) { struct le *le; diff --git a/app/src/main/kotlin/com/tutpro/baresip/Account.kt b/app/src/main/kotlin/com/tutpro/baresip/Account.kt index 622adbed..304d12c7 100644 --- a/app/src/main/kotlin/com/tutpro/baresip/Account.kt +++ b/app/src/main/kotlin/com/tutpro/baresip/Account.kt @@ -91,7 +91,7 @@ class Account(val accp: String) { return null } - fun findUA(aor: String): UserAgent? { + fun findUa(aor: String): UserAgent? { for (ua in MainActivity.uas) { if (ua.account.aor == aor) return ua } diff --git a/app/src/main/kotlin/com/tutpro/baresip/BaresipService.kt b/app/src/main/kotlin/com/tutpro/baresip/BaresipService.kt index ff34b88b..4b5b583d 100644 --- a/app/src/main/kotlin/com/tutpro/baresip/BaresipService.kt +++ b/app/src/main/kotlin/com/tutpro/baresip/BaresipService.kt @@ -21,6 +21,10 @@ import java.io.ObjectInputStream import android.support.v4.content.LocalBroadcastManager import android.os.Build import android.support.v4.app.NotificationCompat.VISIBILITY_PRIVATE +import java.nio.charset.StandardCharsets +import kotlin.experimental.and +import android.R.attr.name +import java.nio.charset.Charset class BaresipService: Service() { @@ -120,6 +124,21 @@ class BaresipService: Service() { } } + file = File(path, "messages") + if (file.exists()) { + try { + val fis = FileInputStream(file) + val ois = ObjectInputStream(fis) + @SuppressWarnings("unchecked") + MainActivity.messages = ois.readObject() as ArrayList + Log.d(LOG_TAG, "Restored ${MainActivity.messages.size} messages") + ois.close() + fis.close() + } catch (e: Exception) { + Log.w(LOG_TAG, "InputStream exception: - " + e.toString()) + } + } + ContactsActivity.generateContacts(path + "/contacts") Thread(Runnable { baresipStart(path) }).start() @@ -191,7 +210,7 @@ class BaresipService: Service() { } @Keep - fun addUA(uap: String) { + fun uaAdd(uap: String) { Log.d(LOG_TAG, "addUA at BaresipService") val ua = UserAgent(uap) MainActivity.uas.add(ua) @@ -205,14 +224,13 @@ class BaresipService: Service() { } val intent = Intent("service event") intent.putExtra("event", "ua added") - intent.putExtra("uap", uap) - intent.putExtra("callp", "") + intent.putExtra("params", arrayListOf(uap)) LocalBroadcastManager.getInstance(this).sendBroadcast(intent) updateStatusNotification() } @Keep - fun updateStatus(event: String, uap: String, callp: String) { + fun uaEvent(event: String, uap: String, callp: String) { Log.d(LOG_TAG, "updateStatus got event $event/$uap/$callp") if (!IS_SERVICE_RUNNING) return val ua = UserAgent.find(MainActivity.uas, uap) @@ -295,11 +313,76 @@ class BaresipService: Service() { } val intent = Intent("service event") intent.putExtra("event", event) - intent.putExtra("uap", uap) - intent.putExtra("callp", callp) + intent.putExtra("params", arrayListOf(uap, callp)) LocalBroadcastManager.getInstance(this).sendBroadcast(intent) } - + + @Keep + fun messageEvent(uap: String, peer: String, msg: ByteArray) { + Log.d(LOG_TAG, "message event $uap/$peer") + var s = "Decoding of message failed!" + try { + s = String(msg, StandardCharsets.UTF_8) + Log.d(LOG_TAG, "UTF-8 $s") + } catch (e: Exception) { + Log.e(LOG_TAG, "UTF-8 decode failed") + } + val timeStamp = System.currentTimeMillis().toString() + if (!Utils.isVisible()) { + val cnb = NotificationCompat.Builder(this, HIGH_CHANNEL_ID) + cnb.setSmallIcon(R.drawable.ic_stat) + .setColor(0x0ca1fd) + .setContentIntent(npi) + .setDefaults(Notification.DEFAULT_SOUND) + .setAutoCancel(true) + .setContentTitle("Message from ${ContactsActivity.contactName(peer)}") + .setContentText(s) + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) { + cnb.setVibrate(LongArray(0)) + .setVisibility(VISIBILITY_PRIVATE) + .setPriority(Notification.PRIORITY_HIGH) + } + /* val view = RemoteViews(getPackageName(), R.layout.call_notification) + view.setTextViewText(R.id.callFrom, "Call from ${Api.call_peeruri(callp)}") + cnb.setContent(view) */ + val replyIntent = Intent(this, MainActivity::class.java) + replyIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK or + Intent.FLAG_ACTIVITY_SINGLE_TOP or Intent.FLAG_ACTIVITY_CLEAR_TOP) + replyIntent.putExtra("action", "reply") + replyIntent.putExtra("uap", uap) + replyIntent.putExtra("peer", peer) + val replyPendingIntent = PendingIntent.getActivity(this, + 0, replyIntent, PendingIntent.FLAG_UPDATE_CURRENT) + val archiveIntent = Intent(this, MainActivity::class.java) + archiveIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK or + Intent.FLAG_ACTIVITY_SINGLE_TOP) + archiveIntent.putExtra("action", "archive") + archiveIntent.putExtra("uap", uap) + archiveIntent.putExtra("time", timeStamp) + val archivePendingIntent = PendingIntent.getActivity(this, + 1, archiveIntent, PendingIntent.FLAG_UPDATE_CURRENT) + val deleteIntent = Intent(this, MainActivity::class.java) + deleteIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK or + Intent.FLAG_ACTIVITY_SINGLE_TOP) + deleteIntent.putExtra("action", "delete") + deleteIntent.putExtra("uap", uap) + deleteIntent.putExtra("time", timeStamp) + val deletePendingIntent = PendingIntent.getActivity(this, + 2, deleteIntent, PendingIntent.FLAG_UPDATE_CURRENT) + /* view.setOnClickPendingIntent(R.id.answerButton, answerPendingIntent) + view.setOnClickPendingIntent(R.id.rejectButton, rejectPendingIntent) + cnb.setCustomBigContentView(view) */ + cnb.addAction(R.drawable.ic_stat, "Reply", replyPendingIntent) + cnb.addAction(R.drawable.ic_stat, "Archive", archivePendingIntent) + cnb.addAction(R.drawable.ic_stat, "Delete", deletePendingIntent) + nm.notify(MESSAGE_NOTIFICATION_ID, cnb.build()) + } + val intent = Intent("service event") + intent.putExtra("event", "message") + intent.putExtra("params", arrayListOf(uap, peer, s, timeStamp)) + LocalBroadcastManager.getInstance(this).sendBroadcast(intent) + } + private fun updateStatusNotification() { val contentView = RemoteViews(getPackageName(), R.layout.status_notification) for (i: Int in 0 .. 5) { @@ -321,9 +404,11 @@ class BaresipService: Service() { private fun cleanStop() { HistoryActivity.saveHistory() + MessagesActivity.saveMessages() MainActivity.uas.clear() MainActivity.images.clear() MainActivity.history.clear() + MainActivity.messages.clear() baresipStop() BaresipService.IS_SERVICE_RUNNING = false unregisterReceiver(nr) @@ -339,8 +424,9 @@ class BaresipService: Service() { var IS_SERVICE_RUNNING = false val STATUS_NOTIFICATION_ID = 101 - val DEFAULT_CHANNEL_ID = "com.tutpro.baresip.default" val CALL_NOTIFICATION_ID = 102 + val MESSAGE_NOTIFICATION_ID = 103 + val DEFAULT_CHANNEL_ID = "com.tutpro.baresip.default" val HIGH_CHANNEL_ID = "com.tutpro.baresip.high" var disconnected = false val RUN_FOREGROUNG = false diff --git a/app/src/main/kotlin/com/tutpro/baresip/MainActivity.kt b/app/src/main/kotlin/com/tutpro/baresip/MainActivity.kt index 9c4b8e35..781accda 100644 --- a/app/src/main/kotlin/com/tutpro/baresip/MainActivity.kt +++ b/app/src/main/kotlin/com/tutpro/baresip/MainActivity.kt @@ -29,6 +29,7 @@ import android.widget.RelativeLayout import android.widget.* import android.view.* import java.util.* +import kotlin.collections.ArrayList class MainActivity : AppCompatActivity() { @@ -39,7 +40,8 @@ class MainActivity : AppCompatActivity() { internal lateinit var securityButton: ImageButton internal lateinit var callButton: ImageButton internal lateinit var holdButton: ImageButton - internal lateinit var historyButton: ImageButton + internal lateinit var messagesButton: ImageButton + internal lateinit var callsButton: ImageButton internal lateinit var dtmf: EditText internal lateinit var uaAdapter: UaSpinnerAdapter internal lateinit var aorSpinner: Spinner @@ -70,7 +72,8 @@ class MainActivity : AppCompatActivity() { securityButton = findViewById(R.id.securityButton) as ImageButton callButton = findViewById(R.id.callButton) as ImageButton holdButton = findViewById(R.id.holdButton) as ImageButton - historyButton = findViewById(R.id.historyButton) as ImageButton + messagesButton = findViewById(R.id.messagesButton) as ImageButton + callsButton = findViewById(R.id.callsButton) as ImageButton dtmf = findViewById(R.id.dtmf) as EditText am = getSystemService(Context.AUDIO_SERVICE) as AudioManager @@ -83,8 +86,7 @@ class MainActivity : AppCompatActivity() { serviceEventReceiver = object : BroadcastReceiver() { override fun onReceive(context: Context, intent: Intent) { handleServiceEvent(intent.getStringExtra("event"), - intent.getStringExtra("uap"), - intent.getStringExtra("callp")) + intent.getStringArrayListExtra("params")) } } LocalBroadcastManager.getInstance(this).registerReceiver(serviceEventReceiver, @@ -114,11 +116,6 @@ class MainActivity : AppCompatActivity() { callee.hint = "Callee" callButton.tag = "Call" callButton.setImageResource(R.drawable.call) - if (History.aorHistory(history, aor) > 0) { - historyButton.visibility = View.VISIBLE - } else { - historyButton.visibility = View.INVISIBLE - } securityButton.visibility = View.INVISIBLE dtmf.visibility = View.INVISIBLE } else { @@ -147,8 +144,8 @@ class MainActivity : AppCompatActivity() { } val view_count = layout.childCount // Log.d("Baresip", "View count is $view_count") - if (view_count > 7) - layout.removeViews(7, view_count - 7) + if (view_count > 8) + layout.removeViews(8, view_count - 8) for (c in Call.uaCalls(calls, ua, "in")) for (call_index in Call.uaCalls(calls, ua, "in").indices) { val startIndex = (call_index + 1) * 10 @@ -294,8 +291,16 @@ class MainActivity : AppCompatActivity() { } } - historyButton.visibility = View.INVISIBLE - historyButton.setOnClickListener { + messagesButton.setOnClickListener { + val i = Intent(this@MainActivity, MessagesActivity::class.java) + val b = Bundle() + b.putString("aor", uas[aorSpinner.selectedItemPosition].account.aor) + i.putExtras(b) + startActivityForResult(i, MESSAGES_CODE) + } + + callsButton.visibility = View.VISIBLE + callsButton.setOnClickListener { val i = Intent(this@MainActivity, HistoryActivity::class.java) val b = Bundle() b.putString("aor", uas[aorSpinner.selectedItemPosition].account.aor) @@ -313,16 +318,17 @@ class MainActivity : AppCompatActivity() { moveTaskToBack(true) } - private fun handleServiceEvent(event: String, uap: String, callp: String) { + private fun handleServiceEvent(event: String, params: ArrayList) { + val uap = params[0] val ua = UserAgent.find(uas, uap) if (ua == null) { Log.w("Baresip", "handleServiceEvent '$event' did not find ua $uap") return } + val ev = event.split(",") + Log.d("Baresip", "Handling service event ${ev[0]} for $uap") val aor = ua.account.aor val acc = ua.account - val ev = event.split(",") - Log.d("Baresip", "Handling service event ${ev[0]} for $uap/$callp/$aor") for (account_index in uas.indices) { if (uas[account_index].account.aor == aor) { when (ev[0]) { @@ -334,6 +340,7 @@ class MainActivity : AppCompatActivity() { uaAdapter.notifyDataSetChanged() } "call incoming" -> { + val callp = params[1] val peer_uri = Api.call_peeruri(callp) val new_call = Call(callp, ua, peer_uri, "in", "Answer", null) if (ONE_CALL_ONLY && (calls.size > 0)) { @@ -359,6 +366,7 @@ class MainActivity : AppCompatActivity() { } } "call established" -> { + val callp = params[1] val call = Call.find(calls, callp) if (call == null) { Log.e("Baresip", "Established call $callp not found") @@ -424,6 +432,7 @@ class MainActivity : AppCompatActivity() { History.aorRemoveHistory(history, aor) } "call verify" -> { + val callp = params[1] val call = Call.find(calls, callp) if (call == null) { Log.e("Baresip", "Call $callp to be verified is not found") @@ -478,6 +487,7 @@ class MainActivity : AppCompatActivity() { verifyDialog.create().show() } "call verified", "call secure" -> { + val callp = params[1] val call = Call.find(calls, callp) if (call == null) { Log.e("Baresip", "Call $callp that is verified is not found") @@ -509,6 +519,7 @@ class MainActivity : AppCompatActivity() { } } "call closed" -> { + val callp = params[1] val call = Call.find(calls, callp) if (call == null) { Log.d("Baresip", "Call $callp that is closed is not found") @@ -534,7 +545,7 @@ class MainActivity : AppCompatActivity() { addCallViews(ua, callsIn[i], (i + 1) * 10) } } - historyButton.visibility = View.VISIBLE + callsButton.visibility = View.VISIBLE } if (!call.hasHistory) { if (History.aorHistory(history, aor) > HISTORY_SIZE) @@ -558,7 +569,8 @@ class MainActivity : AppCompatActivity() { securityButton.visibility = View.INVISIBLE dtmf.removeTextChangedListener(call.dtmfWatcher) dtmf.visibility = View.INVISIBLE - historyButton.visibility = View.VISIBLE + messagesButton.visibility = View.VISIBLE + callsButton.visibility = View.VISIBLE } calls.remove(call) if (!call.hasHistory) { @@ -570,6 +582,25 @@ class MainActivity : AppCompatActivity() { } if (calls.size == 0) am.mode = AudioManager.MODE_NORMAL } + "message" -> { + val peer_uri = params[1] + val msg = params[2] + val time = params[3] + val new_message = Message(aor, peer_uri, R.drawable.arrow_down_green, msg, + time.toLong(), true) + Log.d("Baresip", "Incoming message $aor/$peer_uri/$msg") + messages.add(new_message) + if (Utils.isVisible()) { + if (ua != uas[aorSpinner.selectedItemPosition]) + aorSpinner.setSelection(account_index) + val i = Intent(applicationContext, MessagesActivity::class.java) + i.setFlags(Intent.FLAG_ACTIVITY_REORDER_TO_FRONT) + val b = Bundle() + b.putString("aor", ua.account.aor) + i.putExtras(b) + startActivity(i) + } + } else -> Log.w("Baresip", "Unknown event '${ev[0]}'") } break @@ -581,16 +612,52 @@ class MainActivity : AppCompatActivity() { setIntent(intent) val action = intent.getStringExtra("action") Log.d("Baresip", "Got onNewIntent action $action") - if (action == null) return - val callp = intent.getStringExtra("callp") when (action) { "answer" -> { - answerCall = callp + answerCall = intent.getStringExtra("callp") } "reject" -> { - rejectCall = callp + rejectCall = intent.getStringExtra("callp") moveTaskToBack(true) } + "reply", "archive", "delete" -> { + val uap = intent.getStringExtra("uap") + val ua = UserAgent.find(MainActivity.uas, uap) + if (ua == null) { + Log.e("Baresip", "onNewIntent did not find ua $uap") + return + } + val aor = ua.account.aor + when (action) { + "reply" -> { + val i = Intent(this@MainActivity, MessagesActivity::class.java) + val b = Bundle() + if (ua != uas[aorSpinner.selectedItemPosition]) { + for (account_index in uas.indices) { + if (uas[account_index].account.aor == aor) { + aorSpinner.setSelection(account_index) + break + } + } + } + b.putString("aor", aor) + b.putString("peer", intent.getStringExtra("peer")) + i.putExtras(b) + startActivityForResult(i, MESSAGES_CODE) + } + "archive" -> { + MessagesActivity.archiveUaMessage(ua.account.aor, + intent.getStringExtra("time").toLong()) + moveTaskToBack(true) + } + "delete" -> { + MessagesActivity.deleteUaMessage(ua.account.aor, + intent.getStringExtra("time").toLong()) + moveTaskToBack(true) + } + } + nm.cancel(BaresipService.MESSAGE_NOTIFICATION_ID) + } } } @@ -605,13 +672,13 @@ class MainActivity : AppCompatActivity() { Log.d("Baresip", "Resumed") imm.hideSoftInputFromWindow(callee.windowToken, 0) visible = true - if ((answerCall != "") && (layout.childCount > 7)) { + if ((answerCall != "") && (layout.childCount > 8)) { answerCall = "" /* if multiple calls, right answer button needs to be searched */ val answerButton = layout.findViewById(10 + 5) as ImageButton answerButton.performClick() } - if ((rejectCall != "") && (layout.childCount > 7)) { + if ((rejectCall != "") && (layout.childCount > 8)) { rejectCall = "" /* if multiple calls, right reject button needs to be searched */ val rejectButton = layout.findViewById(10 + 6) as ImageButton @@ -996,6 +1063,7 @@ class MainActivity : AppCompatActivity() { internal var images = ArrayList() var history: ArrayList = ArrayList() internal var calls = ArrayList() + internal var messages = ArrayList() var filesPath = "" var visible = true var answerCall = "" @@ -1008,9 +1076,12 @@ class MainActivity : AppCompatActivity() { const val ABOUT_CODE = 5 const val ACCOUNT_CODE = 6 const val CONTACT_CODE = 7 + const val MESSAGES_CODE = 8 + const val MESSAGE_CODE = 9 const val RECORD_AUDIO_PERMISSION = 1 const val HISTORY_SIZE = 100 + const val MESSAGE_HISTORY_SIZE = 100 const val ONE_CALL_ONLY = true } diff --git a/app/src/main/kotlin/com/tutpro/baresip/Message.kt b/app/src/main/kotlin/com/tutpro/baresip/Message.kt new file mode 100644 index 00000000..9d200950 --- /dev/null +++ b/app/src/main/kotlin/com/tutpro/baresip/Message.kt @@ -0,0 +1,7 @@ +package com.tutpro.baresip + +import java.io.Serializable + +class Message(val aor: String, val peerURI: String, val direction: Int, val message: String, + val timeStamp: Long, var new: Boolean): Serializable { +} diff --git a/app/src/main/kotlin/com/tutpro/baresip/MessageActivity.kt b/app/src/main/kotlin/com/tutpro/baresip/MessageActivity.kt new file mode 100644 index 00000000..6d85f7ac --- /dev/null +++ b/app/src/main/kotlin/com/tutpro/baresip/MessageActivity.kt @@ -0,0 +1,102 @@ +package com.tutpro.baresip + +import android.app.Activity +import android.content.Context +import android.content.Intent +import android.os.Bundle +import android.support.v7.app.AppCompatActivity +import android.util.Log +import android.view.MenuItem +import android.view.inputmethod.InputMethodManager +import android.widget.* + +class MessageActivity : AppCompatActivity() { + + internal lateinit var receiver: AutoCompleteTextView + internal lateinit var message: EditText + internal lateinit var sendButton: ImageButton + internal lateinit var imm: InputMethodManager + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + + setContentView(R.layout.activity_message) + + val aor = intent.extras.getString("aor") + val peerURI = intent.extras.getString("peer") + + val ua = Account.findUa(aor) + if (ua == null) { + Log.e("Baresip", "MessageActivity did not find ua of $aor") + val i = Intent() + setResult(Activity.RESULT_CANCELED, i) + finish() + } + + val title = findViewById(R.id.messageToTitle) as TextView + message = findViewById(R.id.text) as EditText + receiver = findViewById(R.id.receiver) as AutoCompleteTextView + + imm = getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager + + if (peerURI == "") { + title.text = "Message to ..." + receiver.threshold = 2 + receiver.setAdapter(ArrayAdapter(this, android.R.layout.select_dialog_item, + ContactsActivity.contacts.map { Contact -> Contact.name })) + receiver.requestFocus() + } else { + title.text = "Reply to ..." + receiver.setText(ContactsActivity.contactName(peerURI), false) + message.requestFocus() + } + imm.toggleSoftInput(InputMethodManager.SHOW_FORCED, InputMethodManager.HIDE_IMPLICIT_ONLY) + + sendButton = findViewById(R.id.sendButton) as ImageButton + sendButton.setOnClickListener { + val receiverText = receiver.text.toString() + val msg = message.text.toString() + if (receiverText.length > 0) { + var uri = ContactsActivity.findContactURI(receiverText) + if (!uri.startsWith("sip:")) uri = "sip:$uri" + if (!uri.contains("@")) { + val host = aor.substring(aor.indexOf("@") + 1) + uri = "$uri@$host" + } + if (msg.length > 0) { + imm.hideSoftInputFromWindow(receiver.getWindowToken(), 0) + imm.hideSoftInputFromWindow(message.getWindowToken(), 0) + val res = message_send(ua!!.uap, uri, msg) + if (res != 0) { + Toast.makeText(getApplicationContext(), "Sending of message failed!", + Toast.LENGTH_SHORT).show() + } else { + val new_message = Message(aor, uri, R.drawable.arrow_up_green, msg, + System.currentTimeMillis(), false) + MainActivity.messages.add(new_message) + MessagesActivity.uaMessages.add(0, new_message) + val i = Intent() + setResult(Activity.RESULT_OK, i) + finish() + } + } + } + } + } + + override fun onOptionsItemSelected(item: MenuItem): Boolean { + when (item.itemId) { + android.R.id.home -> { + imm.hideSoftInputFromWindow(receiver.getWindowToken(), 0) + imm.hideSoftInputFromWindow(message.getWindowToken(), 0) + val i = Intent() + setResult(Activity.RESULT_CANCELED, i) + finish() + } + } + return true + } + + external fun message_send(uap: String, peer_uri: String, message: String) : Int + +} diff --git a/app/src/main/kotlin/com/tutpro/baresip/MessageListAdapter.kt b/app/src/main/kotlin/com/tutpro/baresip/MessageListAdapter.kt new file mode 100644 index 00000000..b381a6c8 --- /dev/null +++ b/app/src/main/kotlin/com/tutpro/baresip/MessageListAdapter.kt @@ -0,0 +1,47 @@ +package com.tutpro.baresip + +import android.content.Context +import android.graphics.Typeface +import android.text.format.DateUtils.isToday +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.text.SimpleDateFormat +import java.util.* + +class MessageListAdapter(private val cxt: Context, private val rows: ArrayList) : + ArrayAdapter(cxt, R.layout.message, rows) { + + override fun getView(position: Int, convertView: View?, parent: ViewGroup): View { + val message = rows[position] + val inflater = context.getSystemService(Context.LAYOUT_INFLATER_SERVICE) as LayoutInflater + val messageView = inflater.inflate(R.layout.message, parent, false) + val directionView = messageView.findViewById(R.id.direction) as ImageView + directionView.setImageResource(message.direction) + val peerView = messageView.findViewById(R.id.peer) as TextView + peerView.text = ContactsActivity.contactName(message.peerURI) + val timeView = messageView.findViewById(R.id.time) as TextView + val time: String + val cal = GregorianCalendar() + cal.timeInMillis = message.timeStamp + if (isToday(message.timeStamp)) { + val fmt = SimpleDateFormat("HH:mm") + time = fmt.format(cal.time) + } else { + val fmt = SimpleDateFormat("dd.MM") + time = fmt.format(cal.time) + } + timeView.text = time + val textView = messageView.findViewById(R.id.text) as TextView + textView.text = message.message + if (message.new) { + textView.setTypeface(null, Typeface.BOLD) + message.new = false + } + return messageView + } + +} diff --git a/app/src/main/kotlin/com/tutpro/baresip/MessagesActivity.kt b/app/src/main/kotlin/com/tutpro/baresip/MessagesActivity.kt new file mode 100644 index 00000000..03a6ae70 --- /dev/null +++ b/app/src/main/kotlin/com/tutpro/baresip/MessagesActivity.kt @@ -0,0 +1,166 @@ +package com.tutpro.baresip + +import android.app.Activity +import android.content.DialogInterface +import android.content.Intent +import android.os.Bundle +import android.support.v7.app.AlertDialog +import android.support.v7.app.AppCompatActivity +import android.util.Log +import android.view.MenuItem +import android.widget.AdapterView +import android.widget.ImageButton +import android.widget.ListView +import java.io.File +import java.io.FileOutputStream +import java.io.IOException +import java.io.ObjectOutputStream +import java.util.* + +class MessagesActivity: AppCompatActivity() { + + internal lateinit var aor: String + internal lateinit var mlAdapter: MessageListAdapter + internal lateinit var plusButton: ImageButton + + public override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + setContentView(R.layout.activity_messages) + + val listView = findViewById(R.id.message) as ListView + plusButton = findViewById(R.id.plusButton) as ImageButton + + aor = intent.extras.getString("aor") + uaMessages = uaMessages(aor) + + mlAdapter = MessageListAdapter(this, uaMessages) + listView.adapter = mlAdapter + listView.isLongClickable = true + + listView.onItemClickListener = AdapterView.OnItemClickListener { _, _, pos, _ -> + val i = Intent(this@MessagesActivity, MessageActivity::class.java) + val b = Bundle() + b.putString("aor", aor) + b.putString("peer", uaMessages[pos].peerURI) + i.putExtras(b) + startActivityForResult(i, MainActivity.MESSAGE_CODE) + } + + listView.onItemLongClickListener = AdapterView.OnItemLongClickListener { _, _, pos, _ -> + val dialogClickListener = DialogInterface.OnClickListener { _, which -> + when (which) { + DialogInterface.BUTTON_NEGATIVE -> { + MainActivity.messages.remove(uaMessages[pos]) + uaMessages.removeAt(pos) + if (uaMessages.size == 0) { + val i = Intent() + setResult(Activity.RESULT_CANCELED, i) + finish() + } + mlAdapter.notifyDataSetChanged() + } + DialogInterface.BUTTON_POSITIVE -> { + } + } + } + + val builder = AlertDialog.Builder(this@MessagesActivity, + R.style.Theme_AppCompat) + builder.setMessage("Delete message from " + + ContactsActivity.contactName(uaMessages[pos].peerURI) + "?") + .setPositiveButton("Cancel", dialogClickListener) + .setNegativeButton("Delete", dialogClickListener) + .show() + true + } + + plusButton.setOnClickListener { + val i = Intent(this@MessagesActivity, MessageActivity::class.java) + val b = Bundle() + b.putString("aor", aor) + b.putString("peer", "") + i.putExtras(b) + startActivityForResult(i, MainActivity.MESSAGE_CODE) + } + + val peer = intent.extras.getString("peer") + if (peer != null) { + val i = Intent(this@MessagesActivity, MessageActivity::class.java) + val b = Bundle() + b.putString("aor", aor) + b.putString("peer", peer) + i.putExtras(b) + startActivityForResult(i, MainActivity.MESSAGE_CODE) + } + + } + + override fun onOptionsItemSelected(item: MenuItem): Boolean { + when (item.itemId) { + android.R.id.home -> { + Log.d("Baresip", "Back array was pressed at Messages") + val i = Intent() + setResult(Activity.RESULT_CANCELED, i) + finish() + } + } + return true + } + + override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { + when (requestCode) { + MainActivity.MESSAGE_CODE -> { + if (resultCode == RESULT_OK) { + mlAdapter.notifyDataSetChanged() + } + } + } + } + + private fun uaMessages(aor: String) : ArrayList { + val res = ArrayList() + for (m in MainActivity.messages.reversed()) { + if (m.aor == aor) { + res.add(m) + } + } + return res + } + + companion object { + + var uaMessages = ArrayList() + + fun archiveUaMessage(aor: String, time: Long) { + for (i in MainActivity.messages.indices.reversed()) + if ((MainActivity.messages[i].aor == aor) && + (MainActivity.messages[i].timeStamp == time)) { + MainActivity.messages[i].new = false + return + } + } + + fun deleteUaMessage(aor: String, time: Long) { + for (i in MainActivity.messages.indices.reversed()) + if ((MainActivity.messages[i].aor == aor) && + (MainActivity.messages[i].timeStamp == time)) { + MainActivity.messages.removeAt(i) + return + } + } + + fun saveMessages() { + val file = File(MainActivity.filesPath, "messages") + try { + val fos = FileOutputStream(file) + val oos = ObjectOutputStream(fos) + oos.writeObject(MainActivity.messages) + oos.close() + fos.close() + } catch (e: IOException) { + Log.w("Baresip", "OutputStream exception: " + e.toString()) + e.printStackTrace() + } + } + } +} \ No newline at end of file diff --git a/app/src/main/kotlin/com/tutpro/baresip/RunOnStartup.kt b/app/src/main/kotlin/com/tutpro/baresip/RunOnStartup.kt index 02f15778..fb811d78 100644 --- a/app/src/main/kotlin/com/tutpro/baresip/RunOnStartup.kt +++ b/app/src/main/kotlin/com/tutpro/baresip/RunOnStartup.kt @@ -11,7 +11,7 @@ class RunOnStartup : BroadcastReceiver() { override fun onReceive(context: Context, intent: Intent) { if ((intent.action == Intent.ACTION_BOOT_COMPLETED) or (intent.action == "com.tutpro.baresip.Restart")) { - Log.d("Baresip", "Start baresip upon boot completed or restart") + Log.d("Baresip", "Start baresip upon boot completed or restart") val i = Intent(context, MainActivity::class.java) i.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) val b = Bundle() diff --git a/app/src/main/res/drawable/calls.xml b/app/src/main/res/drawable/calls.xml new file mode 100644 index 00000000..6095689a --- /dev/null +++ b/app/src/main/res/drawable/calls.xml @@ -0,0 +1,5 @@ + + + diff --git a/app/src/main/res/drawable/clock.png b/app/src/main/res/drawable/clock.png deleted file mode 100644 index b4fa3631..00000000 Binary files a/app/src/main/res/drawable/clock.png and /dev/null differ diff --git a/app/src/main/res/drawable/messages.xml b/app/src/main/res/drawable/messages.xml new file mode 100644 index 00000000..5e3cf2ad --- /dev/null +++ b/app/src/main/res/drawable/messages.xml @@ -0,0 +1,5 @@ + + + diff --git a/app/src/main/res/drawable/plus.xml b/app/src/main/res/drawable/plus.xml new file mode 100644 index 00000000..3c1f7c0b --- /dev/null +++ b/app/src/main/res/drawable/plus.xml @@ -0,0 +1,5 @@ + + + diff --git a/app/src/main/res/drawable/send.xml b/app/src/main/res/drawable/send.xml new file mode 100644 index 00000000..b4200017 --- /dev/null +++ b/app/src/main/res/drawable/send.xml @@ -0,0 +1,5 @@ + + + diff --git a/app/src/main/res/layout/activity_main.xml b/app/src/main/res/layout/activity_main.xml index ede58845..18273238 100644 --- a/app/src/main/res/layout/activity_main.xml +++ b/app/src/main/res/layout/activity_main.xml @@ -100,16 +100,30 @@ + + + + android:visibility="visible" > diff --git a/app/src/main/res/layout/activity_message.xml b/app/src/main/res/layout/activity_message.xml new file mode 100644 index 00000000..3c661bf2 --- /dev/null +++ b/app/src/main/res/layout/activity_message.xml @@ -0,0 +1,50 @@ + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/layout/activity_messages.xml b/app/src/main/res/layout/activity_messages.xml new file mode 100644 index 00000000..61b01871 --- /dev/null +++ b/app/src/main/res/layout/activity_messages.xml @@ -0,0 +1,29 @@ + + + + + + + + + + + diff --git a/app/src/main/res/layout/message.xml b/app/src/main/res/layout/message.xml new file mode 100644 index 00000000..7d3e1939 --- /dev/null +++ b/app/src/main/res/layout/message.xml @@ -0,0 +1,51 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 23bbc2ef..b965faf2 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -36,4 +36,5 @@ EMPTY value means that media encryption is not supported and that media transport protocol thus is RTP/AVP. + MessageActivity