diff --git a/app/build.gradle b/app/build.gradle index 3d2ba6e0..c42ef38f 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -1,5 +1,6 @@ apply plugin: 'com.android.application' apply plugin: 'kotlin-android' +apply plugin: 'kotlin-kapt' android { compileSdkVersion = 32 @@ -23,6 +24,7 @@ android { lintOptions { checkDependencies true } + vectorDrawables.useSupportLibrary = true } splits { abi { @@ -38,6 +40,7 @@ android { } buildFeatures { viewBinding true + dataBinding true } sourceSets { main.java.srcDirs += 'src/main/kotlin' @@ -52,7 +55,7 @@ android { dependencies { implementation fileTree(include: ['*.jar'], dir: 'libs') - implementation 'androidx.appcompat:appcompat:1.5.0' + implementation 'androidx.appcompat:appcompat:1.5.1' implementation 'androidx.constraintlayout:constraintlayout:2.1.4' implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlin_version" implementation 'com.google.android.material:material:1.6.1' 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..08d11f24 --- /dev/null +++ b/app/src/main/kotlin/com/tutpro/baresip/Codec.kt @@ -0,0 +1,3 @@ +package com.tutpro.baresip + +data class Codec(val name: String, var enabled: Boolean) \ No newline at end of file diff --git a/app/src/main/kotlin/com/tutpro/baresip/CodecsActivity.kt b/app/src/main/kotlin/com/tutpro/baresip/CodecsActivity.kt index e898b592..4b8a0ea8 100644 --- a/app/src/main/kotlin/com/tutpro/baresip/CodecsActivity.kt +++ b/app/src/main/kotlin/com/tutpro/baresip/CodecsActivity.kt @@ -1,22 +1,26 @@ package com.tutpro.baresip +import android.annotation.SuppressLint import android.os.Bundle import android.view.Menu import android.view.MenuItem -import android.view.View -import android.widget.* -import android.widget.LinearLayout.LayoutParams import androidx.appcompat.app.AppCompatActivity - +import androidx.recyclerview.widget.ItemTouchHelper +import androidx.recyclerview.widget.ItemTouchHelper.* +import androidx.recyclerview.widget.LinearLayoutManager +import androidx.recyclerview.widget.RecyclerView import com.tutpro.baresip.databinding.ActivityCodecsBinding +import java.util.* class CodecsActivity : AppCompatActivity() { private lateinit var binding: ActivityCodecsBinding private lateinit var acc: Account private lateinit var ua: UserAgent + private lateinit var codecsAdapter: CodecsAdapter + private var aor = "" - private var newCodecs = ArrayList() + private var newCodecs = ArrayList() private var media = "" override fun onCreate(savedInstanceState: Bundle?) { @@ -35,68 +39,90 @@ class CodecsActivity : AppCompatActivity() { ua = UserAgent.ofAor(aor)!! acc = ua.account - val codecs: ArrayList + val allCodecs: ArrayList val accCodecs: ArrayList - val title = binding.CodecsTitle + val codecsTitle = binding.CodecsTitle + val codecList = binding.CodecList + + itemTouchHelper.attachToRecyclerView(codecList) if (media == "audio") { - title.text = getString(R.string.audio_codecs) - codecs = ArrayList(Api.audio_codecs().split(",")) + codecsTitle.text = getString(R.string.audio_codecs) + allCodecs = ArrayList(Api.audio_codecs().split(",")) accCodecs = acc.audioCodec } else { - title.text = getString(R.string.video_codecs) - codecs = ArrayList(Api.video_codecs().split(",").distinct()) + codecsTitle.text = getString(R.string.video_codecs) + allCodecs = ArrayList(Api.video_codecs().split(",").distinct()) accCodecs = acc.videoCodec } - newCodecs.addAll(accCodecs) + for (codec in accCodecs) + newCodecs.add(Codec(codec, true)) + for (codec in allCodecs) + if (codec !in accCodecs) + newCodecs.add(Codec(codec, false)) - while (newCodecs.size < codecs.size) newCodecs.add("-") + codecsAdapter = CodecsAdapter(newCodecs) - val layout = binding.SpinnerTable - val spinnerList = Array(codecs.size) { ArrayList() } - for (i in codecs.indices) { - val spinner = Spinner(applicationContext) - spinner.id = i + 100 - spinner.layoutParams = TableRow.LayoutParams(LayoutParams.MATCH_PARENT, - LayoutParams.WRAP_CONTENT) - spinner.layoutParams.height = 75 - layout.addView(spinner) - if (accCodecs.size > i) { - val codec = accCodecs[i] - spinnerList[i].add(codec) - spinnerList[i].add("-") - for (c in codecs) if (c != codec) spinnerList[i].add(c) - } else { - spinnerList[i].addAll(codecs) - spinnerList[i].add(0, "-") - } - val codecSpinner = findViewById(spinner.id) - val adapter = ArrayAdapter(this, android.R.layout.simple_spinner_item, - spinnerList[i]) - 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[parent.id - 100] = parent.selectedItem.toString() - } - override fun onNothingSelected(parent: AdapterView<*>) { + codecList.layoutManager = LinearLayoutManager(this) + codecList.adapter = codecsAdapter + + codecsAdapter.setOnItemClickListener(object : CodecsAdapter.OnItemClickListener { + @SuppressLint("NotifyDataSetChanged") + override fun onItemClick(position: Int) { + if (!newCodecs[position].enabled) { + codecsAdapter.enableItem(position) + codecsAdapter.notifyDataSetChanged() } } - } + }) - binding.CodecsTitle.setOnClickListener { + itemTouchHelper.attachToRecyclerView(binding.CodecList) + + codecsTitle.setOnClickListener { if (media == "audio") Utils.alertView(this, getString(R.string.audio_codecs), - getString(R.string.audio_codecs_help)) + getString(R.string.audio_codecs_help)) else Utils.alertView(this, getString(R.string.video_codecs), - getString(R.string.video_codecs_help)) + getString(R.string.video_codecs_help)) } } + private val itemTouchHelper by lazy { + val simpleItemTouchCallback = object : ItemTouchHelper.SimpleCallback( + UP or DOWN or START or END, + RIGHT + ) { + override fun onMove( + recyclerView: RecyclerView, + viewHolder: RecyclerView.ViewHolder, + target: RecyclerView.ViewHolder + ): Boolean { + val fromPosition = viewHolder.adapterPosition + val toPosition = target.adapterPosition + codecsAdapter.moveItem(fromPosition, toPosition) + codecsAdapter.notifyItemMoved(fromPosition, toPosition) + return true + } + + override fun onSwiped(viewHolder: RecyclerView.ViewHolder, direction: Int) { + codecsAdapter.removeItem(viewHolder.adapterPosition) + codecsAdapter.notifyItemRemoved(viewHolder.adapterPosition) + } + + @SuppressLint("NotifyDataSetChanged") + override fun clearView(recyclerView: RecyclerView, viewHolder: RecyclerView.ViewHolder) { + super.clearView(recyclerView, viewHolder) + recyclerView.post { codecsAdapter.notifyDataSetChanged() } + } + } + + ItemTouchHelper(simpleItemTouchCallback) + } + override fun onCreateOptionsMenu(menu: Menu): Boolean { super.onCreateOptionsMenu(menu) @@ -116,28 +142,33 @@ class CodecsActivity : AppCompatActivity() { R.id.checkIcon -> { var save = false - val mc = ArrayList(LinkedHashSet(newCodecs.filter { it != "-" } as ArrayList)) - val mcList = Utils.implode(mc, ",") + val codecs = ArrayList() + + for (codec in newCodecs) + if (codec.enabled) + codecs.add(codec.name) + + val codecList = Utils.implode(codecs, ",") if (media == "audio") - if (mc != acc.audioCodec) { - if (Api.account_set_audio_codecs(acc.accp, mcList) == 0) { - Log.d(TAG, "New audio codecs '$mcList'") - acc.audioCodec = mc + if (codecs != acc.audioCodec) { + if (Api.account_set_audio_codecs(acc.accp, codecList) == 0) { + Log.d(TAG, "New audio codecs '$codecList'") + acc.audioCodec = codecs save = true } else { - Log.e(TAG, "Setting of audio codecs '$mcList' failed") + Log.e(TAG, "Setting of audio codecs '$codecList' failed") } } if (media == "video") - if (mc != acc.videoCodec) { - if (Api.account_set_video_codecs(acc.accp, mcList) == 0) { - Log.d(TAG, "New video codecs '$mcList'") - acc.videoCodec = mc + if (codecs != acc.videoCodec) { + if (Api.account_set_video_codecs(acc.accp, codecList) == 0) { + Log.d(TAG, "New video codecs '$codecs'") + acc.videoCodec = codecs save = true } else { - Log.e(TAG, "Setting of video codecs '$mcList' failed") + Log.e(TAG, "Setting of video codecs '$codecs' failed") } } diff --git a/app/src/main/kotlin/com/tutpro/baresip/CodecsAdapter.kt b/app/src/main/kotlin/com/tutpro/baresip/CodecsAdapter.kt new file mode 100644 index 00000000..00e162f8 --- /dev/null +++ b/app/src/main/kotlin/com/tutpro/baresip/CodecsAdapter.kt @@ -0,0 +1,78 @@ +package com.tutpro.baresip + +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import android.widget.ImageView +import android.widget.TextView +import androidx.recyclerview.widget.RecyclerView +import java.util.* + +class CodecsAdapter(private val codecs: ArrayList): + RecyclerView.Adapter() { + + private var codecListener: OnItemClickListener? = null + + interface OnItemClickListener { + fun onItemClick(position: Int) + } + + fun setOnItemClickListener(listener: OnItemClickListener) { + codecListener = listener + } + + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): CodecViewHolder { + val v = LayoutInflater.from(parent.context).inflate(R.layout.codec, parent, false) + return CodecViewHolder(v, codecListener!!) + } + + override fun getItemCount(): Int { + return codecs.size + } + + fun moveItem(fromPosition: Int, toPosition: Int) { + val codec: Codec = codecs[fromPosition] + codec.enabled = codecs[toPosition].enabled + codecs.removeAt(fromPosition) + codecs.add(toPosition, codec) + } + + fun removeItem(position: Int) { + val codec: Codec = codecs[position] + if (codec.enabled) { + codec.enabled = false + codecs.removeAt(position) + codecs.add(codec) + } + } + + fun enableItem(position: Int) { + val codec: Codec = codecs[position] + codec.enabled = true + codecs.removeAt(position) + codecs.add(0, codec) + } + + override fun onBindViewHolder(holder: CodecViewHolder, position: Int) { + val codec = codecs[position] + holder.codecName.text = codec.name + holder.codecName.alpha = if (codec.enabled) 1.0f else 0.5f + holder.actionImage.setImageResource( + if (codec.enabled) R.drawable.reorder else R.drawable.add + ) + } + + class CodecViewHolder(itemView: View, listener: OnItemClickListener): + RecyclerView.ViewHolder(itemView) { + var codecName: TextView = itemView.findViewById(R.id.codecName) + var actionImage: ImageView = itemView.findViewById(R.id.actionImage) + init { + itemView.setOnClickListener { + val position = adapterPosition + if (position != RecyclerView.NO_POSITION) { + listener.onItemClick(position) + } + } + } + } +} diff --git a/app/src/main/kotlin/com/tutpro/baresip/MainActivity.kt b/app/src/main/kotlin/com/tutpro/baresip/MainActivity.kt index 52dea8f0..e2ebf4c7 100644 --- a/app/src/main/kotlin/com/tutpro/baresip/MainActivity.kt +++ b/app/src/main/kotlin/com/tutpro/baresip/MainActivity.kt @@ -38,7 +38,6 @@ import java.io.File import java.net.URLDecoder import kotlin.system.exitProcess - class MainActivity : AppCompatActivity() { private lateinit var binding: ActivityMainBinding diff --git a/app/src/main/res/drawable/add.xml b/app/src/main/res/drawable/add.xml new file mode 100644 index 00000000..85b60d8b --- /dev/null +++ b/app/src/main/res/drawable/add.xml @@ -0,0 +1,8 @@ + + + diff --git a/app/src/main/res/drawable/reorder.xml b/app/src/main/res/drawable/reorder.xml new file mode 100644 index 00000000..57af84b5 --- /dev/null +++ b/app/src/main/res/drawable/reorder.xml @@ -0,0 +1,10 @@ + + + diff --git a/app/src/main/res/layout/activity_account.xml b/app/src/main/res/layout/activity_account.xml index ee2a216d..584a29b2 100644 --- a/app/src/main/res/layout/activity_account.xml +++ b/app/src/main/res/layout/activity_account.xml @@ -140,6 +140,7 @@ android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_alignParentStart="true" + android:layout_centerVertical="true" android:layout_toStartOf="@id/Register" android:textSize="18sp" android:text="@string/register" > @@ -150,6 +151,7 @@ android:layout_height="wrap_content" android:layout_alignParentEnd="true" android:layout_gravity="end" + android:layout_centerVertical="true" android:checked="false" > @@ -367,6 +369,7 @@ android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_alignParentStart="true" + android:layout_centerVertical="true" android:layout_toStartOf="@id/Default" android:textSize="18sp" android:text="@string/default_account" > @@ -376,6 +379,7 @@ android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_alignParentEnd="true" + android:layout_centerVertical="true" android:layout_gravity="end" android:checked="false" > diff --git a/app/src/main/res/layout/activity_codecs.xml b/app/src/main/res/layout/activity_codecs.xml index f8062030..a580e1a1 100644 --- a/app/src/main/res/layout/activity_codecs.xml +++ b/app/src/main/res/layout/activity_codecs.xml @@ -17,11 +17,11 @@ android:text="@string/audio_codecs" > - - + android:layout_marginTop="32dp" + android:scrollbars="vertical" /> diff --git a/app/src/main/res/layout/codec.xml b/app/src/main/res/layout/codec.xml new file mode 100644 index 00000000..6686dc4b --- /dev/null +++ b/app/src/main/res/layout/codec.xml @@ -0,0 +1,36 @@ + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/values-fi/strings.xml b/app/src/main/res/values-fi/strings.xml index 0c2b53a0..e8590075 100644 --- a/app/src/main/res/values-fi/strings.xml +++ b/app/src/main/res/values-fi/strings.xml @@ -143,12 +143,6 @@ Jos merkitty, rekisteröinti on aktiivinen ja REGISTER-sanomat lähetetään noin 12 minuutin välein. - Audion koodausmenetelmät - Luettelo käytössä olevista audion - koodausmenetelmistä prioriteettijärjestyksessä - Videon koodausmenetelmät - Luettelo käytössä olevista videon - koodausmenetelmistä prioriteettijärjestyksessä Media NAT hallinta Valitsee media NAT hallintaprotokollan (vapaaehtoinen). Vaihtoehtoja ovat STUN (Session Traversal Utilities @@ -291,6 +285,14 @@ Haluatko poistaa viestiketjun \'%1$s\'\? Tyhjennä Haluatko tyhjentää tilin \'%1$s\' viestihistorian\? + + Audion koodausmenetelmät + Luettelo käytössä olevista audion + koodausmenetelmistä prioriteettijärjestyksessä + Videon koodausmenetelmät + Luettelo käytössä olevista videon + koodausmenetelmistä prioriteettijärjestyksessä + Koodausmenetelmä toiminto (järjestä tai lisää) Asetukset Käynnistä automaattisesti diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index c1b10ee9..ccea2f53 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -144,10 +144,6 @@ If checked, registration is enabled and REGISTER requests are sent at 12 minute intervals. - Audio Codecs - List of supported audio codecs in priority order - Video Codecs - List of supported video codecs in priority order Media NAT Traversal Selects media NAT traversal protocol (if any). Possible choices are STUN (Session Traversal Utilities for NAT, RFC 5389) and ICE (Interactive Connectivity @@ -281,6 +277,12 @@ Do you want to delete chat with \'%1$s\'\? Delete Do you want to delete chat history of account \'%1$s\'\? + + Audio Codecs + List of supported audio codecs in priority order + Video Codecs + List of supported video codecs in priority order + Codec action (reorder or add) Settings Start Automatically